From 5f14a5240ed521d1c64aa46eab7de756b30f5af1 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:28:17 -0600 Subject: [PATCH 01/20] add initial solidity contracts --- .gitignore | 2 + foundry.toml | 8 ++ solidity/src/CadenceRandomConsumer.sol | 128 +++++++++++++++++++++++++ solidity/src/CoinToss.sol | 30 ++++++ 4 files changed, 168 insertions(+) create mode 100644 foundry.toml create mode 100644 solidity/src/CadenceRandomConsumer.sol create mode 100644 solidity/src/CoinToss.sol diff --git a/.gitignore b/.gitignore index 5ff0b16..2833333 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.pkey imports/ +cache/ +out/ \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..2b5e79b --- /dev/null +++ b/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "./solidity/src" +out = "./solidity/out" +libs = ["./solidity/lib"] +script = "./solidity/script" +test = "./solidity/test" + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol new file mode 100644 index 0000000..4573405 --- /dev/null +++ b/solidity/src/CadenceRandomConsumer.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @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 { + // Cadence Arch pre-compile address + address constant public cadenceArch = 0x0000000000000000000000010000000000000001; + + // 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; + } + + // Events + event RandomnessRequested( + uint256 requestId, + uint64 flowHeight, + uint256 evmHeight + ); + event RandomnessFulfilled( + uint256 requestId, + uint64 flowHeight, + uint256 evmHeight, + uint256 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; + + /** + * @dev This method serves as the commit step in this contract's commit-reveal scheme + * + * Here a caller places commits at Flow block n to reveal a random number at >= block n+1 + * This is because the random source for a Flow block is not available until after the finalization of that block. + * Implementing contracts may wish to affiliate the request ID with the caller's address or some other identifier + * so the relevant request can be fulfilled. + * Emits a {RandomnessRequested} event. + * + * @return requestId + */ + function _requestRandomness() public returns (uint256) { + // Identify the request by the current request counter + uint256 requestId = _requestCounter; + // 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(_getFlowBlockHeight(), block.number); + + // Store the request and increment the request counter + _requests.push(request); + _requestCounter++; + + emit RandomnessRequested(requestId, request.flowHeight, request.evmHeight); + + // Finally return the request ID + return requestId; + } + + /** + * @dev This method serves as the reveal step in this contract's commit-reveal scheme + * + * Here a caller reveals a random number at least one block after the commit block + * This is because the random source for a Flow block is not available until after the finalization of that block. + * Note that the random source for a given Flow block is singular. In order to ensure that requests made at the same + * block height are unique, implementing contracts should use some pseudo-random method to generate a unique value + * from the seed along with a salt. + * Emits a {RandomnessFulfilled} event. + * + * @param requestId + * @return randomResult + */ + function _fulfillRandomness(uint32 requestId) internal returns (uint64) { + // Get the request details + require(requestId < _requests.length, "Invalid request ID - value exceeds the number of existing requests"); + Request memory request = _requests[requestId]; + + // Ensure that the request is fulfilled at a Flow block height greater than the one at which the request was made + uint64 flowHeight = _getFlowBlockHeight(); + require(request.flowHeight < flowHeight, "Cannot fulfill request until subsequent Flow network block height"); + + // Get the random source for the Flow block at which the request was made + uint64 randomResult = _getRandomSource(request.flowHeight); + randomResult = uint64(uint256(keccak256(abi.encodePacked(randomResult, requestId)))); + + emit RandomnessFulfilled(requestId, request.flowHeight, request.evmHeight, randomResult); + + // Return the random result + return randomResult; + } + + /** + * @dev This method returns the current Flow block height + * + * @return flowBlockHeight + */ + function _getFlowBlockHeight() internal view returns (uint64) { + (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("flowBlockHeight()")); + require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching Flow block height"); + + uint64 output = abi.decode(data, (uint64)); + return output; + } + + /** + * @dev This method uses the Cadence Arch pre-compiles to returns a random source for a given Flow block height. + * The provided height must be at least one block in the past. + * + * @param flowHeight + * @return randomSource + */ + function _getRandomSource(uint64 flowHeight) private view returns (uint64) { + (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"); + + uint64 output = abi.decode(data, (uint64)); + return output; + } +} diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol new file mode 100644 index 0000000..629caed --- /dev/null +++ b/solidity/src/CoinToss.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "./CadenceRandomConsumer.sol"; + +contract CoinToss is CadenceRandomConsumer { + uint8 constant betMultiplier = 2; + + mapping (address => uint256) public coinTosses; + mapping (uint256 => uint256) public bets; + + function flipCoin() public payable { + uint256 requestId = _requestRandomness(); + coinTosses[msg.sender] = requestId; + // get the value of the bet from the transaction + bets[requestId] = msg.value; + } + + function revealCoin() public { + // reveal random result and calculate winnings + uint256 requestId = coinTosses[msg.sender]; + uint64 randomResult = _fulfillRandomness(uint32(requestId)); + uint256 bet = bets[requestId]; + uint256 winnings = bet * betMultiplier; + if (randomResult % 2 == 0) { + // win + payable(msg.sender).transfer(winnings); + } + } +} From cc6470439b1aa6794c4413267c19efd49e805b11 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:31:44 -0600 Subject: [PATCH 02/20] forge install: forge-std v1.9.2 --- .gitmodules | 3 +++ solidity/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 solidity/lib/forge-std diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3077b9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "solidity/lib/forge-std"] + path = solidity/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/solidity/lib/forge-std b/solidity/lib/forge-std new file mode 160000 index 0000000..1714bee --- /dev/null +++ b/solidity/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d From 554c778a621c2ba599c12fb0d8cfc7b0edb96d84 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:54:37 -0600 Subject: [PATCH 03/20] fix CoinToss.flipCoin() on 0 value transmission --- solidity/src/CoinToss.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 629caed..a6b3ca4 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -5,14 +5,14 @@ import "./CadenceRandomConsumer.sol"; contract CoinToss is CadenceRandomConsumer { uint8 constant betMultiplier = 2; - - mapping (address => uint256) public coinTosses; - mapping (uint256 => uint256) public bets; + + mapping(address => uint256) public coinTosses; + mapping(uint256 => uint256) public bets; function flipCoin() public payable { + require(msg.value > 0, "Must send ETH to place a bet"); uint256 requestId = _requestRandomness(); coinTosses[msg.sender] = requestId; - // get the value of the bet from the transaction bets[requestId] = msg.value; } From 72e386b67aed39412677694b3862016d795185ec Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:33:00 -0600 Subject: [PATCH 04/20] fix CoinToss.sol & add test coverage --- solidity/src/CadenceRandomConsumer.sol | 60 ++++++------ solidity/src/CoinToss.sol | 70 ++++++++++++-- solidity/test/CoinToss.t.sol | 121 +++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 42 deletions(-) create mode 100644 solidity/test/CoinToss.t.sol diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 4573405..75658cb 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -8,7 +8,7 @@ pragma solidity 0.8.19; */ abstract contract CadenceRandomConsumer { // Cadence Arch pre-compile address - address constant public cadenceArch = 0x0000000000000000000000010000000000000001; + address public constant cadenceArch = 0x0000000000000000000000010000000000000001; // A struct to store the request details struct Request { @@ -19,17 +19,8 @@ abstract contract CadenceRandomConsumer { } // Events - event RandomnessRequested( - uint256 requestId, - uint64 flowHeight, - uint256 evmHeight - ); - event RandomnessFulfilled( - uint256 requestId, - uint64 flowHeight, - uint256 evmHeight, - uint256 randomResult - ); + event RandomnessRequested(uint256 requestId, uint64 flowHeight, uint256 evmHeight); + event RandomnessFulfilled(uint256 requestId, uint64 flowHeight, uint256 evmHeight, uint256 randomResult); // A list of requests where each request is identified by its index in the array Request[] private _requests; @@ -38,26 +29,27 @@ abstract contract CadenceRandomConsumer { /** * @dev This method serves as the commit step in this contract's commit-reveal scheme - * + * * Here a caller places commits at Flow block n to reveal a random number at >= block n+1 * This is because the random source for a Flow block is not available until after the finalization of that block. * Implementing contracts may wish to affiliate the request ID with the caller's address or some other identifier * so the relevant request can be fulfilled. - * Emits a {RandomnessRequested} event. - * - * @return requestId + * Emits a {RandomnessRequested} event. + * + * @return requestId The ID of the request. */ function _requestRandomness() public returns (uint256) { - // Identify the request by the current request counter + // Identify the request by the current request counter, incrementing first so implementations can use 0 for + // invalid requests - e.g. myRequests[msg.sender] == 0 means the caller has no pending requests + _requestCounter++; uint256 requestId = _requestCounter; // 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(_getFlowBlockHeight(), block.number); - // Store the request and increment the request counter + // Store the request in the list of requests _requests.push(request); - _requestCounter++; emit RandomnessRequested(requestId, request.flowHeight, request.evmHeight); @@ -67,28 +59,30 @@ abstract contract CadenceRandomConsumer { /** * @dev This method serves as the reveal step in this contract's commit-reveal scheme - * + * * Here a caller reveals a random number at least one block after the commit block * This is because the random source for a Flow block is not available until after the finalization of that block. * Note that the random source for a given Flow block is singular. In order to ensure that requests made at the same * block height are unique, implementing contracts should use some pseudo-random method to generate a unique value * from the seed along with a salt. - * Emits a {RandomnessFulfilled} event. - * - * @param requestId - * @return randomResult + * Emits a {RandomnessFulfilled} event. + * + * @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) { - // Get the request details - require(requestId < _requests.length, "Invalid request ID - value exceeds the number of existing requests"); - Request memory request = _requests[requestId]; + // 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; + 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 = _getFlowBlockHeight(); require(request.flowHeight < flowHeight, "Cannot fulfill request until subsequent Flow network block height"); // Get the random source for the Flow block at which the request was made - uint64 randomResult = _getRandomSource(request.flowHeight); + uint64 randomResult = _getRandomSource(request.flowHeight); // returns bytes32 + // Pack the randomResult into a uint64, hashing with the requestId to vary results across shared block heights randomResult = uint64(uint256(keccak256(abi.encodePacked(randomResult, requestId)))); emit RandomnessFulfilled(requestId, request.flowHeight, request.evmHeight, randomResult); @@ -99,8 +93,8 @@ abstract contract CadenceRandomConsumer { /** * @dev This method returns the current Flow block height - * - * @return flowBlockHeight + * + * @return flowBlockHeight The current Flow block height. */ function _getFlowBlockHeight() internal view returns (uint64) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("flowBlockHeight()")); @@ -113,9 +107,9 @@ abstract contract CadenceRandomConsumer { /** * @dev This method uses the Cadence Arch pre-compiles to returns a random source for a given Flow block height. * The provided height must be at least one block in the past. - * - * @param flowHeight - * @return randomSource + * + * @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) private view returns (uint64) { (bool ok, bytes memory data) = diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index a6b3ca4..977cd86 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -3,28 +3,80 @@ pragma solidity 0.8.19; import "./CadenceRandomConsumer.sol"; +/** + * @dev This contract is a simple coin toss game where users can place win prizes by flipping a coin. + */ contract CoinToss is CadenceRandomConsumer { - uint8 constant betMultiplier = 2; + // A constant to store the multiplier for the prize + uint8 public constant multiplier = 2; + // A mapping to store the request ID for each user mapping(address => uint256) public coinTosses; - mapping(uint256 => uint256) public bets; + // 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); + + /** + * @dev Checks if a user has an open request. + */ + function hasOpenRequest(address user) public view returns (bool) { + return coinTosses[user] != 0; + } + + /** + * @dev Checks if a value is non-zero. + */ + function isNonZero(uint256 value) public pure returns (bool) { + return value > 0; + } + + /** + * @dev Allows a user to flip a coin by sending FLOW to the contract. This is the commit step in the commit-reveal scheme. + */ function flipCoin() public payable { - require(msg.value > 0, "Must send ETH to place a bet"); + require(isNonZero(msg.value), "Must send FLOW to place flip a coin"); + require(!isNonZero(coinTosses[msg.sender]), "Must close previous coin flip before placing a new one"); + + // request randomness uint256 requestId = _requestRandomness(); + // insert the request ID into the coinTosses mapping coinTosses[msg.sender] = requestId; - bets[requestId] = msg.value; + // insert the value sent by the sender with the flipCoin function call into the openRequests mapping + openRequests[requestId] = msg.value; + + emit CoinFlipped(msg.sender, requestId, msg.value); } + /** + * @dev Allows a user to reveal the result of the coin flip and claim their prize. + */ function revealCoin() public { + require(hasOpenRequest(msg.sender), "Caller has not flipped a coin - nothing to reveal"); + // reveal random result and calculate winnings uint256 requestId = coinTosses[msg.sender]; + // delete the open request from the coinTosses mapping + delete coinTosses[msg.sender]; + + // fulfill the random request uint64 randomResult = _fulfillRandomness(uint32(requestId)); - uint256 bet = bets[requestId]; - uint256 winnings = bet * betMultiplier; - if (randomResult % 2 == 0) { - // win - payable(msg.sender).transfer(winnings); + uint8 coinFace = uint8(randomResult % 2); + + // get the value sent in the flipCoin function & remove the request from the openRequests mapping + uint256 amount = openRequests[requestId]; + delete openRequests[requestId]; + + // calculate the prize + uint256 prize = 0; + // send the prize if the random result is even + if (coinFace == 0) { + prize = amount * multiplier; + bool sent = payable(msg.sender).send(prize); // Use send to avoid revert + require(sent, "Failed to send prize"); } + + emit CoinRevealed(msg.sender, requestId, coinFace, prize); } } diff --git a/solidity/test/CoinToss.t.sol b/solidity/test/CoinToss.t.sol new file mode 100644 index 0000000..3937478 --- /dev/null +++ b/solidity/test/CoinToss.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "../src/CoinToss.sol"; + +contract CoinTossTest is Test { + CoinToss private coinToss; + + address payable user = payable(address(100)); + uint64 mockFlowBlockHeight = 12345; + + function setUp() public { + // Deploy the CoinToss contract before each test + coinToss = new CoinToss(); + + // Fund test accounts + vm.deal(address(coinToss), 10 ether); + vm.deal(user, 10 ether); + + // Mock the Cadence Arch precompile for flowBlockHeight() call + vm.mockCall( + coinToss.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)) + ); + } + + function testFlipCoin() public { + // Move forward one Flow block when called + vm.mockCall( + coinToss.cadenceArch(), + abi.encodeWithSignature("flowBlockHeight()"), + abi.encode(mockFlowBlockHeight + 1) // Simulate a new Flow block + ); + + // Simulate that the next call is made by `user` + vm.prank(user); + coinToss.flipCoin{value: 1 ether}(); + + // Check that the value amount was stored correctly + uint256 requestId = coinToss.coinTosses(user); + assertEq(coinToss.openRequests(requestId), 1 ether, "Value amount sent should be 1 full FLOW"); + } + + function testFlipCoinFailNoValue() public { + vm.prank(user); + vm.expectRevert("Must send FLOW to place flip a coin"); // Expect a revert since no Ether is sent + coinToss.flipCoin(); + } + + function testRevealCoinWins() public { + vm.mockCall( + coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1) + ); + + // First, flip the coin + uint256 sentValue = 1 ether; + vm.prank(user); + coinToss.flipCoin{value: sentValue}(); + + // Get the user's balance before revealing the coin + uint256 initialBalance = user.balance; + + vm.mockCall( + coinToss.cadenceArch(), + abi.encodeWithSignature("flowBlockHeight()"), + abi.encode(mockFlowBlockHeight + 2) // Simulate a new Flow block + ); + // 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(), + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight + 1), + abi.encode(uint64(1)) // Mocked result + ); + vm.prank(user); + coinToss.revealCoin(); + + uint256 finalBalance = user.balance; + + // Ensure the user has received their prize + uint8 multiplier = coinToss.multiplier(); + uint256 expectedPrize = sentValue * multiplier; + assertEq(finalBalance, initialBalance + expectedPrize, "User should have received a prize"); + } + + function testRevealCoinLoses() public { + vm.mockCall( + coinToss.cadenceArch(), + abi.encodeWithSignature("flowBlockHeight()"), + abi.encode(mockFlowBlockHeight + 1) + ); + + vm.prank(user); + coinToss.flipCoin{value: 1 ether}(); + + vm.mockCall( + coinToss.cadenceArch(), + abi.encodeWithSignature("flowBlockHeight()"), + abi.encode(mockFlowBlockHeight + 2) + ); + vm.mockCall( + coinToss.cadenceArch(), + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight + 1), + abi.encode(uint64(0)) + ); + + // Ensure that the user doesn't get paid + uint256 initialBalance = user.balance; + vm.prank(user); + coinToss.revealCoin(); + uint256 finalBalance = user.balance; + + // Ensure the user balance hasn't changed (no winnings) + assertEq(finalBalance, initialBalance, "User should not receive winnings"); + } +} From 01c59031003c7719fb04d555378407b69b3e8771 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:59:06 -0600 Subject: [PATCH 05/20] update dependencies --- flow.json | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/flow.json b/flow.json index 9fca6ef..fbcad8f 100644 --- a/flow.json +++ b/flow.json @@ -19,8 +19,18 @@ }, "dependencies": { "Burner": { - "source": "previewnet://b6763b4399a888c8.Burner", - "hash": "9f84f00c8d2afd70916514e56f9459ef7f426b70a7440dd1b64f2723ff779ce7", + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "previewnet": "b6763b4399a888c8", + "testnet": "8c5303eaa26202d6" + } + }, + "EVM": { + "source": "mainnet://e467b9dd11fa00df.EVM", + "hash": "1b1f3fe59d964b8afde33d3150ab89f257373aa253ae412c8b02fb176dd03698", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -29,8 +39,8 @@ } }, "FlowToken": { - "source": "previewnet://4445e7ad11568276.FlowToken", - "hash": "633b6a3cd0a3d68cb665478b297f9f3e37cc9c6c8829a4fda5b22ed7cb908a44", + "source": "mainnet://1654653399040a61.FlowToken", + "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -39,8 +49,8 @@ } }, "FungibleToken": { - "source": "previewnet://a0225e7000ac82a9.FungibleToken", - "hash": "3cad0cb7d88b8af34ab82a1580c6d70906649f7e974c12958a55c822d4f1e808", + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "1410889b47fef8b02f6867eef3d67a75288a56a651b67a7e815ce273ad301cff", "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -49,8 +59,8 @@ } }, "FungibleTokenMetadataViews": { - "source": "previewnet://a0225e7000ac82a9.FungibleTokenMetadataViews", - "hash": "2206d6ac3d18e2a26ed8e610cb28d1e054423b20a8bdc2ac6a10bffc7e579a9c", + "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", + "hash": "294ada6a3df68757fcac4d794f62307c2ea4fe49c93f67e3771d3c6d8377dd47", "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -59,8 +69,8 @@ } }, "MetadataViews": { - "source": "previewnet://b6763b4399a888c8.MetadataViews", - "hash": "a49bab604f143ced27662b28d16887707ed7a4e60e626eb1e742ea93bf0bffb0", + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "be26ea7959d7cbc06ac69fe00926b812c4da67984ea2d1bde1029141ae091378", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -69,8 +79,8 @@ } }, "NonFungibleToken": { - "source": "previewnet://b6763b4399a888c8.NonFungibleToken", - "hash": "ebf4d24a324803365c238d95c621ee472f36acc1ed4158ff5d255e8fb865e3cc", + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "49a58b950afdaf0728fdb7d4eb47cf4f2ec3077d655f274b7fdeb504c742f528", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -79,8 +89,8 @@ } }, "RandomBeaconHistory": { - "source": "previewnet://b6763b4399a888c8.RandomBeaconHistory", - "hash": "9536f036aed0afbd5c84b4ce4a1def7466bff989bcb07c7be9918028b46f05db", + "source": "mainnet://e467b9dd11fa00df.RandomBeaconHistory", + "hash": "fe7f11c46dd54446bd60d88015ffe5057d7245c4b77374e11db2adf0a8156167", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -89,8 +99,8 @@ } }, "ViewResolver": { - "source": "previewnet://b6763b4399a888c8.ViewResolver", - "hash": "eaa3214b0e178c5692cfc243a14c739150d5d57ef3f9204a252d70667a67db76", + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", From a166fe6aefa368850626241c043070dbef0a8dd5 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:27:14 -0600 Subject: [PATCH 06/20] add Cadence transactions to interact with CoinToss.sol --- transactions/evm-coin-toss/1_create_coa.cdc | 51 ++++++++++++++++++++ transactions/evm-coin-toss/2_flip_coin.cdc | 40 +++++++++++++++ transactions/evm-coin-toss/3_reveal_coin.cdc | 38 +++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 transactions/evm-coin-toss/1_create_coa.cdc create mode 100644 transactions/evm-coin-toss/2_flip_coin.cdc create mode 100644 transactions/evm-coin-toss/3_reveal_coin.cdc diff --git a/transactions/evm-coin-toss/1_create_coa.cdc b/transactions/evm-coin-toss/1_create_coa.cdc new file mode 100644 index 0000000..d60ca38 --- /dev/null +++ b/transactions/evm-coin-toss/1_create_coa.cdc @@ -0,0 +1,51 @@ +import "EVM" +import "FungibleToken" +import "FlowToken" + +/// Configures a COA in the signer's Flow account, funding with the specified amount. If the COA already exists, the +/// transaction reverts. +/// +transaction(amount: UFix64) { + let coa: &EVM.CadenceOwnedAccount + let sentVault: @FlowToken.Vault + + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + let storagePath = /storage/evm + let publicPath = /public/evm + + // Revert if the CadenceOwnedAccount already exists + if signer.storage.type(at: storagePath) != nil { + panic("Storage collision - Resource already stored at path=".concat(storagePath.toString())) + } + + // Configure the CadenceOwnedAccount + signer.storage.save<@EVM.CadenceOwnedAccount>(<-EVM.createCadenceOwnedAccount(), to: storagePath) + let addressableCap = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath) + signer.capabilities.unpublish(publicPath) + signer.capabilities.publish(addressableCap, at: publicPath) + + // Reference the CadeceOwnedAccount + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Missing or mis-typed CadenceOwnedAccount at /storage/evm") + + // Withdraw the amount from the signer's FlowToken vault + let vaultRef = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("Could not borrow reference to the owner's Vault!") + self.sentVault <- vaultRef.withdraw(amount: amount) as! @FlowToken.Vault + } + + pre { + self.sentVault.balance == amount: + "Expected amount =".concat(amount.toString()).concat(" but sentVault.balance=").concat(self.sentVault.balance.toString()) + } + + execute { + // Deposit the amount into the CadenceOwnedAccount if the balance is greater than zero + if self.sentVault.balance > 0.0 { + self.coa.deposit(from: <-self.sentVault) + } else { + destroy self.sentVault + } + } +} \ No newline at end of file diff --git a/transactions/evm-coin-toss/2_flip_coin.cdc b/transactions/evm-coin-toss/2_flip_coin.cdc new file mode 100644 index 0000000..4236746 --- /dev/null +++ b/transactions/evm-coin-toss/2_flip_coin.cdc @@ -0,0 +1,40 @@ +import "EVM" + +/// Calls the CoinToss.flipCoin() method in the specified contract address, completing the commit step in the CoinToss +/// contract's two-step commit-reveal coin toss. +/// +transaction(coinTossContractAddress: String, amount: UFix64) { + /// The CadenceOwnedAccount reference used to call the flipCoin() method + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue) &Account) { + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Missing or mis-typed CadenceOwnedAccount at /storage/evm") + } + + execute { + // Deserialize the contract address string into an Address object + let contractAddress = EVM.addressFromString(coinTossContractAddress) + + // Encode the flipCoin() method signature and parameters as calldata + let calldata = EVM.encodeABIWithSignature("flipCoin()", []) + + // The value sent with the call to flipCoin() is the amount of FLOW tokens to wager + let value = EVM.Balance(attoflow: 0) + value.setFLOW(flow: amount) + + // Call the flipCoin() method in the CoinToss contract + let callResult = self.coa.call( + to: contractAddress, + data: calldata, + gasLimit: 15_000_000, + value: value + ) + + assert( + callResult.status == EVM.Status.successful, + message: "Call to ".concat(coinTossContractAddress).concat(" failed with error code=") + .concat(callResult.errorCode.toString()) + ) + } +} diff --git a/transactions/evm-coin-toss/3_reveal_coin.cdc b/transactions/evm-coin-toss/3_reveal_coin.cdc new file mode 100644 index 0000000..74dc18b --- /dev/null +++ b/transactions/evm-coin-toss/3_reveal_coin.cdc @@ -0,0 +1,38 @@ +import "EVM" + +/// Calls the CoinToss.revealCoin() method in the specified contract address, completing the reveal step in the CoinToss +/// contract's two-step commit-reveal coin toss. If the random result is heads (0), the caller wins a prize double the +/// value they submitted in the flipCoin() transaction. +/// +transaction(coinTossContractAddress: String) { + /// The CadenceOwnedAccount reference used to call the flipCoin() method + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue) &Account) { + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Missing or mis-typed CadenceOwnedAccount at /storage/evm") + } + + execute { + // Deserialize the contract address string into an Address object + let contractAddress = EVM.addressFromString(coinTossContractAddress) + // Encode the revealCoin() method signature and parameters as calldata + let calldata = EVM.encodeABIWithSignature("revealCoin()", []) + // The value sent with the call to revealCoin() is zero + let value = EVM.Balance(attoflow: 0) + + // Call the revealCoin() method in the CoinToss contract + let callResult = self.coa.call( + to: contractAddress, + data: calldata, + gasLimit: 15_000_000, + value: value + ) + + assert( + callResult.status == EVM.Status.successful, + message: "Call to ".concat(coinTossContractAddress).concat(" failed with error code=") + .concat(callResult.errorCode.toString()) + ) + } +} From f535eea6350bad3156e587beec7f0b4cf0bf9aed Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:33:15 -0600 Subject: [PATCH 07/20] add foundry tests to ci --- .github/workflows/foundry_tests.yml | 34 +++++++++++++++++++++ .github/workflows/{ci.yml => prg_tests.yml} | 0 2 files changed, 34 insertions(+) create mode 100644 .github/workflows/foundry_tests.yml rename .github/workflows/{ci.yml => prg_tests.yml} (100%) diff --git a/.github/workflows/foundry_tests.yml b/.github/workflows/foundry_tests.yml new file mode 100644 index 0000000..6e51435 --- /dev/null +++ b/.github/workflows/foundry_tests.yml @@ -0,0 +1,34 @@ +name: test + +on: pull_request + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.github/workflows/ci.yml b/.github/workflows/prg_tests.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/prg_tests.yml From 8f5797be5484bd64d800b875cee45a3d4692391b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:37:18 -0600 Subject: [PATCH 08/20] rename transactions --- transactions/evm-coin-toss/{1_create_coa.cdc => 0_create_coa.cdc} | 0 transactions/evm-coin-toss/{2_flip_coin.cdc => 1_flip_coin.cdc} | 0 .../evm-coin-toss/{3_reveal_coin.cdc => 2_reveal_coin.cdc} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename transactions/evm-coin-toss/{1_create_coa.cdc => 0_create_coa.cdc} (100%) rename transactions/evm-coin-toss/{2_flip_coin.cdc => 1_flip_coin.cdc} (100%) rename transactions/evm-coin-toss/{3_reveal_coin.cdc => 2_reveal_coin.cdc} (100%) diff --git a/transactions/evm-coin-toss/1_create_coa.cdc b/transactions/evm-coin-toss/0_create_coa.cdc similarity index 100% rename from transactions/evm-coin-toss/1_create_coa.cdc rename to transactions/evm-coin-toss/0_create_coa.cdc diff --git a/transactions/evm-coin-toss/2_flip_coin.cdc b/transactions/evm-coin-toss/1_flip_coin.cdc similarity index 100% rename from transactions/evm-coin-toss/2_flip_coin.cdc rename to transactions/evm-coin-toss/1_flip_coin.cdc diff --git a/transactions/evm-coin-toss/3_reveal_coin.cdc b/transactions/evm-coin-toss/2_reveal_coin.cdc similarity index 100% rename from transactions/evm-coin-toss/3_reveal_coin.cdc rename to transactions/evm-coin-toss/2_reveal_coin.cdc From 13a72745453e3bb15f06272a2a68c2f27c1cbb0a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:17:11 -0600 Subject: [PATCH 09/20] add CadenceArchWrapper contract & implement in CoinToss.sol --- solidity/src/CadenceArchWrapper.sol | 56 ++++++++++++++++++++++++++ solidity/src/CadenceRandomConsumer.sol | 40 +++--------------- solidity/src/CoinToss.sol | 3 +- solidity/test/CoinToss.t.sol | 37 ++++++++++------- 4 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 solidity/src/CadenceArchWrapper.sol diff --git a/solidity/src/CadenceArchWrapper.sol b/solidity/src/CadenceArchWrapper.sol new file mode 100644 index 0000000..f65d524 --- /dev/null +++ b/solidity/src/CadenceArchWrapper.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @dev This contract is a base contract to facilitate easier consumption of the Cadence Arch pre-compiles. Implementing + * contracts can use this contract to fetch the current Flow block height and fetch random numbers from the Cadence + * runtime. + */ +abstract contract CadenceArchWrapper { + // Cadence Arch pre-compile address + address public constant cadenceArch = 0x0000000000000000000000010000000000000001; + + /** + * @dev This method returns the current Flow block height. + * + * @return flowBlockHeight The current Flow block height. + */ + function _flowBlockHeight() internal view returns (uint64) { + (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("flowBlockHeight()")); + require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching Flow block height"); + + uint64 output = abi.decode(data, (uint64)); + return output; + } + + /** + * @dev This method uses the Cadence Arch pre-compiles to return a random number from the Cadence runtime. Consumers + * should know this is a revertible random source and should only be used as a source of randomness when called by + * trusted callers - i.e. with trust that the caller won't revert on result. + * + * @return randomSource The random source. + */ + function _revertibleRandom() internal view returns (uint64) { + (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("revertibleRandom()")); + require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching revertible random number"); + + uint64 output = abi.decode(data, (uint64)); + return output; + } + + /** + * @dev This method uses the Cadence Arch pre-compiles to returns a random source for a given Flow block height. + * The provided height must be at least one block in the past. + * + * @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) { + (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"); + + uint64 output = abi.decode(data, (uint64)); + return output; + } +} diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 75658cb..6d16e22 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; +import {CadenceArchWrapper} from "./CadenceArchWrapper.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 { - // Cadence Arch pre-compile address - address public constant cadenceArch = 0x0000000000000000000000010000000000000001; - +abstract contract CadenceRandomConsumer is CadenceArchWrapper { // A struct to store the request details struct Request { // The Flow block height at which the request was made @@ -46,7 +45,7 @@ abstract contract CadenceRandomConsumer { // 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(_getFlowBlockHeight(), block.number); + Request memory request = Request(_flowBlockHeight(), block.number); // Store the request in the list of requests _requests.push(request); @@ -77,7 +76,7 @@ abstract contract CadenceRandomConsumer { 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 = _getFlowBlockHeight(); + uint64 flowHeight = _flowBlockHeight(); require(request.flowHeight < flowHeight, "Cannot fulfill request until subsequent Flow network block height"); // Get the random source for the Flow block at which the request was made @@ -90,33 +89,4 @@ abstract contract CadenceRandomConsumer { // Return the random result return randomResult; } - - /** - * @dev This method returns the current Flow block height - * - * @return flowBlockHeight The current Flow block height. - */ - function _getFlowBlockHeight() internal view returns (uint64) { - (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("flowBlockHeight()")); - require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching Flow block height"); - - uint64 output = abi.decode(data, (uint64)); - return output; - } - - /** - * @dev This method uses the Cadence Arch pre-compiles to returns a random source for a given Flow block height. - * The provided height must be at least one block in the past. - * - * @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) private view returns (uint64) { - (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"); - - uint64 output = abi.decode(data, (uint64)); - return output; - } } diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 977cd86..8f46184 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.19; import "./CadenceRandomConsumer.sol"; /** - * @dev This contract is a simple coin toss game where users can place win prizes by flipping a coin. + * @dev This contract is a simple coin toss game where users can place win prizes by flipping a coin as a demonstration + * of safe usage of Flow EVM's native secure randomness. */ contract CoinToss is CadenceRandomConsumer { // A constant to store the multiplier for the prize diff --git a/solidity/test/CoinToss.t.sol b/solidity/test/CoinToss.t.sol index 3937478..b86c227 100644 --- a/solidity/test/CoinToss.t.sol +++ b/solidity/test/CoinToss.t.sol @@ -34,7 +34,7 @@ contract CoinTossTest is Test { vm.mockCall( coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), - abi.encode(mockFlowBlockHeight + 1) // Simulate a new Flow block + abi.encode(mockFlowBlockHeight) // Simulate a new Flow block ); // Simulate that the next call is made by `user` @@ -52,9 +52,24 @@ contract CoinTossTest is Test { coinToss.flipCoin(); } + function testRevealCoinFailSameBlock() public { + vm.mockCall( + coinToss.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.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 + 1) + coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight) ); // First, flip the coin @@ -66,16 +81,14 @@ contract CoinTossTest is Test { uint256 initialBalance = user.balance; vm.mockCall( - coinToss.cadenceArch(), - abi.encodeWithSignature("flowBlockHeight()"), - abi.encode(mockFlowBlockHeight + 2) // Simulate a new Flow block + coinToss.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(), - abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight + 1), - abi.encode(uint64(1)) // Mocked result + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), + abi.encode(uint64(1)) ); vm.prank(user); coinToss.revealCoin(); @@ -90,22 +103,18 @@ contract CoinTossTest is Test { function testRevealCoinLoses() public { vm.mockCall( - coinToss.cadenceArch(), - abi.encodeWithSignature("flowBlockHeight()"), - abi.encode(mockFlowBlockHeight + 1) + coinToss.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 + 2) + coinToss.cadenceArch(), abi.encodeWithSignature("flowBlockHeight()"), abi.encode(mockFlowBlockHeight + 1) ); vm.mockCall( coinToss.cadenceArch(), - abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight + 1), + abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), abi.encode(uint64(0)) ); From 8e05274a527959964c9544db728c50cde717a443 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:18:51 -0600 Subject: [PATCH 10/20] reformat CoinToss.sol --- solidity/src/CoinToss.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 8f46184..37a5e09 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -26,19 +26,12 @@ contract CoinToss is CadenceRandomConsumer { return coinTosses[user] != 0; } - /** - * @dev Checks if a value is non-zero. - */ - function isNonZero(uint256 value) public pure returns (bool) { - return value > 0; - } - /** * @dev Allows a user to flip a coin by sending FLOW to the contract. This is the commit step in the commit-reveal scheme. */ function flipCoin() public payable { - require(isNonZero(msg.value), "Must send FLOW to place flip a coin"); - require(!isNonZero(coinTosses[msg.sender]), "Must close previous coin flip before placing a new one"); + require(_isNonZero(msg.value), "Must send FLOW to place flip a coin"); + require(!_isNonZero(coinTosses[msg.sender]), "Must close previous coin flip before placing a new one"); // request randomness uint256 requestId = _requestRandomness(); @@ -80,4 +73,11 @@ contract CoinToss is CadenceRandomConsumer { emit CoinRevealed(msg.sender, requestId, coinFace, prize); } + + /** + * @dev Checks if a value is non-zero. + */ + function _isNonZero(uint256 value) internal pure returns (bool) { + return value > 0; + } } From 54590274f44da70338f6b0725547d8cf1243c522 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:23:00 -0600 Subject: [PATCH 11/20] update README --- README.md | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6fd4f33..45d4b49 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ # Random Coin Toss -While both are backed by Flow's Random Beacon, +> :information_source: This repository contains demonstrations for safe usage of Flow's protocol-native secure randomness in both Cadence and Solidity smart contracts. + +On Flow, there are two routes to get a random value. While both are backed by Flow's Random Beacon, it is important for developers to mindfully choose between `revertibleRandom` or seeding their own PRNG utilizing the `RandomBeaconHistory` smart contract: -- Under the hood, the FVM also just instantiates a PRNG for each transaction that `revertibleRandom` draws from. - Though, with `revertibleRandom` a developer is calling the PRNG that is controlled by the transaction, - which also has the power to abort and revert if it doesn't like `revertibleRandom`'s outputs. - `revertibleRandom` is only suitable for smart contract functions that exclusively run within the trusted transactions. -- In contrast, using the `RandomBeaconHistory` means to use a deterministically-seeded PRNG. - The `RandomBeaconHistory` is key for effectively implementing a commit-and-reveal scheme. - During the commit phase, the user commits to proceed with a future source of randomness, - which is revealed after the commit transaction concluded. - For each block, the `RandomBeaconHistory` automatically stores the subsequently generated source of randomness. - -> 🚨 A transaction can atomically revert all its action during its runtime and abort. +- Under the hood, the FVM instantiates a PRNG for each transaction from which `revertibleRandom` generates random numbers. + However, when using `revertibleRandom` a developer is relying on the PRNG that is controlled by the transaction, + which also has the power to abort and revert based on `revertibleRandom`'s outputs. Therefore, + `revertibleRandom` is only suitable for smart contract functions that exclusively run within credibly-neutral transactions which a developer can trust won't revert based on undesirable random outputs. +- In contrast, using the `RandomBeaconHistory` allows developers to use a deterministically-seeded PRNG. + The `RandomBeaconHistory` is key for effectively implementing a commit-and-reveal scheme which is itself a pattern that prevents users from gaming a transaction based on the result of randomness. + During the commit phase, the user commits to proceed with a **future** source of randomness + which is revealed after the commit transaction concludes. + For each block, the `RandomBeaconHistory` automatically stores the subsequently generated source of randomness in the final transaction of that block. This source of randomness is committed by Flow's protocol service account. + +> 🚨 A transaction can atomically revert the entirety of its action based on results exposed within the scope of that transaction. > Therefore, it is possible for a transaction calling into your smart contract to post-select favorable > results and revert the transaction for unfavorable results. > @@ -24,8 +26,7 @@ or seeding their own PRNG utilizing the `RandomBeaconHistory` smart contract: > > ✅ Utilizing a commit-and-reveal scheme is important for developers to protect their smart contracts from transaction post-selection attacks. - -Via a commit-and-reveal scheme, flow's native secure randomness can be safely used within Cadence smart contracts +Via a commit-and-reveal scheme, Flow's protocol-native secure randomness can be safely used within both Cadence and Solidity smart contracts when contracts are transacted on by untrusted parties. By providing examples of commit-reveal implementations we hope to foster a more secure ecosystem of decentralized applications and encourage developers to build with best practices. @@ -36,11 +37,11 @@ The contracts contained in this repo demonstrate how to use Flow's onchain rando in contracts that are transacted on by untrusted parties. Safe randomness here meaning non-revertible randomness, i.e. mitigating post-selection attacks via a commit-and-reveal scheme. -Random sources are committed to the [`RandomBeaconHistory` contract](./contracts/RandomBeaconHistory.cdc) by the service +Random sources are committed to the [`RandomBeaconHistory` contract](https://github.com/onflow/flow-core-contracts/blob/master/contracts/RandomBeaconHistory.cdc) by the service account at the end of every block. The RandomBeaconHistory contract provides a convenient archive, where for each past block height (starting Nov 2023) the respective 'source of randomness' can be retrieved. -When used naively, `revertibleRandom` as well as the [`RandomBeaconHistory` contract](./contracts/RandomBeaconHistory.cdc) +When used naively, `revertibleRandom` as well as the [`RandomBeaconHistory` contract](https://github.com/onflow/flow-core-contracts/blob/master/contracts/RandomBeaconHistory.cdc) are subject to post-selection attacks from transactions. In simple terms, using the random source in your contract without the protection of a commit-reveal mechanism would enable non-trusted callers to condition their interactions with your contract on the @@ -49,11 +50,11 @@ game. To achieve non-revertible randomness, the contract should be structured to resolve in two phases: -1. Commit - Caller commits to the resolution of their bet with some yet unknown source of randomness (i.e. in the - future) -2. Reveal - Caller can then resolve the result of their bet once the source of randomness is available in the `RandomBeaconHistory` with a separate transaction. - From a technical perspective, this could also be called "resolving transaction", because the transaction simply executes the smart contract with the locked-in - inputs, whose output all parties committed to accept in the previous phase. +1. **Commit** - Caller commits to the resolution of their bet with some yet unknown source of randomness (i.e. in the + future) +2. **Reveal** - Caller can then resolve the result of their bet once the source of randomness is available in the `RandomBeaconHistory` with a separate transaction. + From a technical perspective, this could also be called a "resolving transaction", because the transaction simply executes the smart contract with the locked-in + inputs, whose output all parties committed to accept in the previous phase. Though a caller could still condition the revealing transaction on the coin flip result, all the inputs influencing the bet's outcome have already been fixed (the source of randomness being the last one that is only generated after the commit transaction concluded). @@ -63,14 +64,17 @@ All that the resolving transaction (reveal phase) is doing is affirming the win The ticket owner could revert their resolving transaction. Though that does not change whether the ticket won or lost. Furthermore, the player has already incurred the cost of their bet and gains nothing by reverting the reveal step. +Given that Flow has both Cadence and EVM runtimes, commit-reveal patterns covering Cadence and Solidity are found in this repo as well as transactions demonstrating how Flow accounts can interact with EVM implementations from the Cadence runtime via COAs. + ## Deployments |Contract|Testnet|Mainnet| |---|---|---| -|[CoinToss](./contracts/CoinToss.cdc)|[0xd1299e755e8be5e7](https://contractbrowser.com/A.d1299e755e8be5e7.CoinToss)|N/A| -|[Xorshift128plus](./contracts/Xorshift128plus.cdc)|[0xed24dbe901028c5c](https://contractbrowser.com/A.ed24dbe901028c5c.Xorshift128plus)|[0x45caec600164c9e6](https://contractbrowser.com/A.45caec600164c9e6.Xorshift128plus)| +|[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)|TBD|N/A| -## Further Reading +## Further Reading - We recommend the **Flow developer documentation** [**_Advanced Concepts → Flow VRF_**](https://developers.flow.com/build/advanced-concepts/randomness) @@ -80,3 +84,4 @@ incurred the cost of their bet and gains nothing by reverting the reveal step. - [Pull request introducing the `RandomBeaconHistory` system smart contract.](https://github.com/onflow/flow-core-contracts/pull/375) - [FLIP 123: _On-Chain randomness history for commit-reveal schemes_](https://github.com/onflow/flips/pull/123) describes the need for a commit-and-reveal scheme and discusses ideas for additional convenience functionality to further optimize the developer experience in the future. +- For more on Cadence Arch pre-compiles and accessing random values from EVM on Flow, see documentation on the [Cadence Arch precompiled contracts](https://developers.flow.com/evm/how-it-works#precompiled-contracts). From c909e775ed77fa8223581ada50cb1683a5f4fe81 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:17:40 -0600 Subject: [PATCH 12/20] Update README.md Co-authored-by: Tarak Ben Youssef <50252200+tarakby@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45d4b49..45b2a91 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ or seeding their own PRNG utilizing the `RandomBeaconHistory` smart contract: However, when using `revertibleRandom` a developer is relying on the PRNG that is controlled by the transaction, which also has the power to abort and revert based on `revertibleRandom`'s outputs. Therefore, `revertibleRandom` is only suitable for smart contract functions that exclusively run within credibly-neutral transactions which a developer can trust won't revert based on undesirable random outputs. -- In contrast, using the `RandomBeaconHistory` allows developers to use a deterministically-seeded PRNG. +- In contrast, using the `RandomBeaconHistory` allows developers to use a committed random source (or seed) that can't be reverted. The `RandomBeaconHistory` is key for effectively implementing a commit-and-reveal scheme which is itself a pattern that prevents users from gaming a transaction based on the result of randomness. During the commit phase, the user commits to proceed with a **future** source of randomness which is revealed after the commit transaction concludes. From 58618718823a0ee680050799e70ead633250c538 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:56:53 -0600 Subject: [PATCH 13/20] update statements relating to PRG implementation details --- README.md | 3 +-- solidity/src/CadenceRandomConsumer.sol | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 45b2a91..4a2d0d9 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ On Flow, there are two routes to get a random value. While both are backed by Fl it is important for developers to mindfully choose between `revertibleRandom` or seeding their own PRNG utilizing the `RandomBeaconHistory` smart contract: -- Under the hood, the FVM instantiates a PRNG for each transaction from which `revertibleRandom` generates random numbers. - However, when using `revertibleRandom` a developer is relying on the PRNG that is controlled by the transaction, +- When using `revertibleRandom` a developer is relying on randomness generation controlled by the transaction, which also has the power to abort and revert based on `revertibleRandom`'s outputs. Therefore, `revertibleRandom` is only suitable for smart contract functions that exclusively run within credibly-neutral transactions which a developer can trust won't revert based on undesirable random outputs. - In contrast, using the `RandomBeaconHistory` allows developers to use a committed random source (or seed) that can't be reverted. diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 6d16e22..26a3139 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -81,7 +81,10 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { // 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 + // 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)))); emit RandomnessFulfilled(requestId, request.flowHeight, request.evmHeight, randomResult); From cf1ec53a0ceca3d852dc991afd1099e16a6de0cc Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:08:52 -0600 Subject: [PATCH 14/20] fix CadenceRandomConsumer._requestRandomness as internal --- solidity/src/CadenceRandomConsumer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 26a3139..0387d11 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -37,7 +37,7 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { * * @return requestId The ID of the request. */ - function _requestRandomness() public returns (uint256) { + function _requestRandomness() internal returns (uint256) { // Identify the request by the current request counter, incrementing first so implementations can use 0 for // invalid requests - e.g. myRequests[msg.sender] == 0 means the caller has no pending requests _requestCounter++; From e7794147e95bc7d844b1bdcec35be2237993f7cf Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:09:09 -0600 Subject: [PATCH 15/20] add missing receive() function to CoinToss --- solidity/src/CoinToss.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 37a5e09..3efd1fc 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -80,4 +80,9 @@ contract CoinToss is CadenceRandomConsumer { function _isNonZero(uint256 value) internal pure returns (bool) { return value > 0; } + + /** + * @dev Fallback function to receive FLOW. + */ + receive() external payable {} } From 802c871e4b3adc41ce7aecb20350efd4d556d0f9 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:10:18 -0600 Subject: [PATCH 16/20] update .gitignore --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 2833333..8d47772 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +# Local keys *.pem *.pkey +# Cadence dependencies imports/ + +# Cadence testing +coverage.lcov +coverage.json + +# Foundry directories cache/ out/ \ No newline at end of file From 86c24eccff62a11177cfde0198f5acc6a8870cb0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:19:06 -0600 Subject: [PATCH 17/20] fix getRandomSource precompile call --- solidity/src/CadenceArchWrapper.sol | 7 ++++++- solidity/test/CoinToss.t.sol | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/solidity/src/CadenceArchWrapper.sol b/solidity/src/CadenceArchWrapper.sol index f65d524..9756797 100644 --- a/solidity/src/CadenceArchWrapper.sol +++ b/solidity/src/CadenceArchWrapper.sol @@ -50,7 +50,12 @@ abstract contract CadenceArchWrapper { cadenceArch.staticcall(abi.encodeWithSignature("getRandomSource(uint64)", flowHeight)); require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching random source"); - uint64 output = abi.decode(data, (uint64)); + // Decode the result as bytes32 and then cast it to uint64 + 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; } } diff --git a/solidity/test/CoinToss.t.sol b/solidity/test/CoinToss.t.sol index b86c227..55ae667 100644 --- a/solidity/test/CoinToss.t.sol +++ b/solidity/test/CoinToss.t.sol @@ -88,7 +88,7 @@ contract CoinTossTest is Test { vm.mockCall( coinToss.cadenceArch(), abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), - abi.encode(uint64(1)) + abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000001)) ); vm.prank(user); coinToss.revealCoin(); @@ -115,7 +115,7 @@ contract CoinTossTest is Test { vm.mockCall( coinToss.cadenceArch(), abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), - abi.encode(uint64(0)) + abi.encode(bytes32(0x0)) ); // Ensure that the user doesn't get paid From 839d6979402839deb985ce9fc75121207060d991 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:19:41 -0600 Subject: [PATCH 18/20] update CoinToss.sol deployment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a2d0d9..7aec946 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)|TBD|N/A| +|[CoinToss.sol](./contracts/CoinToss.sol)|[0x5FC8d32690cc91D4c39d9d3abcBD16989F875707](https://evm-testnet.flowscan.io/address/0x5FC8d32690cc91D4c39d9d3abcBD16989F875707?tab=contract_code)|N/A| ## Further Reading From 9be7f7703fcf2a2415ede94a04866c372610653f Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:20:52 -0600 Subject: [PATCH 19/20] add Cadence tests covering CoinToss.sol interactions via COA --- tests/coin_toss_evm_tests.cdc | 102 +++++++++++++++++++ tests/scripts/get_evm_balance.cdc | 6 ++ tests/test_helpers.cdc | 25 +++++ transactions/evm-coin-toss/2_reveal_coin.cdc | 2 +- transactions/evm/deploy.cdc | 56 ++++++++++ transactions/evm/deposit_flow.cdc | 34 +++++++ 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/coin_toss_evm_tests.cdc create mode 100644 tests/scripts/get_evm_balance.cdc create mode 100644 tests/test_helpers.cdc create mode 100644 transactions/evm/deploy.cdc create mode 100644 transactions/evm/deposit_flow.cdc diff --git a/tests/coin_toss_evm_tests.cdc b/tests/coin_toss_evm_tests.cdc new file mode 100644 index 0000000..e3557ae --- /dev/null +++ b/tests/coin_toss_evm_tests.cdc @@ -0,0 +1,102 @@ +import Test +import BlockchainHelpers + +import "test_helpers.cdc" + +import "EVM" + +access(all) let serviceAccount = Test.serviceAccount() +access(all) let user = Test.createAccount() +access(all) var userCOAAddress = "" +access(all) var coinTossAddress = "" + +access(all) +fun setup() { + // Create COA in service account + var coaResult = executeTransaction( + "../transactions/evm-coin-toss/0_create_coa.cdc", + [100.0], + serviceAccount + ) + Test.expect(coaResult, Test.beSucceeded()) + + // fund user account with FLOW + mintFlow(to: user, amount: 1000.0) + + // create CadenceOwnedAccount + coaResult = executeTransaction( + "../transactions/evm-coin-toss/0_create_coa.cdc", + [100.0], + user + ) + Test.expect(coaResult, Test.beSucceeded()) + + // get the coa hex from the emitted event + var evts = Test.eventsOfType(Type()) + Test.assertEqual(2, evts.length) + var coaEvent = evts[1] as! EVM.CadenceOwnedAccountCreated + userCOAAddress = coaEvent.address + + // deploy the CoinToss contract from the compiled bytecode + let deployResult = executeTransaction( + "../transactions/evm/deploy.cdc", + [getCoinTossBytecode(), UInt64(1_000_000), 0.0], + user + ) + Test.expect(deployResult, Test.beSucceeded()) + + // get the deployed contract address from the emitted event + evts = Test.eventsOfType(Type()) + Test.assertEqual(5, evts.length) + let deployEvent = evts[4] as! EVM.TransactionExecuted + coinTossAddress = deployEvent.contractAddress + + // fund the CoinToss contract with FLOW + let depositResult = executeTransaction( + "../transactions/evm/deposit_flow.cdc", + [coinTossAddress, 100.0], + // [userCOAAddress, 100.0], + serviceAccount + ) + Test.expect(depositResult, Test.beSucceeded()) + + // confirm starting EVM balances + let coaBalance = getEVMBalance(userCOAAddress) + Test.assertEqual(100.0, coaBalance) + + let contractBalance = getEVMBalance(coinTossAddress) + Test.assertEqual(100.0, contractBalance) +} + +access(all) +fun testFlipCoinSucceeds() { + // flip the coin + let flipResult = executeTransaction( + "../transactions/evm-coin-toss/1_flip_coin.cdc", + [coinTossAddress, 1.0], + user + ) + Test.expect(flipResult, Test.beSucceeded()) +} + +access(all) +fun testFlipCoinWithOpenBetFails() { + // flip the coin + let flipResult = executeTransaction( + "../transactions/evm-coin-toss/1_flip_coin.cdc", + [coinTossAddress, 1.0], + user + ) + Test.expect(flipResult, Test.beFailed()) +} + +access(all) +fun testRevealCoinSucceeds() { + // reveal the coin flip + let revealResult = executeTransaction( + "../transactions/evm-coin-toss/2_reveal_coin.cdc", + [coinTossAddress], + user + ) + Test.expect(revealResult, Test.beSucceeded()) +} \ No newline at end of file diff --git a/tests/scripts/get_evm_balance.cdc b/tests/scripts/get_evm_balance.cdc new file mode 100644 index 0000000..227244f --- /dev/null +++ b/tests/scripts/get_evm_balance.cdc @@ -0,0 +1,6 @@ +import "EVM" + +access(all) +fun main(evmAddressHex: String): UFix64 { + return EVM.addressFromString(evmAddressHex).balance().inFLOW() +} \ No newline at end of file diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc new file mode 100644 index 0000000..b5a37cc --- /dev/null +++ b/tests/test_helpers.cdc @@ -0,0 +1,25 @@ +import Test + +import "EVM" + +access(all) let coinTossBytecode = "608060405234801561001057600080fd5b50610bed806100206000396000f3fe6080604052600436106100745760003560e01c80638c0647461161004e5780638c064746146100cb578063cd1a44db14610113578063d0d250bd1461014e578063fcf36b6a1461018357600080fd5b80631b3ed72214610080578063799ae223146100ac5780638c03fe15146100b657600080fd5b3661007b57005b600080fd5b34801561008c57600080fd5b50610095600281565b60405160ff90911681526020015b60405180910390f35b6100b46101b0565b005b3480156100c257600080fd5b506100b46102f5565b3480156100d757600080fd5b506101036100e6366004610a41565b6001600160a01b0316600090815260026020526040902054151590565b60405190151581526020016100a3565b34801561011f57600080fd5b5061014061012e366004610a71565b60036020526000908152604090205481565b6040519081526020016100a3565b34801561015a57600080fd5b5061016b6801000000000000000181565b6040516001600160a01b0390911681526020016100a3565b34801561018f57600080fd5b5061014061019e366004610a41565b60026020526000908152604090205481565b3461020e5760405162461bcd60e51b815260206004820152602360248201527f4d7573742073656e6420464c4f5720746f20706c61636520666c69702061206360448201526237b4b760e91b60648201526084015b60405180910390fd5b336000908152600260205260409020541561028a5760405162461bcd60e51b815260206004820152603660248201527f4d75737420636c6f73652070726576696f757320636f696e20666c6970206265604482015275666f726520706c6163696e672061206e6577206f6e6560501b6064820152608401610205565b600061029461047a565b3360008181526002602090815260408083208590558483526003825291829020349081905582518581529182015292935090917faaf5204450758753a474bce0ff33a69922372c674f7f6cde7adc701030fb3c4c910160405180910390a250565b3360009081526002602052604090205461036b5760405162461bcd60e51b815260206004820152603160248201527f43616c6c657220686173206e6f7420666c6970706564206120636f696e202d206044820152701b9bdd1a1a5b99c81d1bc81c995d99585b607a1b6064820152608401610205565b3360009081526002602052604081208054908290559061038a82610582565b90506000610399600283610a8a565b600084815260036020526040812080549082905591925060ff8316810361042e576103c5600283610ad5565b604051909150600090339083156108fc0290849084818181858888f1935050505090508061042c5760405162461bcd60e51b81526020600482015260146024820152734661696c656420746f2073656e64207072697a6560601b6044820152606401610205565b505b6040805186815260ff8516602082015290810182905233907ffa8362afdddf0c36107d3f3ad3c875ecd4c311118133c0e4b2b8e6ac2a0e31059060600160405180910390a25050505050565b600180546000918261048b83610af2565b919050555060006001549050600060405180604001604052806104ac6107c9565b67ffffffffffffffff90811682524360209283015260008054600181018255908052835160029091027f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563810180549290931667ffffffffffffffff199092168217909255838301517f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56490920182905560408051878152938401919091528201529091507f7886956edccde10901843c3e51fc26016455e9248e210a29c2a57ead63ad226e9060600160405180910390a150919050565b600080610590600184610b0b565b60005490915063ffffffff82161061061b5760405162461bcd60e51b815260206004820152604260248201527f496e76616c69642072657175657374204944202d2076616c756520657863656560448201527f647320746865206e756d626572206f66206578697374696e6720726571756573606482015261747360f01b608482015260a401610205565b6000808263ffffffff168154811061063557610635610b2f565b6000918252602080832060408051808201909152600290930201805467ffffffffffffffff1683526001015490820152915061066f6107c9565b90508067ffffffffffffffff16826000015167ffffffffffffffff16106107085760405162461bcd60e51b815260206004820152604160248201527f43616e6e6f742066756c66696c6c207265717565737420756e74696c2073756260448201527f73657175656e7420466c6f77206e6574776f726b20626c6f636b2068656967686064820152601d60fa1b608482015260a401610205565b600061071783600001516108fb565b6040516001600160c01b031960c083901b1660208201526001600160e01b031960e089901b166028820152909150602c0160408051808303601f19018152828252805160209182012086518783015163ffffffff8c16865267ffffffffffffffff9182169386019390935292840191909152908116606083015291507f9f3bf4fa8022192c0b0425a106a1a48898dba8d4e18790dd57868b1cc56f58e69060800160405180910390a195945050505050565b60408051600481526024810182526020810180516001600160e01b03166329f43eb360e11b179052905160009182918291680100000000000000019161080f9190610b45565b600060405180830381855afa9150503d806000811461084a576040519150601f19603f3d011682016040523d82523d6000602084013e61084f565b606091505b5091509150816108dd5760405162461bcd60e51b815260206004820152604d60248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e6720466c6f7760648201526c08189b1bd8dac81a195a59da1d609a1b608482015260a401610205565b6000818060200190518101906108f39190610b74565b949350505050565b60405167ffffffffffffffff8216602482015260009081908190680100000000000000019060440160408051601f198184030181529181526020820180516001600160e01b0316633c53afdf60e11b179052516109589190610b45565b600060405180830381855afa9150503d8060008114610993576040519150601f19603f3d011682016040523d82523d6000602084013e610998565b606091505b509150915081610a225760405162461bcd60e51b815260206004820152604960248201527f556e7375636365737366756c2063616c6c20746f20436164656e63652041726360448201527f68207072652d636f6d70696c65207768656e206665746368696e672072616e646064820152686f6d20736f7572636560b81b608482015260a401610205565b600081806020019051810190610a389190610b9e565b95945050505050565b600060208284031215610a5357600080fd5b81356001600160a01b0381168114610a6a57600080fd5b9392505050565b600060208284031215610a8357600080fd5b5035919050565b600067ffffffffffffffff80841680610ab357634e487b7160e01b600052601260045260246000fd5b92169190910692915050565b634e487b7160e01b600052601160045260246000fd5b8082028115828204841417610aec57610aec610abf565b92915050565b600060018201610b0457610b04610abf565b5060010190565b63ffffffff828116828216039080821115610b2857610b28610abf565b5092915050565b634e487b7160e01b600052603260045260246000fd5b6000825160005b81811015610b665760208186018101518583015201610b4c565b506000920191825250919050565b600060208284031215610b8657600080fd5b815167ffffffffffffffff81168114610a6a57600080fd5b600060208284031215610bb057600080fd5b505191905056fea26469706673582212205d1c2726f88985efee2736987a6840951541f037e98ba6a57e8abb6e7accd38164736f6c63430008130033" + +access(all) +fun getCoinTossBytecode(): String { + return coinTossBytecode +} + +access(all) +fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { + return Test.executeScript(Test.readFile(path), args) +} + +access(all) +fun getEVMBalance(_ evmAddressHex: String): UFix64 { + let res = _executeScript( + "./scripts/get_evm_balance.cdc", + [evmAddressHex] + ) + Test.expect(res, Test.beSucceeded()) + return res.returnValue! as! UFix64 +} \ No newline at end of file diff --git a/transactions/evm-coin-toss/2_reveal_coin.cdc b/transactions/evm-coin-toss/2_reveal_coin.cdc index 74dc18b..7e3f5ad 100644 --- a/transactions/evm-coin-toss/2_reveal_coin.cdc +++ b/transactions/evm-coin-toss/2_reveal_coin.cdc @@ -32,7 +32,7 @@ transaction(coinTossContractAddress: String) { assert( callResult.status == EVM.Status.successful, message: "Call to ".concat(coinTossContractAddress).concat(" failed with error code=") - .concat(callResult.errorCode.toString()) + .concat(callResult.errorCode.toString()).concat(" and message=").concat(callResult.errorMessage) ) } } diff --git a/transactions/evm/deploy.cdc b/transactions/evm/deploy.cdc new file mode 100644 index 0000000..221fef8 --- /dev/null +++ b/transactions/evm/deploy.cdc @@ -0,0 +1,56 @@ +import "FungibleToken" +import "FlowToken" + +import "EVM" + +/// Deploys a compiled solidity contract from bytecode to the EVM, with the signer's COA as the deployer +/// +transaction(bytecode: String, gasLimit: UInt64, value: UFix64) { + + let coa: auth(EVM.Deploy) &EVM.CadenceOwnedAccount + var sentVault: @FlowToken.Vault? + + prepare(signer: auth(BorrowValue) &Account) { + + let storagePath = StoragePath(identifier: "evm")! + self.coa = signer.storage.borrow(from: storagePath) + ?? panic("Could not borrow reference to the signer's bridged account") + + // Rebalance Flow across VMs if there is not enough Flow in the EVM account to cover the value + let evmFlowBalance: UFix64 = self.coa.balance().inFLOW() + if self.coa.balance().inFLOW() < value { + let withdrawAmount: UFix64 = value - evmFlowBalance + let vaultRef = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("Could not borrow reference to the owner's Vault!") + + self.sentVault <- vaultRef.withdraw(amount: withdrawAmount) as! @FlowToken.Vault + } else { + self.sentVault <- nil + } + } + + execute { + + // Deposit Flow into the EVM account if necessary otherwise destroy the sent Vault + if self.sentVault != nil { + self.coa.deposit(from: <-self.sentVault!) + } else { + destroy self.sentVault + } + + let valueBalance = EVM.Balance(attoflow: 0) + valueBalance.setFLOW(flow: value) + // Finally deploy the contract + let evmResult = self.coa.deploy( + code: bytecode.decodeHex(), + gasLimit: gasLimit, + value: valueBalance + ) + assert( + evmResult.status == EVM.Status.successful && evmResult.deployedContract != nil, + message: "EVM deployment failed with error code: ".concat(evmResult.errorCode.toString()) + .concat(" and message: ").concat(evmResult.errorMessage) + ) + } +} diff --git a/transactions/evm/deposit_flow.cdc b/transactions/evm/deposit_flow.cdc new file mode 100644 index 0000000..dc045a8 --- /dev/null +++ b/transactions/evm/deposit_flow.cdc @@ -0,0 +1,34 @@ +import "FungibleToken" +import "FlowToken" + +import "EVM" + +/// Deposits $FLOW to the provided EVM address from the signer's FlowToken Vault +/// +transaction(toHex: String, amount: UFix64) { + let recipient: EVM.EVMAddress + let preBalance: UFix64 + let signerVault: auth(FungibleToken.Withdraw) &FlowToken.Vault + + prepare(signer: auth(BorrowValue) &Account) { + // Parse the recipient's address + self.recipient = EVM.addressFromString(toHex) + // Note the pre-transfer balance + self.preBalance = self.recipient.balance().inFLOW() + + // Reference the signer's FlowToken Vault + self.signerVault = signer.storage.borrow(from: /storage/flowTokenVault) + ?? panic("Could not borrow reference to the owner's vault") + } + + execute { + // Withdraw tokens from the signer's vault + let fromVault <- self.signerVault.withdraw(amount: amount) as! @FlowToken.Vault + // Deposit tokens into the COA + self.recipient.deposit(from: <-fromVault) + } + + post { + self.recipient.balance().inFLOW() == self.preBalance + amount: "Error executing transfer!" + } +} From 3b4176e283ac6e325502bfcb032a7185bf181533 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:21:13 -0600 Subject: [PATCH 20/20] add Cadence tests CI workflow action --- .github/workflows/cadence_tests.yml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/cadence_tests.yml diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml new file mode 100644 index 0000000..3260b71 --- /dev/null +++ b/.github/workflows/cadence_tests.yml @@ -0,0 +1,30 @@ +name: CI + +on: pull_request + +jobs: + tests: + name: Flow CLI Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.20.x" + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Install Flow CLI + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + - name: Flow CLI Version + run: flow version + - name: Update PATH + run: echo "/root/.local/bin" >> $GITHUB_PATH + - name: Install dependencies + run: flow deps install + - name: Run tests + run: flow test --cover --covercode="contracts" --coverprofile="coverage.lcov" ./tests/*_tests.cdc