diff --git a/test/crafting/Crafting.t.sol b/test/crafting/Crafting.t.sol new file mode 100644 index 00000000..34a7ce6b --- /dev/null +++ b/test/crafting/Crafting.t.sol @@ -0,0 +1,161 @@ +// 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 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 GuardedMulticaller(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)); + + // 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(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"); + + 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.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(player); + game1155.setApprovalForAll(address(multicaller), true); + assertTrue(game1155.isApprovedForAll(player, address(multicaller))); + + multicaller.execute(signingAuthority, referenceID, targets, data, deadline, signature); + vm.stopPrank(); + + assertTrue(game1155.balanceOf(player, 1) == 9); + assertTrue(game1155.balanceOf(player, 2) == 8); + assertTrue(game721.balanceOf(player) == 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..295d04bd --- /dev/null +++ b/test/crafting/SigUtils.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +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, 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 + ) + ); + } + + /** + * + * @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)); + } + + function getStructHash( + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline + ) internal pure returns (bytes32) + { + return keccak256( + abi.encode( + MULTICALL_TYPEHASH, + _reference, + keccak256(abi.encodePacked(_targets)), + hashBytesArray(_data), + _deadline + ) + ); + } + + /** + * + * @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 keccak256( + abi.encodePacked( + "\x19\x01", + _DOMAIN_SEPARATOR, + getStructHash(_reference, _targets, _data, _deadline) + ) + ); + } +} 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);