// SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol"; import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol"; import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "./utils/EntropyTestUtils.t.sol"; import "../contracts/entropy/EntropyUpgradable.sol"; // TODO // - fuzz test? contract EntropyTest is Test, EntropyTestUtils, EntropyEvents { ERC1967Proxy public proxy; EntropyUpgradable public random; uint128 pythFeeInWei = 7; address public provider1 = address(1); bytes32[] provider1Proofs; uint128 provider1FeeInWei = 8; uint64 provider1ChainLength = 100; bytes provider1Uri = bytes("https://foo.com"); bytes provider1CommitmentMetadata = hex"0100"; address public provider2 = address(2); bytes32[] provider2Proofs; uint128 provider2FeeInWei = 20; bytes provider2Uri = bytes("https://bar.com"); address public user1 = address(3); address public user2 = address(4); address public unregisteredProvider = address(7); uint128 MAX_UINT128 = 2 ** 128 - 1; bytes32 ALL_ZEROS = bytes32(uint256(0)); address public owner = address(8); address public admin = address(9); address public admin2 = address(10); function setUp() public { EntropyUpgradable _random = new EntropyUpgradable(); // deploy proxy contract and point it to implementation proxy = new ERC1967Proxy(address(_random), ""); // wrap in ABI to support easier calls random = EntropyUpgradable(address(proxy)); random.initialize(owner, admin, pythFeeInWei, provider1, false); bytes32[] memory hashChain1 = generateHashChain( provider1, 0, provider1ChainLength ); provider1Proofs = hashChain1; vm.prank(provider1); random.register( provider1FeeInWei, provider1Proofs[0], provider1CommitmentMetadata, provider1ChainLength, provider1Uri ); bytes32[] memory hashChain2 = generateHashChain(provider2, 0, 100); provider2Proofs = hashChain2; vm.prank(provider2); random.register( provider2FeeInWei, provider2Proofs[0], hex"0200", 100, provider2Uri ); } // Test helper method for requesting a random value as user from provider. function request( address user, address provider, uint randomNumber, bool useBlockhash ) public returns (uint64 sequenceNumber) { sequenceNumber = requestWithFee( user, random.getFee(provider), provider, randomNumber, useBlockhash ); } function requestWithFee( address user, uint fee, address provider, uint randomNumber, bool useBlockhash ) public returns (uint64 sequenceNumber) { vm.deal(user, fee); vm.startPrank(user); sequenceNumber = random.request{value: fee}( provider, random.constructUserCommitment(bytes32(randomNumber)), useBlockhash ); vm.stopPrank(); } function assertRequestReverts( uint fee, address provider, uint randomNumber, bool useBlockhash ) public { // Note: for some reason vm.expectRevert() won't catch errors from the request function (?!), // even though they definitely revert. Use a try/catch instead for the moment, though the try/catch // doesn't let you simulate the msg.sender. However, it's fine if the msg.sender is the test contract. bool requestSucceeds = false; try random.request{value: fee}( provider, random.constructUserCommitment(bytes32(uint256(randomNumber))), useBlockhash ) { requestSucceeds = true; } catch { requestSucceeds = false; } assert(!requestSucceeds); } function assertRevealSucceeds( address user, address provider, uint64 sequenceNumber, uint userRandom, bytes32 providerRevelation, bytes32 hash ) public { vm.prank(user); bytes32 randomNumber = random.reveal( provider, sequenceNumber, bytes32(userRandom), providerRevelation ); assertEq( randomNumber, random.combineRandomValues( bytes32(userRandom), providerRevelation, hash ) ); } function assertRevealReverts( address user, address provider, uint64 sequenceNumber, uint userRandom, bytes32 providerRevelation ) public { vm.startPrank(user); vm.expectRevert(); random.reveal( provider, sequenceNumber, bytes32(uint256(userRandom)), providerRevelation ); vm.stopPrank(); } function assertInvariants() public { uint expectedBalance = random .getProviderInfo(provider1) .accruedFeesInWei + random.getProviderInfo(provider2).accruedFeesInWei + random.getAccruedPythFees(); assertEq(address(random).balance, expectedBalance); EntropyStructs.ProviderInfo memory info1 = random.getProviderInfo( provider1 ); assert( info1.originalCommitmentSequenceNumber <= info1.currentCommitmentSequenceNumber ); assert(info1.currentCommitmentSequenceNumber < info1.sequenceNumber); assert(info1.sequenceNumber <= info1.endSequenceNumber); EntropyStructs.ProviderInfo memory info2 = random.getProviderInfo( provider2 ); assert( info2.originalCommitmentSequenceNumber <= info2.currentCommitmentSequenceNumber ); assert(info2.sequenceNumber > info2.currentCommitmentSequenceNumber); assert(info2.sequenceNumber <= info2.endSequenceNumber); } function testBasicFlow() public { vm.roll(17); uint64 sequenceNumber = request(user2, provider1, 42, false); assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 17); assertEq( random.getRequest(provider1, sequenceNumber).useBlockhash, false ); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); EntropyStructs.Request memory reqAfterReveal = random.getRequest( provider1, sequenceNumber ); assertEq(reqAfterReveal.sequenceNumber, 0); // You can only reveal the random number once. This isn't a feature of the contract per se, but it is // the expected behavior. assertRevealReverts( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber] ); } function testDefaultProvider() public { vm.roll(20); uint64 sequenceNumber = request( user2, random.getDefaultProvider(), 42, false ); assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 20); assertEq( random.getRequest(provider1, sequenceNumber).useBlockhash, false ); assertRevealReverts( user2, random.getDefaultProvider(), sequenceNumber, 42, provider2Proofs[sequenceNumber] ); assertRevealSucceeds( user2, random.getDefaultProvider(), sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); } function testNoSuchProvider() public { assertRequestReverts(10000000, unregisteredProvider, 42, false); } function testAuthorization() public { uint64 sequenceNumber = request(user2, provider1, 42, false); assertEq(random.getRequest(provider1, sequenceNumber).requester, user2); // user1 not authorized, must be user2. assertRevealReverts( user1, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber] ); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); } function testAdversarialReveal() public { uint64 sequenceNumber = request(user2, provider1, 42, false); // test revealing with the wrong hashes in the same chain for (uint256 i = 0; i < 10; i++) { if (i != sequenceNumber) { assertRevealReverts( user2, provider1, sequenceNumber, 42, provider1Proofs[i] ); } } // test revealing with the wrong user revealed value. for (uint256 i = 0; i < 42; i++) { assertRevealReverts( user2, provider1, sequenceNumber, i, provider1Proofs[sequenceNumber] ); } // test revealing sequence numbers that haven't been requested yet. for (uint64 i = sequenceNumber + 1; i < sequenceNumber + 3; i++) { assertRevealReverts( user2, provider1, i, 42, provider1Proofs[sequenceNumber] ); assertRevealReverts(user2, provider1, i, 42, provider1Proofs[i]); } } function testConcurrentRequests() public { uint64 s1 = request(user1, provider1, 1, false); uint64 s2 = request(user2, provider1, 2, false); uint64 s3 = request(user1, provider1, 3, false); uint64 s4 = request(user1, provider1, 4, false); assertRevealSucceeds( user1, provider1, s3, 3, provider1Proofs[s3], ALL_ZEROS ); assertInvariants(); uint64 s5 = request(user1, provider1, 5, false); assertRevealSucceeds( user1, provider1, s4, 4, provider1Proofs[s4], ALL_ZEROS ); assertInvariants(); assertRevealSucceeds( user1, provider1, s1, 1, provider1Proofs[s1], ALL_ZEROS ); assertInvariants(); assertRevealSucceeds( user2, provider1, s2, 2, provider1Proofs[s2], ALL_ZEROS ); assertInvariants(); assertRevealSucceeds( user1, provider1, s5, 5, provider1Proofs[s5], ALL_ZEROS ); assertInvariants(); } function testBlockhash() public { vm.roll(1234); uint64 sequenceNumber = request(user2, provider1, 42, true); assertEq( random.getRequest(provider1, sequenceNumber).blockNumber, 1234 ); assertEq( random.getRequest(provider1, sequenceNumber).useBlockhash, true ); vm.roll(1235); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], blockhash(1234) ); } function testNoCheckOnBlockNumberWhenNoBlockHashUsed() public { vm.roll(1234); uint64 sequenceNumber = request(user2, provider1, 42, false); vm.roll(1236); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); vm.roll(1234); sequenceNumber = request(user2, provider1, 42, false); vm.roll(1234); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); vm.roll(1234); sequenceNumber = request(user2, provider1, 42, false); vm.roll(1234 + 257); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], ALL_ZEROS ); } function testCheckOnBlockNumberWhenBlockHashUsed() public { vm.roll(1234); uint64 sequenceNumber = request(user2, provider1, 42, true); vm.roll(1234); assertRevealReverts( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber] ); vm.roll(1234 + 257); assertRevealReverts( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber] ); vm.roll(1235); assertRevealSucceeds( user2, provider1, sequenceNumber, 42, provider1Proofs[sequenceNumber], blockhash(1234) ); } function testProviderCommitmentRotation() public { uint userRandom = 42; uint64 sequenceNumber1 = request(user2, provider1, userRandom, false); uint64 sequenceNumber2 = request(user2, provider1, userRandom, false); assertInvariants(); uint64 newHashChainOffset = sequenceNumber2 + 1; bytes32[] memory newHashChain = generateHashChain( provider1, newHashChainOffset, 10 ); vm.prank(provider1); random.register( provider1FeeInWei, newHashChain[0], hex"0100", 10, provider1Uri ); assertInvariants(); EntropyStructs.ProviderInfo memory info1 = random.getProviderInfo( provider1 ); assertEq(info1.endSequenceNumber, newHashChainOffset + 10); uint64 sequenceNumber3 = request(user2, provider1, 42, false); // Rotating the provider key uses a sequence number assertEq(sequenceNumber3, sequenceNumber2 + 2); // Requests that were in-flight at the time of rotation use the commitment from the time of request for (uint256 i = 0; i < 10; i++) { assertRevealReverts( user2, provider1, sequenceNumber1, userRandom, newHashChain[i] ); } assertRevealSucceeds( user2, provider1, sequenceNumber1, userRandom, provider1Proofs[sequenceNumber1], ALL_ZEROS ); assertInvariants(); // Requests after the rotation use the new commitment assertRevealReverts( user2, provider1, sequenceNumber3, userRandom, provider1Proofs[sequenceNumber3] ); assertRevealSucceeds( user2, provider1, sequenceNumber3, userRandom, newHashChain[sequenceNumber3 - newHashChainOffset], ALL_ZEROS ); assertInvariants(); } function testOutOfRandomness() public { // Should be able to request chainLength - 1 random numbers successfully. for (uint64 i = 0; i < provider1ChainLength - 1; i++) { request(user1, provider1, i, false); } assertRequestReverts( random.getFee(provider1), provider1, provider1ChainLength - 1, false ); } function testGetFee() public { assertEq(random.getFee(provider1), pythFeeInWei + provider1FeeInWei); assertEq(random.getFee(provider2), pythFeeInWei + provider2FeeInWei); // Requesting the fee for a nonexistent provider returns pythFeeInWei. This isn't necessarily desirable behavior, // but it's unlikely to cause a problem. assertEq(random.getFee(unregisteredProvider), pythFeeInWei); // Check that overflowing the fee arithmetic causes the transaction to revert. vm.prank(provider1); random.register( MAX_UINT128, provider1Proofs[0], hex"0100", 100, provider1Uri ); vm.expectRevert(); random.getFee(provider1); } function testOverflow() public { // msg.value overflows the uint128 fee variable assertRequestReverts(2 ** 128, provider1, 42, false); // block number is too large vm.roll(2 ** 96); assertRequestReverts( pythFeeInWei + provider1FeeInWei, provider1, 42, true ); } function testFees() public { // Insufficient fees causes a revert assertRequestReverts(0, provider1, 42, false); assertRequestReverts( pythFeeInWei + provider1FeeInWei - 1, provider1, 42, false ); assertRequestReverts(0, provider2, 42, false); assertRequestReverts( pythFeeInWei + provider2FeeInWei - 1, provider2, 42, false ); // Accrue some fees for both providers for (uint i = 0; i < 3; i++) { request(user2, provider1, 42, false); } request(user2, provider2, 42, false); // this call overpays for the random number requestWithFee( user2, pythFeeInWei + provider2FeeInWei + 10000, provider2, 42, false ); assertEq( random.getProviderInfo(provider1).accruedFeesInWei, provider1FeeInWei * 3 ); assertEq( random.getProviderInfo(provider2).accruedFeesInWei, provider2FeeInWei * 2 ); assertEq(random.getAccruedPythFees(), pythFeeInWei * 5 + 10000); assertInvariants(); // Reregistering updates the required fees vm.prank(provider1); random.register( 12345, provider1Proofs[0], hex"0100", 100, provider1Uri ); assertRequestReverts(pythFeeInWei + 12345 - 1, provider1, 42, false); requestWithFee(user2, pythFeeInWei + 12345, provider1, 42, false); uint128 providerOneBalance = provider1FeeInWei * 3 + 12345; assertEq( random.getProviderInfo(provider1).accruedFeesInWei, providerOneBalance ); assertInvariants(); vm.prank(unregisteredProvider); vm.expectRevert(); random.withdraw(1000); vm.prank(provider1); random.withdraw(1000); assertEq( random.getProviderInfo(provider1).accruedFeesInWei, providerOneBalance - 1000 ); assertInvariants(); vm.prank(provider1); vm.expectRevert(); random.withdraw(providerOneBalance); } function testGetProviderInfo() public { EntropyStructs.ProviderInfo memory providerInfo1 = random .getProviderInfo(provider1); // These two fields aren't used by the Entropy contract itself -- they're just convenient info to store // on-chain -- so they aren't tested in the other tests. assertEq(providerInfo1.uri, provider1Uri); assertEq(providerInfo1.commitmentMetadata, provider1CommitmentMetadata); } function testSetProviderFee() public { assertNotEq(random.getProviderInfo(provider1).feeInWei, 1); vm.prank(provider1); random.setProviderFee(1); assertEq(random.getProviderInfo(provider1).feeInWei, 1); } function testSetProviderFeeByUnregistered() public { vm.prank(unregisteredProvider); vm.expectRevert(); random.setProviderFee(1); } function testSetProviderUri() public { bytes memory newUri = bytes("https://new.com"); assertNotEq0(random.getProviderInfo(provider1).uri, newUri); vm.prank(provider1); random.setProviderUri(newUri); assertEq0(random.getProviderInfo(provider1).uri, newUri); } function testSetProviderUriByUnregistered() public { bytes memory newUri = bytes("https://new.com"); vm.prank(unregisteredProvider); vm.expectRevert(); random.setProviderUri(newUri); } function testRequestWithCallbackAndReveal() public { bytes32 userRandomNumber = bytes32(uint(42)); uint fee = random.getFee(provider1); EntropyStructs.ProviderInfo memory providerInfo = random .getProviderInfo(provider1); vm.roll(1234); vm.deal(user1, fee); vm.startPrank(user1); vm.expectEmit(false, false, false, true, address(random)); emit RequestedWithCallback( provider1, user1, providerInfo.sequenceNumber, userRandomNumber, EntropyStructs.Request({ provider: provider1, sequenceNumber: providerInfo.sequenceNumber, numHashes: SafeCast.toUint32( providerInfo.sequenceNumber - providerInfo.currentCommitmentSequenceNumber ), commitment: keccak256( bytes.concat( random.constructUserCommitment(userRandomNumber), providerInfo.currentCommitment ) ), blockNumber: 1234, requester: user1, useBlockhash: false, isRequestWithCallback: true }) ); vm.roll(1234); uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}( provider1, userRandomNumber ); assertEq( random.getRequest(provider1, assignedSequenceNumber).requester, user1 ); assertEq( random.getRequest(provider1, assignedSequenceNumber).provider, provider1 ); vm.expectRevert(EntropyErrors.InvalidRevealCall.selector); random.reveal( provider1, assignedSequenceNumber, userRandomNumber, provider1Proofs[assignedSequenceNumber] ); vm.stopPrank(); } function testRequestWithCallbackAndRevealWithCallbackByContract() public { bytes32 userRandomNumber = bytes32(uint(42)); uint fee = random.getFee(provider1); EntropyConsumer consumer = new EntropyConsumer(address(random)); vm.deal(user1, fee); vm.prank(user1); uint64 assignedSequenceNumber = consumer.requestEntropy{value: fee}( userRandomNumber ); EntropyStructs.Request memory req = random.getRequest( provider1, assignedSequenceNumber ); bytes32 blockHash = bytes32(uint256(0)); vm.expectEmit(false, false, false, true, address(random)); emit RevealedWithCallback( req, userRandomNumber, provider1Proofs[assignedSequenceNumber], random.combineRandomValues( userRandomNumber, provider1Proofs[assignedSequenceNumber], 0 ) ); vm.prank(user1); random.revealWithCallback( provider1, assignedSequenceNumber, userRandomNumber, provider1Proofs[assignedSequenceNumber] ); assertEq(consumer.sequence(), assignedSequenceNumber); assertEq(consumer.provider(), provider1); assertEq( consumer.randomness(), random.combineRandomValues( userRandomNumber, provider1Proofs[assignedSequenceNumber], // No blockhash is being used in callback method. As it // is being depreceated. Passing 0 for it. 0 ) ); EntropyStructs.Request memory reqAfterReveal = random.getRequest( provider1, assignedSequenceNumber ); assertEq(reqAfterReveal.sequenceNumber, 0); } function testRequestWithCallbackAndRevealWithCallbackByEoa() public { bytes32 userRandomNumber = bytes32(uint(42)); uint fee = random.getFee(provider1); vm.deal(user1, fee); vm.prank(user1); uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}( provider1, userRandomNumber ); EntropyStructs.Request memory req = random.getRequest( provider1, assignedSequenceNumber ); bytes32 blockHash = bytes32(uint256(0)); vm.expectEmit(false, false, false, true, address(random)); emit RevealedWithCallback( req, userRandomNumber, provider1Proofs[assignedSequenceNumber], random.combineRandomValues( userRandomNumber, provider1Proofs[assignedSequenceNumber], 0 ) ); random.revealWithCallback( provider1, assignedSequenceNumber, userRandomNumber, provider1Proofs[assignedSequenceNumber] ); EntropyStructs.Request memory reqAfterReveal = random.getRequest( provider1, assignedSequenceNumber ); assertEq(reqAfterReveal.sequenceNumber, 0); } function testRequestAndRevealWithCallback() public { uint64 sequenceNumber = request(user2, provider1, 42, false); assertEq(random.getRequest(provider1, sequenceNumber).requester, user2); vm.expectRevert(EntropyErrors.InvalidRevealCall.selector); vm.prank(user2); random.revealWithCallback( provider1, sequenceNumber, bytes32(uint256(42)), provider1Proofs[sequenceNumber] ); } function testRequestWithCallbackAndRevealWithCallbackFailing() public { bytes32 userRandomNumber = bytes32(uint(42)); uint fee = random.getFee(provider1); EntropyConsumerFails consumer = new EntropyConsumerFails( address(random) ); vm.deal(address(consumer), fee); vm.startPrank(address(consumer)); uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}( provider1, userRandomNumber ); vm.expectRevert(); random.revealWithCallback( provider1, assignedSequenceNumber, userRandomNumber, provider1Proofs[assignedSequenceNumber] ); } } contract EntropyConsumer is IEntropyConsumer { uint64 public sequence; bytes32 public randomness; address public entropy; address public provider; constructor(address _entropy) { entropy = _entropy; } function requestEntropy( bytes32 randomNumber ) public payable returns (uint64 sequenceNumber) { address provider = IEntropy(entropy).getDefaultProvider(); sequenceNumber = IEntropy(entropy).requestWithCallback{ value: msg.value }(provider, randomNumber); } function getEntropy() internal view override returns (address) { return entropy; } function entropyCallback( uint64 _sequence, address _provider, bytes32 _randomness ) internal override { sequence = _sequence; provider = _provider; randomness = _randomness; } } contract EntropyConsumerFails is IEntropyConsumer { uint64 public sequence; bytes32 public randomness; address public entropy; constructor(address _entropy) { entropy = _entropy; } function getEntropy() internal view override returns (address) { return entropy; } function entropyCallback( uint64 _sequence, address _provider, bytes32 _randomness ) internal override { revert("Callback failed"); } }