From 83e08b5ae969533f0b5bb7ed8fb84fdaaf0bf439 Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 10:12:29 +1000 Subject: [PATCH 01/10] add wip --- test/crafting/Crafting.t.sol | 142 +++++++++++++++++++++++++++++++++++ test/crafting/SigUtils.sol | 56 ++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 test/crafting/Crafting.t.sol create mode 100644 test/crafting/SigUtils.sol diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol new file mode 100644 index 00000000..dcf721a4 --- /dev/null +++ b/test/crafting/Crafting.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; + +import {DeployOperatorAllowlist} from "../utils/DeployAllowlistProxy.sol"; +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; +import {ImmutableERC1155} from "../../contracts/token/erc1155/preset/ImmutableERC1155.sol"; +import {ImmutableERC721} from "../../contracts/token/erc721/preset/ImmutableERC721.sol"; +import {GuardedMulticaller} from "../../contracts/multicall/GuardedMulticaller.sol"; + +import {SigUtils} from "./SigUtils.sol"; + +contract CraftingTest is Test { + OperatorAllowlistUpgradeable public operatorAllowlist; + ImmutableERC1155 public game1155; + ImmutableERC721 public game721; + GuardedMulticaller public multicaller; + SigUtils public sigUtils; + + uint256 public imtblPrivateKey = 1; + uint256 public gameStudioPrivateKey = 2; + uint256 public playerOnePrivateKey = 3; + uint256 public signingAuthorityPrivateKey = 4; + + address public imtbl = vm.addr(imtblPrivateKey); + address public gameStudio = vm.addr(gameStudioPrivateKey); + address public playerOne = vm.addr(playerOnePrivateKey); + address public signingAuthority = vm.addr(signingAuthorityPrivateKey); + + address public proxyAddr; + + function setUp() public { + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); + proxyAddr = deployScript.run(imtbl, imtbl, imtbl); + operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); + + assertTrue(operatorAllowlist.hasRole(operatorAllowlist.REGISTRAR_ROLE(), imtbl)); + + game1155 = new ImmutableERC1155( + gameStudio, "test1155", "test-base-uri", "test-contract-uri", address(operatorAllowlist), gameStudio, 0 + ); + + vm.prank(gameStudio); + game1155.grantMinterRole(gameStudio); + assertTrue(game1155.hasRole(game1155.MINTER_ROLE(), gameStudio)); + + game721 = new ImmutableERC721( + gameStudio, "test721", "TST", "test-base-uri", "test-contract-uri", address(operatorAllowlist), gameStudio, 0 + ); + + vm.prank(gameStudio); + game721.grantMinterRole(gameStudio); + assertTrue(game721.hasRole(game721.MINTER_ROLE(), gameStudio)); + + // Deploy game studio's multicaller contract + multicaller = new GuardedMulticaller(gameStudio, "multicaller", "1"); + assertTrue(multicaller.hasRole(multicaller.DEFAULT_ADMIN_ROLE(), gameStudio)); + + // Grant signer role to signing authority + vm.prank(gameStudio); + multicaller.grantMulticallSignerRole(signingAuthority); + assertTrue(multicaller.hasRole(multicaller.MULTICALL_SIGNER_ROLE(), signingAuthority)); + + // permit required functions + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + GuardedMulticaller.FunctionPermit memory mintPermit = GuardedMulticaller.FunctionPermit( + address(game721), + game721.safeMint.selector, + true + ); + functionPermits[0] = mintPermit; + vm.prank(gameStudio); + multicaller.setFunctionPermits(functionPermits); + assertTrue(multicaller.isFunctionPermitted(address(game721), game721.safeMint.selector)); + + sigUtils = new SigUtils("multicaller", "1"); + } + + function testMintViaMulticaller() public { + // game721.safeMint(playerOne, 1); + + bytes32 referenceID = keccak256("testMintViaMulticaller:1"); + + address[] memory targets = new address[](1); + targets[0] = address(game721); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("safeMint(address,uint256)", playerOne, 1); + + uint256 deadline = block.timestamp + 10; + + // Construct signature + bytes32 structHash = sigUtils.getTypedDataHash(referenceID, targets, data, deadline); + + vm.startPrank(signingAuthority); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signingAuthorityPrivateKey, structHash); + bytes memory signature = abi.encodePacked(r, s, v); // note the order here is different from line above. + vm.stopPrank(); + + vm.prank(playerOne); + multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); + + assertTrue(game721.balanceOf(playerOne) == 1); + + } + + // function testSimpleCraft() public { + // // Game studio mints 10 of tokenID 1 on 1155 to user A + // vm.prank(gameStudio); + // game1155.safeMint(playerOne, 1, 10, ""); + // assertTrue(game1155.balanceOf(playerOne, 1) == 10); + + // // Game studio mints 10 of tokenID 2 on 1155 to user A + // vm.prank(gameStudio); + // game1155.safeMint(playerOne, 2, 10, ""); + // assertTrue(game1155.balanceOf(playerOne, 2) == 10); + + // // Perform a craft using the Multicaller + // // - burn 1 of 1155 tokenID 1 + // // - burn 2 of 1155 tokenID 2 + // // - mint 1 721 to playerOne + // address[] memory targets = new address[](3); + // targets[0] = address(game1155); + // targets[1] = address(game1155); + // targets[2] = address(game721); + + // bytes[] memory data = new bytes[](3); + // data[0] = ""; + // data[1] = ""; + // data[2] = ""; + + // uint256 deadline = block.timestamp + 10; + + // vm.prank(playerOne); + // multicaller.execute(signingAuthority, referenceID, targets, data, deadline, ""); + + // // assertTrue(game1155.balanceOf(playerOne, 1) == 9); + // // assertTrue(game1155.balanceOf(playerOne, 2) == 8); + // // assertTrue(game721.balanceOf(owner) == 1); + // } +} \ No newline at end of file diff --git a/test/crafting/SigUtils.sol b/test/crafting/SigUtils.sol new file mode 100644 index 00000000..0e7625c7 --- /dev/null +++ b/test/crafting/SigUtils.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +contract SigUtils is EIP712 { + bytes32 internal _DOMAIN_SEPARATOR; + + bytes32 internal constant MULTICALL_TYPEHASH = + keccak256("Multicall(bytes32 ref,address[] targets,bytes[] data,uint256 deadline)"); + + constructor(string memory _name, string memory _version) EIP712(_name, _version) {} + + /** + * + * @dev Returns hash of array of bytes + * + * @param _data Array of bytes + */ + function hashBytesArray(bytes[] memory _data) public pure returns (bytes32) { + bytes32[] memory hashedBytesArr = new bytes32[](_data.length); + for (uint256 i = 0; i < _data.length; i++) { + hashedBytesArr[i] = keccak256(_data[i]); + } + return keccak256(abi.encodePacked(hashedBytesArr)); + } + + /** + * + * @dev Returns EIP712 message hash for given parameters + * + * @param _reference Reference + * @param _targets List of addresses to call + * @param _data List of call data + * @param _deadline Expiration timestamp + */ + function getTypedDataHash( + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline + ) public view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + MULTICALL_TYPEHASH, + _reference, + keccak256(abi.encodePacked(_targets)), + hashBytesArray(_data), + _deadline + ) + ) + ); + } +} \ No newline at end of file From 8ddb5f137ac3d8a0a4105fa18526125465da82ac Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 10:12:49 +1000 Subject: [PATCH 02/10] tmp config --- .solhint.json | 2 +- .vscode/settings.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.solhint.json b/.solhint.json index 84828ef4..e1824554 100644 --- a/.solhint.json +++ b/.solhint.json @@ -10,7 +10,7 @@ "max-states-count": ["off", 15], "no-console": "error", "no-empty-blocks": "warn", - "no-global-import": "warn", + "no-global-import": "off", "no-unused-import": "warn", "no-unused-vars": "warn", "one-contract-per-file": "warn", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0062e65b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "solidity.defaultCompiler": "remote", + "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" +} \ No newline at end of file From c9b27edba92af961ad5fda85040b7bcca4f743ef Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 11:00:04 +1000 Subject: [PATCH 03/10] fix signing --- test/crafting/Crafting.t.sol | 3 +- test/crafting/SigUtils.sol | 67 +++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol index dcf721a4..9652a95b 100644 --- a/test/crafting/Crafting.t.sol +++ b/test/crafting/Crafting.t.sol @@ -74,7 +74,7 @@ contract CraftingTest is Test { multicaller.setFunctionPermits(functionPermits); assertTrue(multicaller.isFunctionPermitted(address(game721), game721.safeMint.selector)); - sigUtils = new SigUtils("multicaller", "1"); + sigUtils = new SigUtils("multicaller", "1", address(multicaller)); } function testMintViaMulticaller() public { @@ -102,7 +102,6 @@ contract CraftingTest is Test { multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); assertTrue(game721.balanceOf(playerOne) == 1); - } // function testSimpleCraft() public { diff --git a/test/crafting/SigUtils.sol b/test/crafting/SigUtils.sol index 0e7625c7..344255ee 100644 --- a/test/crafting/SigUtils.sol +++ b/test/crafting/SigUtils.sol @@ -1,15 +1,23 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity 0.8.19; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; - -contract SigUtils is EIP712 { +contract SigUtils { bytes32 internal _DOMAIN_SEPARATOR; bytes32 internal constant MULTICALL_TYPEHASH = keccak256("Multicall(bytes32 ref,address[] targets,bytes[] data,uint256 deadline)"); - constructor(string memory _name, string memory _version) EIP712(_name, _version) {} + constructor(string memory _name, string memory _version, address _verifyingContract) { + _DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(_name)), + keccak256(bytes(_version)), + block.chainid, + _verifyingContract + ) + ); + } /** * @@ -25,6 +33,36 @@ contract SigUtils is EIP712 { return keccak256(abi.encodePacked(hashedBytesArr)); } + // computes the hash of a permit + function getStructHash( + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline + ) internal pure returns (bytes32) + { + // return _hashTypedDataV4( + // keccak256( + // abi.encode( + // MULTICALL_TYPEHASH, + // _reference, + // keccak256(abi.encodePacked(_targets)), + // hashBytesArray(_data), + // _deadline + // ) + // ) + // ); + return keccak256( + abi.encode( + MULTICALL_TYPEHASH, + _reference, + keccak256(abi.encodePacked(_targets)), + hashBytesArray(_data), + _deadline + ) + ); + } + /** * * @dev Returns EIP712 message hash for given parameters @@ -35,22 +73,17 @@ contract SigUtils is EIP712 { * @param _deadline Expiration timestamp */ function getTypedDataHash( - bytes32 _reference, + bytes32 _reference, address[] calldata _targets, bytes[] calldata _data, uint256 _deadline ) public view returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - MULTICALL_TYPEHASH, - _reference, - keccak256(abi.encodePacked(_targets)), - hashBytesArray(_data), - _deadline - ) - ) - ); + return keccak256( + abi.encodePacked( + "\x19\x01", + _DOMAIN_SEPARATOR, + getStructHash(_reference, _targets, _data, _deadline) + ) + ); } } \ No newline at end of file From a152eac21e609ce21328a45be41ddc020160fffa Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 11:51:57 +1000 Subject: [PATCH 04/10] working crafting example --- test/crafting/Crafting.t.sol | 139 ++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 43 deletions(-) diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol index 9652a95b..2a7a2a10 100644 --- a/test/crafting/Crafting.t.sol +++ b/test/crafting/Crafting.t.sol @@ -31,6 +31,12 @@ contract CraftingTest is Test { address public proxyAddr; function setUp() public { + console.log("\nAddresses:"); + console.log("-- imtbl: ", imtbl); + console.log("-- gameStudio: ", gameStudio); + console.log("-- playerOne: ", playerOne); + console.log("-- signingAuthority: ", signingAuthority); + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); proxyAddr = deployScript.run(imtbl, imtbl, imtbl); operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); @@ -49,37 +55,61 @@ contract CraftingTest is Test { gameStudio, "test721", "TST", "test-base-uri", "test-contract-uri", address(operatorAllowlist), gameStudio, 0 ); - vm.prank(gameStudio); - game721.grantMinterRole(gameStudio); - assertTrue(game721.hasRole(game721.MINTER_ROLE(), gameStudio)); - // Deploy game studio's multicaller contract multicaller = new GuardedMulticaller(gameStudio, "multicaller", "1"); assertTrue(multicaller.hasRole(multicaller.DEFAULT_ADMIN_ROLE(), gameStudio)); + // Add multicaller to operator allowlist + address[] memory allowlistTargets = new address[](1); + allowlistTargets[0] = address(multicaller); + + vm.prank(imtbl); + operatorAllowlist.addAddressesToAllowlist(allowlistTargets); + assertTrue(operatorAllowlist.isAllowlisted(address(multicaller))); + + // Grant minter role to the game studio + vm.startPrank(gameStudio); + game721.grantMinterRole(gameStudio); + assertTrue(game721.hasRole(game721.MINTER_ROLE(), gameStudio)); + + // Grant minter role to the multicaller contract + game721.grantMinterRole(address(multicaller)); + assertTrue(game721.hasRole(game721.MINTER_ROLE(), address(multicaller))); + vm.stopPrank(); + // Grant signer role to signing authority vm.prank(gameStudio); multicaller.grantMulticallSignerRole(signingAuthority); assertTrue(multicaller.hasRole(multicaller.MULTICALL_SIGNER_ROLE(), signingAuthority)); - // permit required functions - GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + // Permit required functions + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](2); GuardedMulticaller.FunctionPermit memory mintPermit = GuardedMulticaller.FunctionPermit( address(game721), game721.safeMint.selector, true ); + GuardedMulticaller.FunctionPermit memory burnPermit = GuardedMulticaller.FunctionPermit( + address(game1155), + game1155.burnBatch.selector, + true + ); functionPermits[0] = mintPermit; + functionPermits[1] = burnPermit; vm.prank(gameStudio); multicaller.setFunctionPermits(functionPermits); assertTrue(multicaller.isFunctionPermitted(address(game721), game721.safeMint.selector)); + assertTrue(multicaller.isFunctionPermitted(address(game1155), game1155.burnBatch.selector)); sigUtils = new SigUtils("multicaller", "1", address(multicaller)); + + console.log("\nContracts:"); + console.log("-- game1155: ", address(game1155)); + console.log("-- game721: ", address(game721)); + console.log("-- multicaller: ", address(multicaller)); } function testMintViaMulticaller() public { - // game721.safeMint(playerOne, 1); - bytes32 referenceID = keccak256("testMintViaMulticaller:1"); address[] memory targets = new address[](1); @@ -95,7 +125,7 @@ contract CraftingTest is Test { vm.startPrank(signingAuthority); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signingAuthorityPrivateKey, structHash); - bytes memory signature = abi.encodePacked(r, s, v); // note the order here is different from line above. + bytes memory signature = abi.encodePacked(r, s, v); vm.stopPrank(); vm.prank(playerOne); @@ -104,38 +134,61 @@ contract CraftingTest is Test { assertTrue(game721.balanceOf(playerOne) == 1); } - // function testSimpleCraft() public { - // // Game studio mints 10 of tokenID 1 on 1155 to user A - // vm.prank(gameStudio); - // game1155.safeMint(playerOne, 1, 10, ""); - // assertTrue(game1155.balanceOf(playerOne, 1) == 10); - - // // Game studio mints 10 of tokenID 2 on 1155 to user A - // vm.prank(gameStudio); - // game1155.safeMint(playerOne, 2, 10, ""); - // assertTrue(game1155.balanceOf(playerOne, 2) == 10); - - // // Perform a craft using the Multicaller - // // - burn 1 of 1155 tokenID 1 - // // - burn 2 of 1155 tokenID 2 - // // - mint 1 721 to playerOne - // address[] memory targets = new address[](3); - // targets[0] = address(game1155); - // targets[1] = address(game1155); - // targets[2] = address(game721); - - // bytes[] memory data = new bytes[](3); - // data[0] = ""; - // data[1] = ""; - // data[2] = ""; - - // uint256 deadline = block.timestamp + 10; - - // vm.prank(playerOne); - // multicaller.execute(signingAuthority, referenceID, targets, data, deadline, ""); - - // // assertTrue(game1155.balanceOf(playerOne, 1) == 9); - // // assertTrue(game1155.balanceOf(playerOne, 2) == 8); - // // assertTrue(game721.balanceOf(owner) == 1); - // } + function testCraft() public { + // Game studio mints 10 of tokenID 1 on 1155 to user A + vm.prank(gameStudio); + game1155.safeMint(playerOne, 1, 10, ""); + assertTrue(game1155.balanceOf(playerOne, 1) == 10); + + // Game studio mints 10 of tokenID 2 on 1155 to user A + vm.prank(gameStudio); + game1155.safeMint(playerOne, 2, 10, ""); + assertTrue(game1155.balanceOf(playerOne, 2) == 10); + + // Perform a craft using the Multicaller + // - burn 1 of 1155 tokenID 1 + // - burn 2 of 1155 tokenID 2 + // - mint 1 721 to playerOne + + bytes32 referenceID = keccak256("testCraft:1"); + + address[] memory targets = new address[](2); + targets[0] = address(game1155); + targets[1] = address(game721); + + bytes[] memory data = new bytes[](2); + + uint256[] memory ids = new uint256[](2); + ids[0] = 1; + ids[1] = 2; + + uint256[] memory values = new uint256[](2); + values[0] = 1; + values[1] = 2; + + data[0] = abi.encodeWithSignature("burnBatch(address,uint256[],uint256[])", playerOne, ids, values); + data[1] = abi.encodeWithSignature("safeMint(address,uint256)", playerOne, 1); + + uint256 deadline = block.timestamp + 10; + + // Construct signature + bytes32 structHash = sigUtils.getTypedDataHash(referenceID, targets, data, deadline); + + vm.startPrank(signingAuthority); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signingAuthorityPrivateKey, structHash); + bytes memory signature = abi.encodePacked(r, s, v); + vm.stopPrank(); + + // Give multicaller approve to burn + vm.startPrank(playerOne); + game1155.setApprovalForAll(address(multicaller), true); + assertTrue(game1155.isApprovedForAll(playerOne, address(multicaller))); + + multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); + vm.stopPrank(); + + assertTrue(game1155.balanceOf(playerOne, 1) == 9); + assertTrue(game1155.balanceOf(playerOne, 2) == 8); + assertTrue(game721.balanceOf(playerOne) == 1); + } } \ No newline at end of file From 779b82bf2cbecb1de52e44163fdab79e8aa8eb5a Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 11:59:00 +1000 Subject: [PATCH 05/10] revert --- .solhint.json | 4 ++-- .vscode/settings.json | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.solhint.json b/.solhint.json index e1824554..473ec1c5 100644 --- a/.solhint.json +++ b/.solhint.json @@ -10,7 +10,7 @@ "max-states-count": ["off", 15], "no-console": "error", "no-empty-blocks": "warn", - "no-global-import": "off", + "no-global-import": "warn", "no-unused-import": "warn", "no-unused-vars": "warn", "one-contract-per-file": "warn", @@ -32,7 +32,7 @@ "var-name-mixedcase": "warn", "imports-on-top": "warn", "visibility-modifier-order": "warn", - + "avoid-call-value": "error", "avoid-low-level-calls": "error", "avoid-sha3": "error", diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 0062e65b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "solidity.defaultCompiler": "remote", - "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" -} \ No newline at end of file From a063100634f1363229016241fa6e32d725f14eda Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 13:20:58 +1000 Subject: [PATCH 06/10] clean up --- test/crafting/Crafting.t.sol | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol index 2a7a2a10..7e7cc8fb 100644 --- a/test/crafting/Crafting.t.sol +++ b/test/crafting/Crafting.t.sol @@ -31,12 +31,6 @@ contract CraftingTest is Test { address public proxyAddr; function setUp() public { - console.log("\nAddresses:"); - console.log("-- imtbl: ", imtbl); - console.log("-- gameStudio: ", gameStudio); - console.log("-- playerOne: ", playerOne); - console.log("-- signingAuthority: ", signingAuthority); - DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); proxyAddr = deployScript.run(imtbl, imtbl, imtbl); operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); @@ -102,36 +96,6 @@ contract CraftingTest is Test { assertTrue(multicaller.isFunctionPermitted(address(game1155), game1155.burnBatch.selector)); sigUtils = new SigUtils("multicaller", "1", address(multicaller)); - - console.log("\nContracts:"); - console.log("-- game1155: ", address(game1155)); - console.log("-- game721: ", address(game721)); - console.log("-- multicaller: ", address(multicaller)); - } - - function testMintViaMulticaller() public { - bytes32 referenceID = keccak256("testMintViaMulticaller:1"); - - address[] memory targets = new address[](1); - targets[0] = address(game721); - - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSignature("safeMint(address,uint256)", playerOne, 1); - - uint256 deadline = block.timestamp + 10; - - // Construct signature - bytes32 structHash = sigUtils.getTypedDataHash(referenceID, targets, data, deadline); - - vm.startPrank(signingAuthority); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signingAuthorityPrivateKey, structHash); - bytes memory signature = abi.encodePacked(r, s, v); - vm.stopPrank(); - - vm.prank(playerOne); - multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); - - assertTrue(game721.balanceOf(playerOne) == 1); } function testCraft() public { From 44c7cc5a9872db854eb34274b2f4bc30ce8e0ff7 Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 13:22:23 +1000 Subject: [PATCH 07/10] clean up --- test/crafting/SigUtils.sol | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/test/crafting/SigUtils.sol b/test/crafting/SigUtils.sol index 344255ee..295d04bd 100644 --- a/test/crafting/SigUtils.sol +++ b/test/crafting/SigUtils.sol @@ -33,7 +33,6 @@ contract SigUtils { return keccak256(abi.encodePacked(hashedBytesArr)); } - // computes the hash of a permit function getStructHash( bytes32 _reference, address[] calldata _targets, @@ -41,17 +40,6 @@ contract SigUtils { uint256 _deadline ) internal pure returns (bytes32) { - // return _hashTypedDataV4( - // keccak256( - // abi.encode( - // MULTICALL_TYPEHASH, - // _reference, - // keccak256(abi.encodePacked(_targets)), - // hashBytesArray(_data), - // _deadline - // ) - // ) - // ); return keccak256( abi.encode( MULTICALL_TYPEHASH, @@ -86,4 +74,4 @@ contract SigUtils { ) ); } -} \ No newline at end of file +} From d4da7806d0d35e76ac6d5730f87c9cc6417f231f Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 13:34:00 +1000 Subject: [PATCH 08/10] clean up --- test/crafting/Crafting.t.sol | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol index 7e7cc8fb..34a7ce6b 100644 --- a/test/crafting/Crafting.t.sol +++ b/test/crafting/Crafting.t.sol @@ -20,16 +20,19 @@ contract CraftingTest is Test { uint256 public imtblPrivateKey = 1; uint256 public gameStudioPrivateKey = 2; - uint256 public playerOnePrivateKey = 3; - uint256 public signingAuthorityPrivateKey = 4; + uint256 public signingAuthorityPrivateKey = 3; + uint256 public playerPrivateKey = 4; address public imtbl = vm.addr(imtblPrivateKey); address public gameStudio = vm.addr(gameStudioPrivateKey); - address public playerOne = vm.addr(playerOnePrivateKey); address public signingAuthority = vm.addr(signingAuthorityPrivateKey); + address public player = vm.addr(playerPrivateKey); address public proxyAddr; + string public multicallerName = "multicaller-name"; + string public multicallerVersion = "multicaller-version"; + function setUp() public { DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); proxyAddr = deployScript.run(imtbl, imtbl, imtbl); @@ -50,7 +53,7 @@ contract CraftingTest is Test { ); // Deploy game studio's multicaller contract - multicaller = new GuardedMulticaller(gameStudio, "multicaller", "1"); + multicaller = new GuardedMulticaller(gameStudio, multicallerName, multicallerVersion); assertTrue(multicaller.hasRole(multicaller.DEFAULT_ADMIN_ROLE(), gameStudio)); // Add multicaller to operator allowlist @@ -95,24 +98,24 @@ contract CraftingTest is Test { assertTrue(multicaller.isFunctionPermitted(address(game721), game721.safeMint.selector)); assertTrue(multicaller.isFunctionPermitted(address(game1155), game1155.burnBatch.selector)); - sigUtils = new SigUtils("multicaller", "1", address(multicaller)); + sigUtils = new SigUtils(multicallerName, multicallerVersion, address(multicaller)); } function testCraft() public { - // Game studio mints 10 of tokenID 1 on 1155 to user A + // Game studio mints 10 of tokenID 1 on 1155 to player vm.prank(gameStudio); - game1155.safeMint(playerOne, 1, 10, ""); - assertTrue(game1155.balanceOf(playerOne, 1) == 10); + game1155.safeMint(player, 1, 10, ""); + assertTrue(game1155.balanceOf(player, 1) == 10); - // Game studio mints 10 of tokenID 2 on 1155 to user A + // Game studio mints 10 of tokenID 2 on 1155 to player vm.prank(gameStudio); - game1155.safeMint(playerOne, 2, 10, ""); - assertTrue(game1155.balanceOf(playerOne, 2) == 10); + game1155.safeMint(player, 2, 10, ""); + assertTrue(game1155.balanceOf(player, 2) == 10); // Perform a craft using the Multicaller // - burn 1 of 1155 tokenID 1 // - burn 2 of 1155 tokenID 2 - // - mint 1 721 to playerOne + // - mint 1 721 to player bytes32 referenceID = keccak256("testCraft:1"); @@ -130,8 +133,8 @@ contract CraftingTest is Test { values[0] = 1; values[1] = 2; - data[0] = abi.encodeWithSignature("burnBatch(address,uint256[],uint256[])", playerOne, ids, values); - data[1] = abi.encodeWithSignature("safeMint(address,uint256)", playerOne, 1); + data[0] = abi.encodeWithSignature("burnBatch(address,uint256[],uint256[])", player, ids, values); + data[1] = abi.encodeWithSignature("safeMint(address,uint256)", player, 1); uint256 deadline = block.timestamp + 10; @@ -144,15 +147,15 @@ contract CraftingTest is Test { vm.stopPrank(); // Give multicaller approve to burn - vm.startPrank(playerOne); + vm.startPrank(player); game1155.setApprovalForAll(address(multicaller), true); - assertTrue(game1155.isApprovedForAll(playerOne, address(multicaller))); + assertTrue(game1155.isApprovedForAll(player, address(multicaller))); multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); vm.stopPrank(); - assertTrue(game1155.balanceOf(playerOne, 1) == 9); - assertTrue(game1155.balanceOf(playerOne, 2) == 8); - assertTrue(game721.balanceOf(playerOne) == 1); + assertTrue(game1155.balanceOf(player, 1) == 9); + assertTrue(game1155.balanceOf(player, 2) == 8); + assertTrue(game721.balanceOf(player) == 1); } } \ No newline at end of file From 3df56cff3665eea95b6706afd39d09523bab21ac Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 23 Aug 2024 13:42:58 +1000 Subject: [PATCH 09/10] clean up --- .solhint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.solhint.json b/.solhint.json index 473ec1c5..84828ef4 100644 --- a/.solhint.json +++ b/.solhint.json @@ -32,7 +32,7 @@ "var-name-mixedcase": "warn", "imports-on-top": "warn", "visibility-modifier-order": "warn", - + "avoid-call-value": "error", "avoid-low-level-calls": "error", "avoid-sha3": "error", From ec9dabb2561e1848e0b01748e181cb1504bd6709 Mon Sep 17 00:00:00 2001 From: Allan Almeida Date: Fri, 6 Sep 2024 10:08:42 +1000 Subject: [PATCH 10/10] add crafting example with gmc2 --- test/multicall/Crafting2.t.sol | 176 +++++++++++++++++++++++ test/multicall/GuardedMulticaller2.t.sol | 22 +-- 2 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 test/multicall/Crafting2.t.sol diff --git a/test/multicall/Crafting2.t.sol b/test/multicall/Crafting2.t.sol new file mode 100644 index 00000000..64542917 --- /dev/null +++ b/test/multicall/Crafting2.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {DeployOperatorAllowlist} from "../utils/DeployAllowlistProxy.sol"; +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; +import {ImmutableERC1155} from "../../contracts/token/erc1155/preset/ImmutableERC1155.sol"; +import {ImmutableERC721} from "../../contracts/token/erc721/preset/ImmutableERC721.sol"; +import {GuardedMulticaller2} from "../../contracts/multicall/GuardedMulticaller2.sol"; + +import {SigUtils} from "./SigUtils.t.sol"; + +contract Crafting2Test is Test { + OperatorAllowlistUpgradeable public operatorAllowlist; + ImmutableERC1155 public game1155; + ImmutableERC721 public game721; + GuardedMulticaller2 public multicaller; + SigUtils public sigUtils; + + uint256 public imtblPrivateKey = 1; + uint256 public gameStudioPrivateKey = 2; + uint256 public signingAuthorityPrivateKey = 3; + uint256 public playerPrivateKey = 4; + + address public imtbl = vm.addr(imtblPrivateKey); + address public gameStudio = vm.addr(gameStudioPrivateKey); + address public signingAuthority = vm.addr(signingAuthorityPrivateKey); + address public player = vm.addr(playerPrivateKey); + + address public proxyAddr; + + string public multicallerName = "multicaller-name"; + string public multicallerVersion = "multicaller-version"; + + function setUp() public { + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); + proxyAddr = deployScript.run(imtbl, imtbl, imtbl); + operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); + + assertTrue(operatorAllowlist.hasRole(operatorAllowlist.REGISTRAR_ROLE(), imtbl)); + + game1155 = new ImmutableERC1155( + gameStudio, "test1155", "test-base-uri", "test-contract-uri", address(operatorAllowlist), gameStudio, 0 + ); + + vm.prank(gameStudio); + game1155.grantMinterRole(gameStudio); + assertTrue(game1155.hasRole(game1155.MINTER_ROLE(), gameStudio)); + + game721 = new ImmutableERC721( + gameStudio, "test721", "TST", "test-base-uri", "test-contract-uri", address(operatorAllowlist), gameStudio, 0 + ); + + // Deploy game studio's multicaller contract + multicaller = new GuardedMulticaller2(gameStudio, multicallerName, multicallerVersion); + assertTrue(multicaller.hasRole(multicaller.DEFAULT_ADMIN_ROLE(), gameStudio)); + + // Add multicaller to operator allowlist + address[] memory allowlistTargets = new address[](1); + allowlistTargets[0] = address(multicaller); + + vm.prank(imtbl); + operatorAllowlist.addAddressesToAllowlist(allowlistTargets); + assertTrue(operatorAllowlist.isAllowlisted(address(multicaller))); + + // Grant minter role to the game studio + vm.startPrank(gameStudio); + game721.grantMinterRole(gameStudio); + assertTrue(game721.hasRole(game721.MINTER_ROLE(), gameStudio)); + + // Grant minter role to the multicaller contract + game721.grantMinterRole(address(multicaller)); + assertTrue(game721.hasRole(game721.MINTER_ROLE(), address(multicaller))); + vm.stopPrank(); + + // Grant signer role to signing authority + vm.prank(gameStudio); + multicaller.grantMulticallSignerRole(signingAuthority); + assertTrue(multicaller.hasRole(multicaller.MULTICALL_SIGNER_ROLE(), signingAuthority)); + + sigUtils = new SigUtils(multicallerName, multicallerVersion, address(multicaller)); + } + + function testCraft() public { + // Game studio mints 10 of tokenID 1 on 1155 to player + vm.prank(gameStudio); + game1155.safeMint(player, 1, 10, ""); + assertTrue(game1155.balanceOf(player, 1) == 10); + + // Game studio mints 10 of tokenID 2 on 1155 to player + vm.prank(gameStudio); + game1155.safeMint(player, 2, 10, ""); + assertTrue(game1155.balanceOf(player, 2) == 10); + + // Perform a craft using the Multicaller + // - burn 1 of 1155 tokenID 1 + // - burn 2 of 1155 tokenID 2 + // - mint 1 721 to player + + bytes32 referenceID = keccak256("testCraft:1"); + + uint256[] memory ids = new uint256[](2); + ids[0] = 1; + ids[1] = 2; + + uint256[] memory values = new uint256[](2); + values[0] = 1; + values[1] = 2; + + // Construct signature + GuardedMulticaller2.Call[] memory calls = new GuardedMulticaller2.Call[](2); + + calls[0] = GuardedMulticaller2.Call( + address(game1155), + "burnBatch(address,uint256[],uint256[])", + abi.encode(player, ids, values) + ); + + calls[1] = GuardedMulticaller2.Call( + address(game721), + "safeMint(address,uint256)", + abi.encode(player, uint256(1)) + ); + + uint256 deadline = block.timestamp + 10; + + bytes32 structHash = sigUtils.hashTypedData(referenceID, calls, deadline); + + vm.startPrank(signingAuthority); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signingAuthorityPrivateKey, structHash); + bytes memory signature = abi.encodePacked(r, s, v); + vm.stopPrank(); + + // Give multicaller approve to burn + vm.startPrank(player); + game1155.setApprovalForAll(address(multicaller), true); + assertTrue(game1155.isApprovedForAll(player, address(multicaller))); + + multicaller.execute(signingAuthority, referenceID, calls, deadline, signature); + vm.stopPrank(); + + assertTrue(game1155.balanceOf(player, 1) == 9); + assertTrue(game1155.balanceOf(player, 2) == 8); + assertTrue(game721.balanceOf(player) == 1); + } + + // function testSignature() public { + // bytes32 referenceID = keccak256("testCraft:1"); + + // address[] memory targets = new address[](2); + // targets[0] = address(game1155); + // targets[1] = address(game721); + + // bytes[] memory data = new bytes[](2); + + // uint256[] memory ids = new uint256[](2); + // ids[0] = 1; + // ids[1] = 2; + + // uint256[] memory values = new uint256[](2); + // values[0] = 1; + // values[1] = 2; + + // data[0] = abi.encodeWithSignature("burnBatch(address,uint256[],uint256[])", player, ids, values); + // data[1] = abi.encodeWithSignature("safeMint(address,uint256)", player, 1); + + // uint256 deadline = block.timestamp + 10; + + // // Construct signature + // bytes32 structHash = sigUtils.hashTypedData(referenceID, calls, deadline); + + // console.log("structHash", structHash); + // } +} \ No newline at end of file diff --git a/test/multicall/GuardedMulticaller2.t.sol b/test/multicall/GuardedMulticaller2.t.sol index 7e73a6f9..eede1cc8 100644 --- a/test/multicall/GuardedMulticaller2.t.sol +++ b/test/multicall/GuardedMulticaller2.t.sol @@ -49,13 +49,13 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); calls[1] = GuardedMulticaller2.Call(address(target), "succeed()", ""); calls[2] = GuardedMulticaller2.Call( address(target1), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -80,7 +80,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -99,7 +99,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -118,7 +118,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -149,7 +149,7 @@ contract GuardedMulticaller2Test is Test { bytes32 ref = "ref"; uint256 deadline = block.timestamp + 1; GuardedMulticaller2.Call[] memory calls = new GuardedMulticaller2.Call[](1); - calls[0] = GuardedMulticaller2.Call(address(0), "succeedWithUint256(uint256)", abi.encodePacked(uint256(42))); + calls[0] = GuardedMulticaller2.Call(address(0), "succeedWithUint256(uint256)", abi.encode(uint256(42))); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); @@ -168,7 +168,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -188,7 +188,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -222,7 +222,7 @@ contract GuardedMulticaller2Test is Test { calls[0] = GuardedMulticaller2.Call( address(target), "succeedWithUint256(uint256)", - abi.encodePacked(uint256(42)) + abi.encode(uint256(42)) ); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); @@ -250,7 +250,7 @@ contract GuardedMulticaller2Test is Test { bytes32 ref = keccak256("ref"); uint256 deadline = block.timestamp + 1; GuardedMulticaller2.Call[] memory calls = new GuardedMulticaller2.Call[](1); - calls[0] = GuardedMulticaller2.Call(address(target), "revertWithData(uint256)", abi.encodePacked(uint256(42))); + calls[0] = GuardedMulticaller2.Call(address(target), "revertWithData(uint256)", abi.encode(uint256(42))); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); @@ -265,7 +265,7 @@ contract GuardedMulticaller2Test is Test { bytes32 ref = keccak256("ref"); uint256 deadline = block.timestamp + 1; GuardedMulticaller2.Call[] memory calls = new GuardedMulticaller2.Call[](1); - calls[0] = GuardedMulticaller2.Call(address(target), "", abi.encodePacked(uint256(42))); + calls[0] = GuardedMulticaller2.Call(address(target), "", abi.encode(uint256(42))); bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest);