diff --git a/README.md b/README.md index 7aec946..05775d9 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Given that Flow has both Cadence and EVM runtimes, commit-reveal patterns coveri |---|---|---| |[CoinToss.cdc](./contracts/CoinToss.cdc)|[0xd1299e755e8be5e7](https://contractbrowser.com/A.d1299e755e8be5e7.CoinToss)|N/A| |[Xorshift128plus.cdc](./contracts/Xorshift128plus.cdc)|[0xed24dbe901028c5c](https://contractbrowser.com/A.ed24dbe901028c5c.Xorshift128plus)|[0x45caec600164c9e6](https://contractbrowser.com/A.45caec600164c9e6.Xorshift128plus)| -|[CoinToss.sol](./contracts/CoinToss.sol)|[0x5FC8d32690cc91D4c39d9d3abcBD16989F875707](https://evm-testnet.flowscan.io/address/0x5FC8d32690cc91D4c39d9d3abcBD16989F875707?tab=contract_code)|N/A| +|[CoinToss.sol](./contracts/CoinToss.sol)|[0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1](https://evm-testnet.flowscan.io/address/0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1?tab=contract_code)|N/A| ## Further Reading diff --git a/solidity/src/CadenceArchWrapper.sol b/solidity/src/CadenceArchUtils.sol similarity index 89% rename from solidity/src/CadenceArchWrapper.sol rename to solidity/src/CadenceArchUtils.sol index 9756797..35e92a6 100644 --- a/solidity/src/CadenceArchWrapper.sol +++ b/solidity/src/CadenceArchUtils.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.19; * contracts can use this contract to fetch the current Flow block height and fetch random numbers from the Cadence * runtime. */ -abstract contract CadenceArchWrapper { +library CadenceArchUtils { // Cadence Arch pre-compile address address public constant cadenceArch = 0x0000000000000000000000010000000000000001; @@ -45,17 +45,13 @@ abstract contract CadenceArchWrapper { * @param flowHeight The Flow block height for which to get the random source. * @return randomSource The random source for the given Flow block height. */ - function _getRandomSource(uint64 flowHeight) internal view returns (uint64) { + function _getRandomSource(uint64 flowHeight) internal view returns (bytes32) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("getRandomSource(uint64)", flowHeight)); require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching random source"); - // Decode the result as bytes32 and then cast it to uint64 + // Decode the result as bytes32 & return bytes32 result = abi.decode(data, (bytes32)); - - // Convert the bytes32 result to uint64 by casting, taking the least significant 8 bytes - uint64 output = uint64(uint256(result)); - - return output; + return result; } } diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 0387d11..a8d11a8 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -1,31 +1,85 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import {CadenceArchWrapper} from "./CadenceArchWrapper.sol"; +import {CadenceArchUtils} from "./CadenceArchUtils.sol"; +import {Xorshift128plus} from "./Xorshift128plus.sol"; /** * @dev This contract is a base contract for secure consumption of Flow's protocol-native randomness via the Cadence * Arch pre-compile. Implementing contracts benefit from the commit-reveal scheme below, ensuring that callers cannot * revert on undesirable random results. */ -abstract contract CadenceRandomConsumer is CadenceArchWrapper { +abstract contract CadenceRandomConsumer { + using Xorshift128plus for Xorshift128plus.PRG; + // A struct to store the request details struct Request { // The Flow block height at which the request was made uint64 flowHeight; // The EVM block height at which the request was made uint256 evmHeight; + // Whether the request has been fulfilled + bool fulfilled; } // Events - event RandomnessRequested(uint256 requestId, uint64 flowHeight, uint256 evmHeight); - event RandomnessFulfilled(uint256 requestId, uint64 flowHeight, uint256 evmHeight, uint256 randomResult); + event RandomnessRequested(uint256 indexed requestId, uint64 flowHeight, uint256 evmHeight); + event RandomnessSourced(uint256 indexed requestId, uint64 flowHeight, uint256 evmHeight, bytes32 randomSource); + event RandomnessFulfilled(uint256 indexed requestId, uint64 randomResult); // A list of requests where each request is identified by its index in the array Request[] private _requests; // A counter to keep track of the number of requests made, serving as the request ID uint256 private _requestCounter; + /////////////////// + // PUBLIC FUNCTIONS + /////////////////// + + /** + * @dev This method checks if a request can be fulfilled. + * + * @param requestId The ID of the randomness request to check. + * @return canFulfill Whether the request can be fulfilled. + */ + function canFulfillRequest(uint256 requestId) public view returns (bool) { + uint256 requestIndex = requestId - 1; + if (requestIndex >= _requests.length) { + return false; + } + Request storage request = _requests[requestIndex]; + uint64 flowHeight = CadenceArchUtils._flowBlockHeight(); + return !request.fulfilled && request.flowHeight < flowHeight; + } + + ///////////////////// + // INTERNAL FUNCTIONS + ///////////////////// + + /** + * @dev This method returns a ***REVERTIBLE** random number in the range [min, max]. + * NOTE: The fact that this method is revertible means that it should only be used in cases where the initiating + * caller is trusted not to revert on the result. + * + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return random The random number in the range [min, max]. + */ + function _getRevertibleRandomInRange(uint64 min, uint64 max) internal view returns (uint64) { + bytes memory seed = abi.encodePacked(_aggregateRevertibleRandom256()); + bytes memory salt = abi.encodePacked(block.number); + + // Instantiate a PRG with the aggregate bytes and salt with current block number + Xorshift128plus.PRG memory prg; + prg.seed(seed, salt); + + return _getNumberInRange(prg, min, max); + } + + /** + * ----- COMMIT STEP ----- + */ + /** * @dev This method serves as the commit step in this contract's commit-reveal scheme * @@ -45,7 +99,7 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { // Store the heights at which the request was made. Note that the Flow block height and EVM block height are // not the same. But since Flow blocks ultimately determine usage of secure randomness, we gate requests by // Flow block height. - Request memory request = Request(_flowBlockHeight(), block.number); + Request memory request = Request(CadenceArchUtils._flowBlockHeight(), block.number, false); // Store the request in the list of requests _requests.push(request); @@ -56,6 +110,106 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { return requestId; } + /** + * ----- REVEAL STEP ----- + */ + + /** + * @dev This method fulfills a random request and returns a random number as a uint64. + * + * @param requestId The ID of the randomness request to fulfill. + * @return randomResult The random number. + */ + function _fulfillRandomRequest(uint256 requestId) internal returns (uint64) { + bytes memory seed = abi.encodePacked(_fulfillRandomness(requestId)); + bytes memory salt = abi.encodePacked(requestId); + + // Instantiate a PRG, seeding with the random value and salting with the request ID + Xorshift128plus.PRG memory prg; + prg.seed(seed, salt); + + uint64 randomResult = prg.nextUInt64(); + + emit RandomnessFulfilled(requestId, randomResult); + + return randomResult; + } + + /** + * @dev This method fulfills a random request and safely returns an unbiased random number in the range inclusive [min, max]. + * + * @param requestId The ID of the randomness request to fulfill. + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return randomResult The random number in the inclusive range [min, max]. + */ + function _fulfillRandomInRange(uint256 requestId, uint64 min, uint64 max) internal returns (uint64) { + // Ensure that the request is fulfilled at a Flow block height greater than the one at which the request was made + // Get the random source for the Flow block at which the request was made + bytes memory seed = abi.encodePacked(_fulfillRandomness(requestId)); + bytes memory salt = abi.encodePacked(requestId); + + // Instantiate a PRG with the random source and the request ID + Xorshift128plus.PRG memory prg; + prg.seed(seed, salt); + + uint64 randomResult = _getNumberInRange(prg, min, max); // Get a random number in the range [min, max] + + emit RandomnessFulfilled(requestId, randomResult); + + return randomResult; + } + + //////////////////// + // PRIVATE FUNCTIONS + //////////////////// + + /** + * @dev This method returns a number in the range [min, max] from the given value with a variation on rejection + * sampling. + * NOTE: You may be tempted to simply use `value % (max - min + 1)` to get a number in a range. However, this + * method is not secure is susceptible to the modulo bias. This method provides an unbiased alternative for secure + * secure use of randomness. + * + * @param prg The PRG to use for generating the random number. + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return random The random number in the range [min, max]. + */ + function _getNumberInRange(Xorshift128plus.PRG memory prg, uint64 min, uint64 max) private pure returns (uint64) { + require(max > min, "Max must be greater than min"); + + uint64 range = max - min; + uint64 bitsRequired = _mostSignificantBit(range); // Number of bits needed to cover the range + uint256 mask = (1 << bitsRequired) - 1; // Create a bitmask to extract relevant bits + + uint256 shiftLimit = 256 / bitsRequired; // Number of shifts needed to cover 256 bits + uint256 shifts = 0; // Initialize shift counter + + uint64 candidate = 0; // Initialize candidate + uint256 value = prg.nextUInt256(); // Assign the first 256 bits of randomness + + while (true) { + candidate = uint64(value & mask); // Apply the bitmask to extract bits + if (candidate <= range) { + break; // Found a suitable candidate within the target range + } + + // Shift by the number of bits covered by the mask + value = value >> bitsRequired; + shifts++; + + // Get a new value if we've exhausted the current one + if (shifts == shiftLimit) { + value = prg.nextUInt256(); + shifts = 0; + } + } + + // Scale candidate to the range [min, max] + return min + candidate; + } + /** * @dev This method serves as the reveal step in this contract's commit-reveal scheme * @@ -69,27 +223,64 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { * @param requestId The ID of the randomness request to fulfill. * @return randomResult The random value generated from the Flow block. */ - function _fulfillRandomness(uint32 requestId) internal returns (uint64) { + function _fulfillRandomness(uint256 requestId) private returns (bytes32) { // Get the request details. Recall that request IDs are 1-indexed to allow for 0 to be used as an invalid value - uint32 requestIndex = requestId - 1; + uint256 requestIndex = requestId - 1; require(requestIndex < _requests.length, "Invalid request ID - value exceeds the number of existing requests"); - Request memory request = _requests[requestIndex]; - // Ensure that the request is fulfilled at a Flow block height greater than the one at which the request was made - uint64 flowHeight = _flowBlockHeight(); - require(request.flowHeight < flowHeight, "Cannot fulfill request until subsequent Flow network block height"); + // Access & validate the request + Request storage request = _requests[requestIndex]; + _validateRequest(request); + request.fulfilled = true; // Mark the request as fulfilled - // Get the random source for the Flow block at which the request was made - uint64 randomResult = _getRandomSource(request.flowHeight); // returns bytes32 - // Pack the randomResult into a uint64, hashing with the requestId to vary results across shared block heights. - // The random seed returned from Cadence Arch is only 32 bytes. Here only 8 bytes were required, but if an - // implementing contract requires more random bytes, the source should be expanded into any arbitrary number of - // bytes using a PRG. - randomResult = uint64(uint256(keccak256(abi.encodePacked(randomResult, requestId)))); + // Get the random source for the Flow block at which the request was made, emit & return + bytes32 randomSource = CadenceArchUtils._getRandomSource(request.flowHeight); - emit RandomnessFulfilled(requestId, request.flowHeight, request.evmHeight, randomResult); + emit RandomnessSourced(requestId, request.flowHeight, request.evmHeight, randomSource); - // Return the random result - return randomResult; + return randomSource; + } + + /** + * @dev This method aggregates 256 bits of randomness by calling _revertibleRandom() 4 times. + * + * @return randomValue The aggregated 256 bits of randomness. + */ + function _aggregateRevertibleRandom256() private view returns (uint256) { + // Call _revertibleRandom() 4 times to aggregate 256 bits of randomness + uint256 randomValue = uint256(CadenceArchUtils._revertibleRandom()); + randomValue |= (uint256(CadenceArchUtils._revertibleRandom()) << 64); + randomValue |= (uint256(CadenceArchUtils._revertibleRandom()) << 128); + randomValue |= (uint256(CadenceArchUtils._revertibleRandom()) << 192); + return randomValue; + } + + /** + * @dev This method returns the most significant bit of a uint64. + * + * @param x The input value. + * @return bits The most significant bit of the input value. + */ + function _mostSignificantBit(uint64 x) private pure returns (uint64) { + uint64 bits = 0; + while (x > 0) { + x >>= 1; + bits++; + } + return bits; + } + + /** + * @dev This method validates a given request, ensuring that it has not been fulfilled and that the Flow block height + * has passed. + * + * @param request The request to validate. + */ + function _validateRequest(Request storage request) private view { + require(!request.fulfilled, "Request already fulfilled"); + require( + request.flowHeight < CadenceArchUtils._flowBlockHeight(), + "Cannot fulfill request until subsequent Flow network block height" + ); } } diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 3efd1fc..148af9b 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import "./CadenceRandomConsumer.sol"; +import {CadenceRandomConsumer} from "./CadenceRandomConsumer.sol"; /** * @dev This contract is a simple coin toss game where users can place win prizes by flipping a coin as a demonstration @@ -16,8 +16,8 @@ contract CoinToss is CadenceRandomConsumer { // A mapping to store the value sent by the user for each request mapping(uint256 => uint256) public openRequests; - event CoinFlipped(address indexed user, uint256 requestId, uint256 amount); - event CoinRevealed(address indexed user, uint256 requestId, uint8 coinFace, uint256 prize); + event CoinFlipped(address indexed user, uint256 indexed requestId, uint256 amount); + event CoinRevealed(address indexed user, uint256 indexed requestId, uint8 coinFace, uint256 prize); /** * @dev Checks if a user has an open request. @@ -54,9 +54,10 @@ contract CoinToss is CadenceRandomConsumer { // delete the open request from the coinTosses mapping delete coinTosses[msg.sender]; - // fulfill the random request - uint64 randomResult = _fulfillRandomness(uint32(requestId)); - uint8 coinFace = uint8(randomResult % 2); + // fulfill the random request within the inclusive range [0, 1] + // NOTE: Could use % 2 without risk of modulo bias since the range is a multiple of the modulus + // but using _fulfillRandomInRange for demonstration purposes + uint8 coinFace = uint8(_fulfillRandomInRange(requestId, 0, 1)); // get the value sent in the flipCoin function & remove the request from the openRequests mapping uint256 amount = openRequests[requestId]; diff --git a/solidity/src/Xorshift128plus.sol b/solidity/src/Xorshift128plus.sol new file mode 100644 index 0000000..d62026b --- /dev/null +++ b/solidity/src/Xorshift128plus.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @dev This library implements the Xorshift128+ pseudo-random number generator (PRG) algorithm. + */ +library Xorshift128plus { + /** + * @dev While not limited to 128 bits of state, this PRG is largely informed by xorshift128+ + */ + struct PRG { + uint64 state0; + uint64 state1; + } + + /** + * @dev Initializer for PRG struct + * + * @param prg The PRG struct to seed + * @param sourceOfRandomness The entropy bytes used to seed the PRG. It is recommended to use at least 16 + * bytes of entropy. + * @param salt The bytes used to salt the source of randomness + */ + function seed(PRG memory prg, bytes memory sourceOfRandomness, bytes memory salt) internal pure { + require( + sourceOfRandomness.length >= 16, "At least 16 bytes of entropy should be used when initializing the PRG" + ); + bytes memory tmp = abi.encodePacked(sourceOfRandomness, salt); + bytes32 hash = keccak256(tmp); + + prg.state0 = _bigEndianBytesToUint64(abi.encodePacked(hash), 0); + prg.state1 = _bigEndianBytesToUint64(abi.encodePacked(hash), 8); + + _requireNonZero(prg); + } + + /** + * @dev Advances the PRG state and generates the next UInt64 value + * See https://arxiv.org/pdf/1404.0390.pdf for implementation details and reasoning for triplet selection. + * Note that state only advances when this function is called from a transaction. Calls from within a script + * will not advance state and will return the same value. + * + * @return The next UInt64 value + */ + function nextUInt64(PRG memory prg) internal pure returns (uint64) { + _requireNonZero(prg); + + uint64 a = prg.state0; + uint64 b = prg.state1; + + prg.state0 = b; + + // Allow the states to wrap around + unchecked { + a ^= a << 23; // a + a ^= a >> 17; // b + a ^= b ^ (b >> 26); // c + } + + prg.state1 = a; + + unchecked { + return a + b; // Addition with wrapping + } + } + + /** + * @dev Advances the PRG state and generates the next UInt256 value by concatenating 4 UInt64 values + * + * @return The next UInt256 value + */ + function nextUInt256(PRG memory prg) internal pure returns (uint256) { + uint256 result = uint256(nextUInt64(prg)); + result |= uint256(nextUInt64(prg)) << 64; + result |= uint256(nextUInt64(prg)) << 128; + result |= uint256(nextUInt64(prg)) << 192; + return result; + } + + /** + * @dev Helper function to convert an array of big endian bytes to Word64 + * + * @param input The bytes to convert + * @param start The index of the first byte to convert + * + * @return The Word64 value + */ + function _bigEndianBytesToUint64(bytes memory input, uint256 start) private pure returns (uint64) { + require(input.length >= start + 8, "Invalid byte length"); + uint64 value = 0; + for (uint256 i = 0; i < 8; i++) { + value = (value << 8) | uint64(uint8(input[start + i])); + } + return value; + } + + function _requireNonZero(PRG memory prg) private pure { + require(prg.state0 != 0 || prg.state1 != 0, "PRG initial state is 0 - must be initialized as non-zero"); + } +} diff --git a/solidity/src/test/TestCadenceRandomConsumer.sol b/solidity/src/test/TestCadenceRandomConsumer.sol new file mode 100644 index 0000000..328ec5b --- /dev/null +++ b/solidity/src/test/TestCadenceRandomConsumer.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {CadenceRandomConsumer} from "../CadenceRandomConsumer.sol"; + +/** + * @dev This contract extends CadenceRandomConsumer to expose internal functions for testing. + */ +contract TestCadenceRandomConsumer is CadenceRandomConsumer { + // Expose internal _getRevertibleRandomInRange function for testing + function getRevertibleRandomInRange(uint64 min, uint64 max) public view returns (uint64) { + return _getRevertibleRandomInRange(min, max); + } + + // Expose internal _requestRandomness function for testing + function requestRandomness() public returns (uint256) { + return _requestRandomness(); + } + + // Expose internal _fulfillRandomRequest function for testing + function fulfillRandomRequest(uint256 requestId) public returns (uint64) { + return _fulfillRandomRequest(requestId); + } + + // Expose internal _fulfillRandomInRange function for testing + function fulfillRandomInRange(uint256 requestId, uint64 min, uint64 max) public returns (uint64) { + return _fulfillRandomInRange(requestId, min, max); + } +} diff --git a/solidity/test/CadenceRandomConsumer.t.sol b/solidity/test/CadenceRandomConsumer.t.sol new file mode 100644 index 0000000..faf8fa3 --- /dev/null +++ b/solidity/test/CadenceRandomConsumer.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {CadenceRandomConsumer} from "../src/CadenceRandomConsumer.sol"; +import {TestCadenceRandomConsumer} from "../src/test/TestCadenceRandomConsumer.sol"; +import {Xorshift128plus} from "../src/Xorshift128plus.sol"; + +contract CadenceRandomConsumerTest is Test { + TestCadenceRandomConsumer private consumer; + + address private cadenceArch = 0x0000000000000000000000010000000000000001; + address payable user = payable(address(100)); + uint64 mockFlowBlockHeight = 12345; + + event RandomnessRequested(uint256 requestId, uint64 flowBlockHeight, uint256 blockNumber); + + // Initialize the test environment + function setUp() public { + consumer = new TestCadenceRandomConsumer(); + + // Fund test accounts + vm.deal(address(consumer), 10 ether); + vm.deal(user, 10 ether); + + // Mock the Cadence Arch precompile for flowBlockHeight() call + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); + + // Mock the Cadence Arch precompile for getRandomSource(uint64) call + vm.mockCall(cadenceArch, abi.encodeWithSignature("getRandomSource(uint64)", 100), abi.encode(uint64(0))); + + // Mock the Cadence Arch precompile for revertibleRandom() call + vm.mockCall(cadenceArch, abi.encodeWithSignature("revertibleRandom()"), abi.encode(uint64(0))); + } + + /** + * Test _getRevertibleRandomInRange. + * Verifies that the random number is within the given range. + */ + function testGetRevertibleRandomInRange() public { + uint64 min = 10; + uint64 max = 100; + + vm.mockCall(cadenceArch, abi.encodeWithSignature("revertibleRandom()"), abi.encode(uint64(999))); + vm.prank(user); + + uint64 randomValue = consumer.getRevertibleRandomInRange(min, max); + + // Assert that the random value is within the expected range + assertTrue(randomValue >= min && randomValue <= max, "Random value is out of range"); + } + + /** + * Test _requestRandomness. + * Ensures a randomness request is created and emits the appropriate event. + */ + function testRequestRandomness() public { + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); + + uint256 requestId = consumer.requestRandomness(); + + // Assert that the request ID is greater than 0 + assertGt(requestId, 0, "Request ID is invalid"); + } + + /** + * Test _fulfillRandomRequest. + * Verifies that fulfilling a random request returns a valid random number. + */ + function testFulfillRandomRequest() public { + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); + // First, request randomness + uint256 requestId = consumer.requestRandomness(); + + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1)); + // Mock the Cadence Arch precompile for getRandomSource(uint64) call + vm.mockCall( + cadenceArch, + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), + abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000011)) + ); + // Fulfill the request + uint64 randomResult = consumer.fulfillRandomRequest(requestId); + + // Assert that the result is non-zero + assertGt(randomResult, 0, "Random result should be greater than 0"); + } + + /** + * Test _fulfillRandomInRange. + * Verifies that fulfilling a random request returns a number within the specified range. + */ + function testFulfillRandomInRange() public { + uint64 min = 5; + uint64 max = 50; + + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); + // First, request randomness + uint256 requestId = consumer.requestRandomness(); + + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1)); + // Mock the Cadence Arch precompile for getRandomSource(uint64) call + vm.mockCall( + cadenceArch, + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), + abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000011)) + ); + // Fulfill the request and get a random number within the range [min, max] + uint64 randomResult = consumer.fulfillRandomInRange(requestId, min, max); + + // Assert that the result is within the specified range + assertTrue(randomResult >= min && randomResult <= max, "Random result is out of range"); + } +} diff --git a/solidity/test/CoinToss.t.sol b/solidity/test/CoinToss.t.sol index 55ae667..580dbd6 100644 --- a/solidity/test/CoinToss.t.sol +++ b/solidity/test/CoinToss.t.sol @@ -7,6 +7,7 @@ import "../src/CoinToss.sol"; contract CoinTossTest is Test { CoinToss private coinToss; + address private cadenceArch = 0x0000000000000000000000010000000000000001; address payable user = payable(address(100)); uint64 mockFlowBlockHeight = 12345; @@ -19,20 +20,16 @@ contract CoinTossTest is Test { vm.deal(user, 10 ether); // Mock the Cadence Arch precompile for flowBlockHeight() call - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); // Mock the Cadence Arch precompile for getRandomSource(uint64) call - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("getRandomSource(uint64)", 100), abi.encode(uint64(0)) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("getRandomSource(uint64)", 100), abi.encode(uint64(0))); } function testFlipCoin() public { // Move forward one Flow block when called vm.mockCall( - coinToss.cadenceArch(), + cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) // Simulate a new Flow block ); @@ -53,24 +50,18 @@ contract CoinTossTest is Test { } function testRevealCoinFailSameBlock() public { - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); vm.prank(user); coinToss.flipCoin{value: 1 ether}(); - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); vm.prank(user); vm.expectRevert("Cannot fulfill request until subsequent Flow network block height"); // Expect a revert since the block hasn't advanced coinToss.revealCoin(); } function testRevealCoinWins() public { - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); // First, flip the coin uint256 sentValue = 1 ether; @@ -80,15 +71,13 @@ contract CoinTossTest is Test { // Get the user's balance before revealing the coin uint256 initialBalance = user.balance; - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1)); // The result gets hashed in CadenceRandomConsumer with the request ID. Unfortunately we can't mockCall internal // functions, so we just use a mocked value that should result in a win (even number). vm.mockCall( - coinToss.cadenceArch(), + cadenceArch, abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), - abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000001)) + abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000011)) ); vm.prank(user); coinToss.revealCoin(); @@ -99,24 +88,27 @@ contract CoinTossTest is Test { uint8 multiplier = coinToss.multiplier(); uint256 expectedPrize = sentValue * multiplier; assertEq(finalBalance, initialBalance + expectedPrize, "User should have received a prize"); + + bool hasOpenRequest = coinToss.hasOpenRequest(user); + assertEq(false, hasOpenRequest, "User should not have an open request after revealing the coin"); + + bool canFullfillRequest = coinToss.canFulfillRequest(uint256(1)); + assertEq(false, canFullfillRequest, "Request should not be fulfillable after revealing the coin"); } function testRevealCoinLoses() public { - vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) - ); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight)); vm.prank(user); coinToss.flipCoin{value: 1 ether}(); + vm.mockCall(cadenceArch, abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1)); vm.mockCall( - coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1) - ); - vm.mockCall( - coinToss.cadenceArch(), + cadenceArch, abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), - abi.encode(bytes32(0x0)) + abi.encode(bytes32(0xff00000000000000000000000000000000000000000000000000000000000001)) ); + // abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000099)) // Ensure that the user doesn't get paid uint256 initialBalance = user.balance; diff --git a/tests/coin_toss_evm_tests.cdc b/tests/coin_toss_evm_tests.cdc index e3557ae..aa65eae 100644 --- a/tests/coin_toss_evm_tests.cdc +++ b/tests/coin_toss_evm_tests.cdc @@ -40,7 +40,7 @@ fun setup() { // deploy the CoinToss contract from the compiled bytecode let deployResult = executeTransaction( "../transactions/evm/deploy.cdc", - [getCoinTossBytecode(), UInt64(1_000_000), 0.0], + [getCoinTossBytecode(), UInt64(15_000_000), 0.0], user ) Test.expect(deployResult, Test.beSucceeded()) @@ -55,7 +55,6 @@ fun setup() { let depositResult = executeTransaction( "../transactions/evm/deposit_flow.cdc", [coinTossAddress, 100.0], - // [userCOAAddress, 100.0], serviceAccount ) Test.expect(depositResult, Test.beSucceeded()) diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index b5a37cc..3e4009d 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -2,7 +2,7 @@ import Test import "EVM" -access(all) let coinTossBytecode = "608060405234801561001057600080fd5b50610bed806100206000396000f3fe6080604052600436106100745760003560e01c80638c0647461161004e5780638c064746146100cb578063cd1a44db14610113578063d0d250bd1461014e578063fcf36b6a1461018357600080fd5b80631b3ed72214610080578063799ae223146100ac5780638c03fe15146100b657600080fd5b3661007b57005b600080fd5b34801561008c57600080fd5b50610095600281565b60405160ff90911681526020015b60405180910390f35b6100b46101b0565b005b3480156100c257600080fd5b506100b46102f5565b3480156100d757600080fd5b506101036100e6366004610a41565b6001600160a01b0316600090815260026020526040902054151590565b60405190151581526020016100a3565b34801561011f57600080fd5b5061014061012e366004610a71565b60036020526000908152604090205481565b6040519081526020016100a3565b34801561015a57600080fd5b5061016b6801000000000000000181565b6040516001600160a01b0390911681526020016100a3565b34801561018f57600080fd5b5061014061019e366004610a41565b60026020526000908152604090205481565b3461020e5760405162461bcd60e51b815260206004820152602360248201527f4d7573742073656e6420464c4f5720746f20706c61636520666c69702061206360448201526237b4b760e91b60648201526084015b60405180910390fd5b336000908152600260205260409020541561028a5760405162461bcd60e51b815260206004820152603660248201527f4d75737420636c6f73652070726576696f757320636f696e20666c6970206265604482015275666f726520706c6163696e672061206e6577206f6e6560501b6064820152608401610205565b600061029461047a565b3360008181526002602090815260408083208590558483526003825291829020349081905582518581529182015292935090917faaf5204450758753a474bce0ff33a69922372c674f7f6cde7adc701030fb3c4c910160405180910390a250565b3360009081526002602052604090205461036b5760405162461bcd60e51b815260206004820152603160248201527f43616c6c657220686173206e6f7420666c6970706564206120636f696e202d206044820152701b9bdd1a1a5b99c81d1bc81c995d99585b607a1b6064820152608401610205565b3360009081526002602052604081208054908290559061038a82610582565b90506000610399600283610a8a565b600084815260036020526040812080549082905591925060ff8316810361042e576103c5600283610ad5565b604051909150600090339083156108fc0290849084818181858888f1935050505090508061042c5760405162461bcd60e51b81526020600482015260146024820152734661696c656420746f2073656e64207072697a6560601b6044820152606401610205565b505b6040805186815260ff8516602082015290810182905233907ffa8362afdddf0c36107d3f3ad3c875ecd4c311118133c0e4b2b8e6ac2a0e31059060600160405180910390a25050505050565b600180546000918261048b83610af2565b919050555060006001549050600060405180604001604052806104ac6107c9565b67ffffffffffffffff90811682524360209283015260008054600181018255908052835160029091027f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563810180549290931667ffffffffffffffff199092168217909255838301517f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56490920182905560408051878152938401919091528201529091507f7886956edccde10901843c3e51fc26016455e9248e210a29c2a57ead63ad226e9060600160405180910390a150919050565b600080610590600184610b0b565b60005490915063ffffffff82161061061b5760405162461bcd60e51b815260206004820152604260248201527f496e76616c69642072657175657374204944202d2076616c756520657863656560448201527f647320746865206e756d626572206f66206578697374696e6720726571756573606482015261747360f01b608482015260a401610205565b6000808263ffffffff168154811061063557610635610b2f565b6000918252602080832060408051808201909152600290930201805467ffffffffffffffff1683526001015490820152915061066f6107c9565b90508067ffffffffffffffff16826000015167ffffffffffffffff16106107085760405162461bcd60e51b815260206004820152604160248201527f43616e6e6f742066756c66696c6c207265717565737420756e74696c2073756260448201527f73657175656e7420466c6f77206e6574776f726b20626c6f636b2068656967686064820152601d60fa1b608482015260a401610205565b600061071783600001516108fb565b6040516001600160c01b031960c083901b1660208201526001600160e01b031960e089901b166028820152909150602c0160408051808303601f19018152828252805160209182012086518783015163ffffffff8c16865267ffffffffffffffff9182169386019390935292840191909152908116606083015291507f9f3bf4fa8022192c0b0425a106a1a48898dba8d4e18790dd57868b1cc56f58e69060800160405180910390a195945050505050565b60408051600481526024810182526020810180516001600160e01b03166329f43eb360e11b179052905160009182918291680100000000000000019161080f9190610b45565b600060405180830381855afa9150503d806000811461084a576040519150601f19603f3d011682016040523d82523d6000602084013e61084f565b606091505b5091509150816108dd5760405162461bcd60e51b815260206004820152604d60248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e6720466c6f7760648201526c08189b1bd8dac81a195a59da1d609a1b608482015260a401610205565b6000818060200190518101906108f39190610b74565b949350505050565b60405167ffffffffffffffff8216602482015260009081908190680100000000000000019060440160408051601f198184030181529181526020820180516001600160e01b0316633c53afdf60e11b179052516109589190610b45565b600060405180830381855afa9150503d8060008114610993576040519150601f19603f3d011682016040523d82523d6000602084013e610998565b606091505b509150915081610a225760405162461bcd60e51b815260206004820152604960248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e672072616e646064820152686f6d20736f7572636560b81b608482015260a401610205565b600081806020019051810190610a389190610b9e565b95945050505050565b600060208284031215610a5357600080fd5b81356001600160a01b0381168114610a6a57600080fd5b9392505050565b600060208284031215610a8357600080fd5b5035919050565b600067ffffffffffffffff80841680610ab357634e487b7160e01b600052601260045260246000fd5b92169190910692915050565b634e487b7160e01b600052601160045260246000fd5b8082028115828204841417610aec57610aec610abf565b92915050565b600060018201610b0457610b04610abf565b5060010190565b63ffffffff828116828216039080821115610b2857610b28610abf565b5092915050565b634e487b7160e01b600052603260045260246000fd5b6000825160005b81811015610b665760208186018101518583015201610b4c565b506000920191825250919050565b600060208284031215610b8657600080fd5b815167ffffffffffffffff81168114610a6a57600080fd5b600060208284031215610bb057600080fd5b505191905056fea26469706673582212205d1c2726f88985efee2736987a6840951541f037e98ba6a57e8abb6e7accd38164736f6c63430008130033" +access(all) let coinTossBytecode = "608060405234801561001057600080fd5b50611247806100206000396000f3fe6080604052600436106100745760003560e01c80638c0647461161004e5780638c064746146100cb578063cd1a44db14610113578063d59fd63c1461014e578063fcf36b6a1461016e57600080fd5b80631b3ed72214610080578063799ae223146100ac5780638c03fe15146100b657600080fd5b3661007b57005b600080fd5b34801561008c57600080fd5b50610095600281565b60405160ff90911681526020015b60405180910390f35b6100b461019b565b005b3480156100c257600080fd5b506100b46102dc565b3480156100d757600080fd5b506101036100e6366004611012565b6001600160a01b0316600090815260026020526040902054151590565b60405190151581526020016100a3565b34801561011f57600080fd5b5061014061012e366004611042565b60036020526000908152604090205481565b6040519081526020016100a3565b34801561015a57600080fd5b50610103610169366004611042565b61044f565b34801561017a57600080fd5b50610140610189366004611012565b60026020526000908152604090205481565b346101f95760405162461bcd60e51b815260206004820152602360248201527f4d7573742073656e6420464c4f5720746f20706c61636520666c69702061206360448201526237b4b760e91b60648201526084015b60405180910390fd5b33600090815260026020526040902054156102755760405162461bcd60e51b815260206004820152603660248201527f4d75737420636c6f73652070726576696f757320636f696e20666c6970206265604482015275666f726520706c6163696e672061206e6577206f6e6560501b60648201526084016101f0565b600061027f6104cd565b33600081815260026020908152604080832085905584835260038252918290203490819055915191825292935083927faaf5204450758753a474bce0ff33a69922372c674f7f6cde7adc701030fb3c4c910160405180910390a350565b336000908152600260205260409020546103525760405162461bcd60e51b815260206004820152603160248201527f43616c6c657220686173206e6f7420666c6970706564206120636f696e202d206044820152701b9bdd1a1a5b99c81d1bc81c995d99585b607a1b60648201526084016101f0565b336000908152600260205260408120805490829055906103748282600161060e565b600083815260036020526040812080549082905591925060ff83168103610409576103a0600283611071565b604051909150600090339083156108fc0290849084818181858888f193505050509050806104075760405162461bcd60e51b81526020600482015260146024820152734661696c656420746f2073656e64207072697a6560601b60448201526064016101f0565b505b6040805160ff8516815260208101839052859133917ffa8362afdddf0c36107d3f3ad3c875ecd4c311118133c0e4b2b8e6ac2a0e3105910160405180910390a350505050565b60008061045d600184611088565b60005490915081106104725750600092915050565b60008082815481106104865761048661109b565b9060005260206000209060030201905060006104a06106cc565b600283015490915060ff161580156104c4575081546001600160401b038083169116105b95945050505050565b60018054600091826104de836110b1565b919050555060006001549050600060405180606001604052806104ff6106cc565b6001600160401b0390811682524360208084019190915260006040938401819052805460018101825590805284517f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5636003909202918201805467ffffffffffffffff191691909416908117909355848201517f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5648201819055858501517f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e565909201805460ff19169215159290921790915583519283529082015291925083917f7886956edccde10901843c3e51fc26016455e9248e210a29c2a57ead63ad226e910160405180910390a250919050565b60008061061a856107fe565b60405160200161062c91815260200190565b60408051601f198184030181528282526020830188905292506000910160408051601f19818403018152828201909152600080835260208301529150610673818484610943565b6000610680828888610a76565b6040516001600160401b038216815290915088907fe3e452ff947e18cea0f62f23d405e522ac808f69346c5f59ddc4bd722ac10d039060200160405180910390a2979650505050505050565b60408051600481526024810182526020810180516001600160e01b03166329f43eb360e11b179052905160009182918291680100000000000000019161071291906110fa565b600060405180830381855afa9150503d806000811461074d576040519150601f19603f3d011682016040523d82523d6000602084013e610752565b606091505b5091509150816107e05760405162461bcd60e51b815260206004820152604d60248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e6720466c6f7760648201526c08189b1bd8dac81a195a59da1d609a1b608482015260a4016101f0565b6000818060200190518101906107f69190611106565b949350505050565b60008061080c600184611088565b60005490915081106108915760405162461bcd60e51b815260206004820152604260248201527f496e76616c69642072657175657374204944202d2076616c756520657863656560448201527f647320746865206e756d626572206f66206578697374696e6720726571756573606482015261747360f01b608482015260a4016101f0565b60008082815481106108a5576108a561109b565b906000526020600020906003020190506108be81610ba5565b60028101805460ff1916600117905580546000906108e4906001600160401b0316610c91565b82546001840154604080516001600160401b0390931683526020830191909152810182905290915085907f01f207584638dfbb3f1d85bba3809672fcb07a3d2e291308293ba78b7f1984909060600160405180910390a2949350505050565b6010825110156109c95760405162461bcd60e51b815260206004820152604560248201527f4174206c65617374203136206279746573206f6620656e74726f70792073686f60448201527f756c642062652075736564207768656e20696e697469616c697a696e67207468606482015264652050524760d81b608482015260a4016101f0565b600082826040516020016109de92919061112f565b6040516020818303038152906040529050600081805190602001209050610a2781604051602001610a1191815260200190565b6040516020818303038152906040526000610dcd565b6001600160401b031685526040805160208101839052610a5891016040516020818303038152906040526008610dcd565b6001600160401b03166020860152610a6f85610e87565b5050505050565b6000826001600160401b0316826001600160401b031611610ad95760405162461bcd60e51b815260206004820152601c60248201527f4d6178206d7573742062652067726561746572207468616e206d696e0000000060448201526064016101f0565b6000610ae58484611144565b90506000610af282610f1d565b90506000610b0d60016001600160401b03841681901b611088565b90506000610b1d8361010061116b565b6001600160401b031690506000806000610b368b610f53565b90505b8481169150866001600160401b0316826001600160401b03161115610b8c576001600160401b0386161c82610b6d816110b1565b935050838303610b8757610b808b610f53565b9050600092505b610b39565b610b96828b61119f565b9b9a5050505050505050505050565b600281015460ff1615610bfa5760405162461bcd60e51b815260206004820152601960248201527f5265717565737420616c72656164792066756c66696c6c65640000000000000060448201526064016101f0565b610c026106cc565b81546001600160401b03918216911610610c8e5760405162461bcd60e51b815260206004820152604160248201527f43616e6e6f742066756c66696c6c207265717565737420756e74696c2073756260448201527f73657175656e7420466c6f77206e6574776f726b20626c6f636b2068656967686064820152601d60fa1b608482015260a4016101f0565b50565b6040516001600160401b038216602482015260009081908190680100000000000000019060440160408051601f198184030181529181526020820180516001600160e01b0316633c53afdf60e11b17905251610ced91906110fa565b600060405180830381855afa9150503d8060008114610d28576040519150601f19603f3d011682016040523d82523d6000602084013e610d2d565b606091505b509150915081610db75760405162461bcd60e51b815260206004820152604960248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e672072616e646064820152686f6d20736f7572636560b81b608482015260a4016101f0565b6000818060200190518101906104c491906111bf565b6000610dda8260086111d8565b83511015610e205760405162461bcd60e51b8152602060048201526013602482015272092dcecc2d8d2c840c4f2e8ca40d8cadccee8d606b1b60448201526064016101f0565b6000805b6008811015610e7d5784610e3882866111d8565b81518110610e4857610e4861109b565b602001015160f81c60f81b60f81c60ff166008836001600160401b0316901b1791508080610e75906110b1565b915050610e24565b5090505b92915050565b80516001600160401b0316151580610eab575060208101516001600160401b031615155b610c8e5760405162461bcd60e51b815260206004820152603860248201527f50524720696e697469616c2073746174652069732030202d206d75737420626560448201527f20696e697469616c697a6564206173206e6f6e2d7a65726f000000000000000060648201526084016101f0565b6000805b6001600160401b03831615610e81576001836001600160401b0316901c92508080610f4b906111eb565b915050610f21565b600080610f5f83610fb5565b6001600160401b031690506040610f7584610fb5565b6001600160401b0316901b176080610f8c84610fb5565b6001600160401b0316901b1760c0610fa384610fb5565b6001600160401b0316901b1792915050565b6000610fc082610e87565b5080516020820180516001600160401b03808216909452601783901b6a7fffffffffffffff80000016909218601181901c657fffffffffff1618601a83901c643fffffffff1683181892831690520190565b60006020828403121561102457600080fd5b81356001600160a01b038116811461103b57600080fd5b9392505050565b60006020828403121561105457600080fd5b5035919050565b634e487b7160e01b600052601160045260246000fd5b8082028115828204841417610e8157610e8161105b565b81810381811115610e8157610e8161105b565b634e487b7160e01b600052603260045260246000fd5b6000600182016110c3576110c361105b565b5060010190565b6000815160005b818110156110eb57602081850181015186830152016110d1565b50600093019283525090919050565b600061103b82846110ca565b60006020828403121561111857600080fd5b81516001600160401b038116811461103b57600080fd5b60006107f661113e83866110ca565b846110ca565b6001600160401b038281168282160390808211156111645761116461105b565b5092915050565b60006001600160401b038084168061119357634e487b7160e01b600052601260045260246000fd5b92169190910492915050565b6001600160401b038181168382160190808211156111645761116461105b565b6000602082840312156111d157600080fd5b5051919050565b80820180821115610e8157610e8161105b565b60006001600160401b038083168181036112075761120761105b565b600101939250505056fea26469706673582212208cd9a222a39b9105db8805b53551c8a7ab5e6afcc19f207f47d185d99c7c98be64736f6c63430008130033" access(all) fun getCoinTossBytecode(): String {