diff --git a/.gitmodules b/.gitmodules index 0254bef..35f6bab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "solidity/lib/forge-std"] path = solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "solidity/lib/flow-sol-utils"] + path = solidity/lib/flow-sol-utils + url = https://github.com/onflow/flow-sol-utils diff --git a/foundry.toml b/foundry.toml index 2b5e79b..755773f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,5 +4,6 @@ out = "./solidity/out" libs = ["./solidity/lib"] script = "./solidity/script" test = "./solidity/test" +cache_path = "./solidity/cache" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/remappings.txt b/remappings.txt index 6c3e85d..037966e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,3 +3,4 @@ ds-test/=solidity/lib/forge-std/lib/ds-test/src/ erc4626-tests/=solidity/lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=solidity/lib/forge-std/src/ openzeppelin-contracts/=solidity/lib/openzeppelin-contracts/ +flow-sol-utils=solidity/lib/flow-sol-utils/src/ \ No newline at end of file diff --git a/solidity/lib/flow-sol-utils b/solidity/lib/flow-sol-utils new file mode 160000 index 0000000..082dd3e --- /dev/null +++ b/solidity/lib/flow-sol-utils @@ -0,0 +1 @@ +Subproject commit 082dd3ec59eea0e8afacb3b49903a7564ce9ce66 diff --git a/solidity/lib/openzeppelin-contracts b/solidity/lib/openzeppelin-contracts index 69c8def..448efee 160000 --- a/solidity/lib/openzeppelin-contracts +++ b/solidity/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 +Subproject commit 448efeea6640bbbc09373f03fbc9c88e280147ba diff --git a/solidity/src/MaybeMintERC721.sol b/solidity/src/MaybeMintERC721.sol index 0906222..3f44a87 100644 --- a/solidity/src/MaybeMintERC721.sol +++ b/solidity/src/MaybeMintERC721.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.24; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {CadenceArchUtils} from "flow-sol-utils/cadence-arch/CadenceArchUtils.sol"; /** * @title MaybeMintERC721 @@ -36,10 +37,29 @@ contract MaybeMintERC721 is ERC721, Ownable { * to the beneficiary before minting the ERC721 to pay for mint */ function mint() external { - // TODO: Get a random number to determine if the mint is successful - // TODO: Set token URI + // Randomly fail mint with 50% chance of reverting + _maybeMint(); + } + + /** + * @dev Mint a new ERC721 token to the caller with some chance of failure. + * NOTE: Production systems using random minting should leverage a commit-reveal scheme + * to ensure that the random minting transaction cannot be reverted on random result. + */ + function _maybeMint() internal { + _splitChanceRevert(); // randomly revert with 50% chance + totalSupply++; // increment the total supply denomination.transferFrom(msg.sender, beneficiary, mintCost); // take payment for mint _mint(msg.sender, totalSupply); // mint the token, assigning the next tokenId + // TODO: Set token URI + } + + /** + * @dev Randomly revert with 50% chance + */ + function _splitChanceRevert() internal view { + uint64 random = CadenceArchUtils._revertibleRandom(); + require(random % 2 == 0, "No mint for you!"); } } diff --git a/solidity/test/MaybeMintERC721.t.sol b/solidity/test/MaybeMintERC721.t.sol index b1b2f9d..b4b0694 100644 --- a/solidity/test/MaybeMintERC721.t.sol +++ b/solidity/test/MaybeMintERC721.t.sol @@ -8,6 +8,9 @@ import {MaybeMintERC721} from "../src/MaybeMintERC721.sol"; import {ExampleERC20} from "../src/test/ExampleERC20.sol"; contract MaybeMintERC721Test is Test { + // Cadence Arch pre-compile address used to get onchain revertible randomness + address private cadenceArch = 0x0000000000000000000000010000000000000001; + // Contracts MaybeMintERC721 private erc721; ExampleERC20 private erc20; @@ -28,15 +31,41 @@ contract MaybeMintERC721Test is Test { erc721 = new MaybeMintERC721(name, symbol, address(erc20), mintCost, beneficiary); } - function test_mint() public { + function testMintRandomEvenSucceeds() public { erc20.mint(user, mintCost); // mint ERC20 to user vm.prank(user); erc20.approve(address(erc721), mintCost); // approve the ERC20 to be spent by MaybeMintERC721 + // Mock the Cadence Arch precompile for revertibleRandom() call, returning 0 - mint should succeed + vm.mockCall(cadenceArch, abi.encodeWithSignature("revertibleRandom()"), abi.encode(uint64(0))); + vm.prank(user); erc721.mint(); // mint ERC721 to user assertEq(erc721.ownerOf(1), user); // user should own the ERC721 token } + + function testMintRandomOddFails() public { + erc20.mint(user, mintCost); // mint ERC20 to user + + vm.prank(user); + erc20.approve(address(erc721), mintCost); // approve the ERC20 to be spent by MaybeMintERC721 + + // Mock the Cadence Arch precompile for revertibleRandom() call, returning 1 - mint should fail + vm.mockCall(cadenceArch, abi.encodeWithSignature("revertibleRandom()"), abi.encode(uint64(3))); + + vm.prank(user); + vm.expectRevert("No mint for you!"); + erc721.mint(); // Attempt to mint ERC721 to user - should revert + } + + function testMintRandomWithoutApproveFails() public { + // Mock the Cadence Arch precompile for revertibleRandom() call, returning 0 - allows mint + vm.mockCall(cadenceArch, abi.encodeWithSignature("revertibleRandom()"), abi.encode(uint64(0))); + + vm.prank(user); + vm.expectRevert(); + erc721.mint(); // Attempt to mint ERC721 to user - reverts as user has not approved ERC20 + } }