diff --git a/BUILD.md b/BUILD.md index 03492dc5d..c54be879d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,7 +15,6 @@ To build and test the contracts: ``` forge test -vvv -yarn test ``` ## Solidity Linter diff --git a/contracts/test/allowlist/OperatorAllowlist.sol b/contracts/test/allowlist/OperatorAllowlist.sol deleted file mode 100644 index fff88ea84..000000000 --- a/contracts/test/allowlist/OperatorAllowlist.sol +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2.0 -pragma solidity >=0.8.19 <0.8.29; - -// Access Control -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; - -// Introspection -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; - -// Interfaces -import {IOperatorAllowlist} from "../../allowlist/IOperatorAllowlist.sol"; - -// Interface to retrieve the implementation stored inside the Proxy contract -interface IProxy { - // Returns the current implementation address used by the proxy contract - // solhint-disable-next-line func-name-mixedcase - function PROXY_getImplementation() external view returns (address); -} - -/* - OperatorAllowlist is an implementation of a Allowlist registry, storing addresses and bytecode - which are allowed to be approved operators and execute transfers of interfacing token contracts (e.g. ERC721/ERC1155). - The registry will be a deployed contract that tokens may interface with and point to. - OperatorAllowlist is not designed to be upgradeable or extended. -*/ - -contract OperatorAllowlist is ERC165, AccessControl, IOperatorAllowlist { - /// ===== State Variables ===== - - /// @notice Only REGISTRAR_ROLE can invoke white listing registration and removal - bytes32 public constant REGISTRAR_ROLE = bytes32("REGISTRAR_ROLE"); - - /// @notice Mapping of Allowlisted addresses - mapping(address aContract => bool allowed) private addressAllowlist; - - /// @notice Mapping of Allowlisted implementation addresses - mapping(address impl => bool allowed) private addressImplementationAllowlist; - - /// @notice Mapping of Allowlisted bytecodes - mapping(bytes32 bytecodeHash => bool allowed) private bytecodeAllowlist; - - /// ===== Events ===== - - /// @notice Emitted when a target address is added or removed from the Allowlist - event AddressAllowlistChanged(address indexed target, bool added); - - /// @notice Emitted when a target smart contract wallet is added or removed from the Allowlist - event WalletAllowlistChanged(bytes32 indexed targetBytes, address indexed targetAddress, bool added); - - /// ===== Constructor ===== - - /** - * @notice Grants `DEFAULT_ADMIN_ROLE` to the supplied `admin` address - * @param admin the address to grant `DEFAULT_ADMIN_ROLE` to - */ - constructor(address admin) { - _grantRole(DEFAULT_ADMIN_ROLE, admin); - } - - /// ===== External functions ===== - - /** - * @notice Add a target address to Allowlist - * @param addressTargets the addresses to be added to the allowlist - */ - function addAddressToAllowlist(address[] calldata addressTargets) external onlyRole(REGISTRAR_ROLE) { - for (uint256 i; i < addressTargets.length; i++) { - addressAllowlist[addressTargets[i]] = true; - emit AddressAllowlistChanged(addressTargets[i], true); - } - } - - /** - * @notice Remove a target address from Allowlist - * @param addressTargets the addresses to be removed from the allowlist - */ - function removeAddressFromAllowlist(address[] calldata addressTargets) external onlyRole(REGISTRAR_ROLE) { - for (uint256 i; i < addressTargets.length; i++) { - delete addressAllowlist[addressTargets[i]]; - emit AddressAllowlistChanged(addressTargets[i], false); - } - } - - /** - * @notice Add a smart contract wallet to the Allowlist. - * This will allowlist the proxy and implementation contract pair. - * First, the bytecode of the proxy is added to the bytecode allowlist. - * Second, the implementation address stored in the proxy is stored in the - * implementation address allowlist. - * @param walletAddr the wallet address to be added to the allowlist - */ - function addWalletToAllowlist(address walletAddr) external onlyRole(REGISTRAR_ROLE) { - // get bytecode of wallet - bytes32 codeHash; - // solhint-disable-next-line no-inline-assembly - assembly { - codeHash := extcodehash(walletAddr) - } - bytecodeAllowlist[codeHash] = true; - // get address of wallet module - address impl = IProxy(walletAddr).PROXY_getImplementation(); - addressImplementationAllowlist[impl] = true; - - emit WalletAllowlistChanged(codeHash, walletAddr, true); - } - - /** - * @notice Remove a smart contract wallet from the Allowlist - * This will remove the proxy bytecode hash and implementation contract address pair from the allowlist - * @param walletAddr the wallet address to be removed from the allowlist - */ - function removeWalletFromAllowlist(address walletAddr) external onlyRole(REGISTRAR_ROLE) { - // get bytecode of wallet - bytes32 codeHash; - // solhint-disable-next-line no-inline-assembly - assembly { - codeHash := extcodehash(walletAddr) - } - delete bytecodeAllowlist[codeHash]; - // get address of wallet module - address impl = IProxy(walletAddr).PROXY_getImplementation(); - delete addressImplementationAllowlist[impl]; - - emit WalletAllowlistChanged(codeHash, walletAddr, false); - } - - /** - * @notice Allows admin to grant `user` `REGISTRAR_ROLE` role - * @param user the address that `REGISTRAR_ROLE` will be granted to - */ - function grantRegistrarRole(address user) external onlyRole(DEFAULT_ADMIN_ROLE) { - grantRole(REGISTRAR_ROLE, user); - } - - /** - * @notice Allows admin to revoke `REGISTRAR_ROLE` role from `user` - * @param user the address that `REGISTRAR_ROLE` will be revoked from - */ - function revokeRegistrarRole(address user) external onlyRole(DEFAULT_ADMIN_ROLE) { - revokeRole(REGISTRAR_ROLE, user); - } - - /// ===== View functions ===== - - /** - * @notice Returns true if an address is Allowlisted, false otherwise - * @param target the address that will be checked for presence in the allowlist - */ - function isAllowlisted(address target) external view override returns (bool) { - if (addressAllowlist[target]) { - return true; - } - - // Check if caller is a Allowlisted smart contract wallet - bytes32 codeHash; - // solhint-disable-next-line no-inline-assembly - assembly { - codeHash := extcodehash(target) - } - if (bytecodeAllowlist[codeHash]) { - // If wallet proxy bytecode is approved, check addr of implementation contract - address impl = IProxy(target).PROXY_getImplementation(); - - return addressImplementationAllowlist[impl]; - } - - return false; - } - - /** - * @notice ERC-165 interface support - * @param interfaceId The interface identifier, which is a 4-byte selector. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, AccessControl) returns (bool) { - return interfaceId == type(IOperatorAllowlist).interfaceId || super.supportsInterface(interfaceId); - } -} diff --git a/contracts/trading/seaport/test/SeaportTestContracts.sol b/contracts/trading/seaport/test/SeaportTestContracts.sol deleted file mode 100644 index e9e6cf326..000000000 --- a/contracts/trading/seaport/test/SeaportTestContracts.sol +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache-2 -// solhint-disable -pragma solidity >=0.8.4; - -/** - * @dev Import test contract helpers from Immutable pinned fork of OpenSea's seaport - * These are not deployed - they are only used for testing - */ -import "seaport/contracts/test/TestERC721.sol"; -import "seaport/contracts/test/TestZone.sol"; diff --git a/test/multicall/GuardedMulticaller.t.sol b/test/multicall/GuardedMulticaller.t.sol new file mode 100644 index 000000000..3328bfce3 --- /dev/null +++ b/test/multicall/GuardedMulticaller.t.sol @@ -0,0 +1,370 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {GuardedMulticaller} from "../../contracts/multicall/GuardedMulticaller.sol"; +import {MockFunctions} from "./MockFunctions.sol"; +import {SigUtils} from "./SigUtils.t.sol"; + +contract GuardedMulticallerTest is Test { + + GuardedMulticaller public gmc; + MockFunctions public mock; + SigUtils public sigUtils; + + address public deployer; + address public signer; + uint256 public signerPk; + address public user; + uint256 public userPk; + + string public constant MULTICALLER_NAME = "Multicaller"; + string public constant MULTICALLER_VERSION = "v1"; + + bytes32 public ref; + uint256 public deadline; + + function setUp() public { + deployer = makeAddr("deployer"); + (signer, signerPk) = makeAddrAndKey("signer"); + (user, userPk) = makeAddrAndKey("user"); + + vm.prank(deployer); + gmc = new GuardedMulticaller(deployer, MULTICALLER_NAME, MULTICALLER_VERSION); + vm.prank(deployer); + gmc.grantMulticallSignerRole(signer); + + sigUtils = new SigUtils(MULTICALLER_NAME, MULTICALLER_VERSION, address(gmc)); + + + vm.prank(deployer); + mock = new MockFunctions(); + + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](2); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.succeed.selector, + permitted: true + }); + functionPermits[1] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.revertWithNoReason.selector, + permitted: true + }); + vm.prank(deployer); + gmc.setFunctionPermits(functionPermits); + + + deadline = block.timestamp + 30 minutes; + ref = keccak256(abi.encodePacked("test_ref")); + } + + function test_SuccessfulExecution() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MockFunctions.succeed.selector); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit GuardedMulticaller.Multicalled(signer, ref, targets, data, deadline); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertWithCustomError() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("revertWithNoReason()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.FailedCall.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfDeadlinePassed() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + + uint256 expiredDeadline = block.timestamp; + vm.warp(expiredDeadline + 30 minutes); + bytes memory signature = signTypedData(signerPk, ref, targets, data, expiredDeadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.Expired.selector, expiredDeadline)); + gmc.execute(signer, ref, targets, data, expiredDeadline, signature); + } + + function test_RevertIfReferenceReused() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + gmc.execute(signer, ref, targets, data, deadline, signature); + + vm.prank(user); + vm.expectRevert(abi.encodePacked(GuardedMulticaller.ReusedReference.selector, ref)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfInvalidReference() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes32 invalidRef = bytes32(0); + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.InvalidReference.selector, invalidRef)); + gmc.execute(signer, invalidRef, targets, data, deadline, signature); + } + + function test_RevertIfUnauthorizedSigner() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSigner.selector, user)); + // Note: execute called with user as signer. + gmc.execute(user, ref, targets, data, deadline, signature); + } + + function test_RevertIfSignatureMismatch() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(userPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSignature.selector, signature)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfEmptyTargets() public { + address[] memory targets = new address[](0); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.EmptyAddressArray.selector)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfTargetsDataMismatch() public { + address[] memory targets = new address[](2); + targets[0] = address(mock); + targets[1] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector( + GuardedMulticaller.AddressDataArrayLengthsMismatch.selector, + targets.length, data.length)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfFunctionNotPermitted() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("notPermitted()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedFunction.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfFunctionDisallowed() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.succeed.selector, + permitted: false + }); + vm.prank(deployer); + gmc.setFunctionPermits(functionPermits); + + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedFunction.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfInvalidSignature() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes32 maliciousRef = keccak256(abi.encodePacked("malicious_ref")); + bytes memory signature = signTypedData(signerPk, maliciousRef, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSignature.selector, signature)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_EmitFunctionPermittedEvent() public { + vm.startPrank(deployer); + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.succeed.selector, + permitted: true + }); + vm.expectEmit(true, true, true, true); + emit GuardedMulticaller.FunctionPermitted( + address(mock), + MockFunctions.succeed.selector, + true + ); + gmc.setFunctionPermits(functionPermits); + + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.succeed.selector, + permitted: false + }); + vm.expectEmit(true, true, true, true); + emit GuardedMulticaller.FunctionPermitted( + address(mock), + MockFunctions.succeed.selector, + false + ); + gmc.setFunctionPermits(functionPermits); + + vm.stopPrank(); + } + + function test_RevertIfEmptyFunctionPermits() public { + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.EmptyFunctionPermitArray.selector)); + gmc.setFunctionPermits(new GuardedMulticaller.FunctionPermit[](0)); + } + + function test_RevertIfAccessControlIssueWhileSettingFunctionPermits() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), + functionSelector: MockFunctions.succeed.selector, + permitted: false + }); + vm.prank(user); + // Will be an access control error. + vm.expectRevert(); + gmc.setFunctionPermits(functionPermits); + } + + + function test_RevertIfSetFunctionPermitsNonContract() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: deployer, + functionSelector: MockFunctions.succeed.selector, + permitted: true + }); + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.NonContractAddress.selector, deployer)); + gmc.setFunctionPermits(functionPermits); + } + + function test_RevertIfGrantRevokeSignerRoleWithInvalidRole() public { + vm.startPrank(user); + + vm.expectRevert(); + gmc.grantMulticallSignerRole(user); + + vm.expectRevert(); + gmc.revokeMulticallSignerRole(user); + + vm.stopPrank(); + } + + function test_HasBeenExecuted() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + gmc.execute(signer, ref, targets, data, deadline, signature); + + assertTrue(gmc.hasBeenExecuted(ref)); + + bytes32 invalidRef = keccak256(abi.encodePacked("invalid_ref")); + assertFalse(gmc.hasBeenExecuted(invalidRef)); + } + + function testIsFunctionPermitted() public { + assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.succeed.selector)); + assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.revertWithNoReason.selector)); + assertFalse(gmc.isFunctionPermitted(address(mock), MockFunctions.notPermitted.selector)); + } + + function testHashBytesArray() public { + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSignature("succeed()"); + data[1] = abi.encodeWithSignature("notSucceed()"); + assertEq(sigUtils.hashBytesArray(data), gmc.hashBytesArray(data)); + } + + function signTypedData( + uint256 _signerPk, + bytes32 _reference, + address[] memory _targets, + bytes[] memory _data, + uint256 _deadline + ) public view returns (bytes memory) { + bytes32 digest = sigUtils.hashTypedData(_reference, _targets, _data, _deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPk, digest); + return abi.encodePacked(r, s, v); + } + +} \ No newline at end of file diff --git a/test/multicall/GuardedMulticaller.test.ts b/test/multicall/GuardedMulticaller.test.ts deleted file mode 100644 index 1974e1248..000000000 --- a/test/multicall/GuardedMulticaller.test.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumberish } from "ethers"; -import moment from "moment"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { randomUUID } from "crypto"; -import { hexlify, keccak256 } from "ethers/lib/utils"; -import { GuardedMulticaller, MockFunctions } from "../../typechain-types"; - -describe("GuardedMulticaller", function () { - let deployerAccount: SignerWithAddress; - let signerAccount: SignerWithAddress; - let userAccount: SignerWithAddress; - - before(async function () { - [deployerAccount, signerAccount, userAccount] = await ethers.getSigners(); - }); - - const multicallerName = "Multicaller"; - const multicallerVersion = "v1"; - - let guardedMulticaller: GuardedMulticaller; - let deadline: number; - let ref: string; - let domain: { name: string; version: string; verifyingContract: string }; - - beforeEach(async function () { - const GuardedMulticallerFactory = await ethers.getContractFactory("GuardedMulticaller"); - guardedMulticaller = (await GuardedMulticallerFactory.deploy( - deployerAccount.address, - multicallerName, - multicallerVersion, - )) as GuardedMulticaller; - await guardedMulticaller.connect(deployerAccount).grantMulticallSignerRole(signerAccount.address); - deadline = moment.utc().add(30, "minute").unix(); - ref = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; - domain = { - name: multicallerName, - version: multicallerVersion, - verifyingContract: guardedMulticaller.address, - }; - }); - - describe("Mock Functions", function () { - let mock: MockFunctions; - - beforeEach(async function () { - const MockFunctionsFactory = await ethers.getContractFactory("MockFunctions"); - mock = (await MockFunctionsFactory.connect(deployerAccount).deploy()) as MockFunctions; - await guardedMulticaller.setFunctionPermits([ - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: true, - }, - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("revertWithNoReason()"), - permitted: true, - }, - ]); - }); - - it("Should successfully execute if valid", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ) - .to.emit(guardedMulticaller, "Multicalled") - .withArgs(signerAccount.address, ref, targets, data, deadline); - }); - - it("Should revert with custom error with empty return data", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("revertWithNoReason")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("FailedCall"); - }); - - it("Should revert if deadline has passed", async function () { - const expiredDeadline = moment.utc().subtract(30, "minute").unix(); - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, expiredDeadline, domain); - await expect( - guardedMulticaller - .connect(userAccount) - .execute(signerAccount.address, ref, targets, data, expiredDeadline, sig), - ).to.be.revertedWith("Expired"); - }); - - it("Should revert if reference is reused - anti-replay", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("ReusedReference"); - }); - - it("Should revert if ref is invalid", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - const invalidRef = `0x${"0".repeat(64)}`; - await expect( - guardedMulticaller - .connect(userAccount) - .execute(signerAccount.address, invalidRef, targets, data, deadline, sig), - ).to.be.revertedWith("InvalidReference"); - }); - - it("Should revert if signer does not have MULTICALLER role", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(userAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(userAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("UnauthorizedSigner"); - }); - - it("Should revert if signer and signature do not match", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(userAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("UnauthorizedSignature"); - }); - - it("Should revert if targets are empty", async function () { - const targets: string[] = []; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("EmptyAddressArray"); - }); - - it("Should revert if targets and data sizes do not match", async function () { - const targets = [mock.address, mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("AddressDataArrayLengthsMismatch"); - }); - - it("Should revert if function not permitted", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("nonPermitted")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("UnauthorizedFunction"); - }); - - it("Should revert if function is disallowed", async function () { - await guardedMulticaller.setFunctionPermits([ - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: false, - }, - ]); - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("UnauthorizedFunction"); - }); - - it("Should revert if signature is invalid", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const maliciousRef = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; - const sig = await signMulticallTypedData(signerAccount, maliciousRef, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ).to.be.revertedWith("UnauthorizedSignature"); - }); - - it("Should emit FunctionPermitted event when setting function permits", async function () { - await expect( - guardedMulticaller.connect(deployerAccount).setFunctionPermits([ - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: true, - }, - ]), - ) - .to.emit(guardedMulticaller, "FunctionPermitted") - .withArgs(mock.address, funcSignatureToFuncSelector("succeed()"), true); - await expect( - guardedMulticaller.connect(deployerAccount).setFunctionPermits([ - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: false, - }, - ]), - ) - .to.emit(guardedMulticaller, "FunctionPermitted") - .withArgs(mock.address, funcSignatureToFuncSelector("succeed()"), false); - }); - - it("Should revert if setting function permits with invalid data", async function () { - await expect(guardedMulticaller.connect(deployerAccount).setFunctionPermits([])).to.be.revertedWith( - "EmptyFunctionPermitArray", - ); - await expect( - guardedMulticaller.connect(userAccount).setFunctionPermits([ - { - target: mock.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: false, - }, - ]), - ).to.be.revertedWith(/AccessControl/); - await expect( - guardedMulticaller.setFunctionPermits([ - { - target: deployerAccount.address, - functionSelector: funcSignatureToFuncSelector("succeed()"), - permitted: true, - }, - ]), - ).to.be.revertedWith("NonContractAddress"); - }); - - it("Should revert if grant/revoke signer role with invalid role", async function () { - await expect( - guardedMulticaller.connect(userAccount).grantMulticallSignerRole(userAccount.address), - ).to.be.revertedWith(/AccessControl/); - - await expect( - guardedMulticaller.connect(userAccount).revokeMulticallSignerRole(userAccount.address), - ).to.be.revertedWith(/AccessControl/); - }); - - it("Should return hasBeenExecuted = true if the call has been executed", async function () { - const targets = [mock.address]; - const data = [mock.interface.encodeFunctionData("succeed")]; - const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); - await expect( - guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), - ) - .to.emit(guardedMulticaller, "Multicalled") - .withArgs(signerAccount.address, ref, targets, data, deadline); - - await expect(await guardedMulticaller.hasBeenExecuted(ref)).to.be.true; - }); - - it("Should return hasBeenExecuted = false for an unknown reference", async function () { - const invalidRef = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; - await expect(await guardedMulticaller.hasBeenExecuted(invalidRef)).to.be.false; - }); - }); -}); - -function funcSignatureToFuncSelector(funcSignature: string): string { - return keccak256(hexlify(ethers.utils.toUtf8Bytes(funcSignature))).substring(0, 10); -} - -async function signMulticallTypedData( - wallet: SignerWithAddress, - ref: string, - targets: string[], - data: string[], - deadline: BigNumberish, - domain: { name: string; version: string; verifyingContract: string }, -): Promise { - return await wallet._signTypedData( - { - name: domain.name, - version: domain.version, - chainId: await wallet.getChainId(), - verifyingContract: domain.verifyingContract, - }, - { - Multicall: [ - { - name: "ref", - type: "bytes32", - }, - { - name: "targets", - type: "address[]", - }, - { - name: "data", - type: "bytes[]", - }, - { - name: "deadline", - type: "uint256", - }, - ], - }, - { ref, targets, data, deadline }, - ); -} diff --git a/test/multicall/GuardedMulticaller2.t.sol b/test/multicall/GuardedMulticaller2.t.sol index 7f36672cd..9f2770b5a 100644 --- a/test/multicall/GuardedMulticaller2.t.sol +++ b/test/multicall/GuardedMulticaller2.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.19 <0.8.29; import "forge-std/Test.sol"; import {GuardedMulticaller2} from "../../contracts/multicall/GuardedMulticaller2.sol"; -import {MockFunctions} from "../../contracts/mocks/MockFunctions.sol"; +import {MockFunctions} from "./MockFunctions.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {SigUtils} from "./SigUtils.t.sol"; diff --git a/contracts/mocks/MockFunctions.sol b/test/multicall/MockFunctions.sol similarity index 85% rename from contracts/mocks/MockFunctions.sol rename to test/multicall/MockFunctions.sol index aa1a5da1d..17ceb0891 100644 --- a/contracts/mocks/MockFunctions.sol +++ b/test/multicall/MockFunctions.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: Unlicense -// This file is part of the test code for GuardedMulticaller +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 pragma solidity >=0.8.19 <0.8.29; contract MockFunctions { @@ -16,7 +16,7 @@ contract MockFunctions { } // solhint-disable-next-line no-empty-blocks - function nonPermitted() public pure { + function notPermitted() public pure { // This function is intentionally left empty to simulate a non-permitted action } @@ -28,4 +28,4 @@ contract MockFunctions { // solhint-disable-next-line custom-errors,reason-string revert RevertWithData(value); } -} +} \ No newline at end of file diff --git a/test/multicall/SigUtils.t.sol b/test/multicall/SigUtils.t.sol index 34cdcf320..4bb18a70f 100644 --- a/test/multicall/SigUtils.t.sol +++ b/test/multicall/SigUtils.t.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: MIT +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 pragma solidity >=0.8.19 <0.8.29; import {GuardedMulticaller2} from "../../contracts/multicall/GuardedMulticaller2.sol"; @@ -9,11 +10,17 @@ contract SigUtils { bytes32 internal constant CALL_TYPEHASH = keccak256("Call(address target,string functionSignature,bytes data)"); - bytes32 internal constant MULTICALL_TYPEHASH = + bytes32 internal constant MULTICALL_TYPEHASHV1 = + keccak256( + "Multicall(bytes32 ref,address[] targets,bytes[] data,uint256 deadline)" + ); + + bytes32 internal constant MULTICALL_TYPEHASHV2 = keccak256( "Multicall(bytes32 ref,Call[] calls,uint256 deadline)Call(address target,string functionSignature,bytes data)" ); + bytes32 private immutable cachedDomainSeparator; constructor(string memory _name, string memory _version, address _verifyingContract) { @@ -35,7 +42,31 @@ contract SigUtils { GuardedMulticaller2.Call[] calldata _calls, uint256 _deadline ) public view returns (bytes32) { - bytes32 digest = keccak256(abi.encode(MULTICALL_TYPEHASH, _reference, _hashCallArray(_calls), _deadline)); + bytes32 digest = keccak256(abi.encode(MULTICALL_TYPEHASHV2, _reference, _hashCallArray(_calls), _deadline)); return keccak256(abi.encodePacked("\x19\x01", cachedDomainSeparator, digest)); } + + + function hashTypedData( + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline + ) public view returns (bytes32) { + bytes32 digest = keccak256(abi.encode( + MULTICALL_TYPEHASHV1, + _reference, + keccak256(abi.encodePacked(_targets)), + hashBytesArray(_data), + _deadline)); + return keccak256(abi.encodePacked("\x19\x01", cachedDomainSeparator, digest)); + } + + 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)); + } } diff --git a/test/royalty-enforcement/MockMarketplace.sol b/test/royalty-enforcement/MockMarketplace.sol new file mode 100644 index 000000000..68985b61e --- /dev/null +++ b/test/royalty-enforcement/MockMarketplace.sol @@ -0,0 +1,82 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity >=0.8.19 <0.8.29; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +contract MockMarketplace { + error ZeroAddress(); + + IERC721 public immutable tokenAddress; + IERC2981 public immutable royaltyAddress; + + constructor(address _tokenAddress) { + tokenAddress = IERC721(_tokenAddress); + royaltyAddress = IERC2981(_tokenAddress); + } + + function executeTransfer(address recipient, uint256 _tokenId) public { + tokenAddress.transferFrom(msg.sender, recipient, _tokenId); + } + + /// @notice This code is only for testing purposes. Do not use similar + /// @notice constructions in production code as they are open to attack. + /// @dev For details see: https://github.com/crytic/slither/wiki/Detector-Documentation#arbitrary-from-in-transferfrom + function executeTransferFrom(address from, address to, uint256 _tokenId) public { + // slither-disable-next-line arbitrary-send-erc20 + tokenAddress.transferFrom(from, to, _tokenId); + } + + function executeApproveForAll(address operator, bool approved) public { + tokenAddress.setApprovalForAll(operator, approved); + } + + /// @notice This code is only for testing purposes. Do not use similar + /// @notice constructions in production code as they are open to attack. + /// @dev For details see: https://github.com/crytic/slither/wiki/Detector-Documentation#arbitrary-from-in-transferfrom + function executeTransferRoyalties(address from, address recipient, uint256 _tokenId, uint256 price) public payable { + if (from == address(0)) { + revert ZeroAddress(); + } + // solhint-disable-next-line custom-errors + require(msg.value == price, "insufficient msg.value"); + (address receiver, uint256 royaltyAmount) = royaltyAddress.royaltyInfo(_tokenId, price); + if (receiver == address(0)) { + revert ZeroAddress(); + } + uint256 sellerAmt = msg.value - royaltyAmount; + payable(receiver).transfer(royaltyAmount); + payable(from).transfer(sellerAmt); + // slither-disable-next-line arbitrary-send-erc20 + tokenAddress.transferFrom(from, recipient, _tokenId); + } +} + + +// function executeTransferRoyalties( +// address seller, +// address buyer, +// uint256 tokenId, +// uint256 price +// ) external payable { +// // Get royalty info +// (address recipient, uint256 royaltyAmount) = IERC2981(address(nft)).royaltyInfo(tokenId, price); + +// // Transfer NFT +// nft.transferFrom(seller, buyer, tokenId); + +// // Transfer royalty to recipient +// if (royaltyAmount > 0) { +// (bool success, ) = recipient.call{value: royaltyAmount}(""); +// require(success, "Royalty transfer failed"); +// } + +// // Transfer remaining amount to seller +// uint256 sellerAmount = price - royaltyAmount; +// if (sellerAmount > 0) { +// (bool success, ) = seller.call{value: sellerAmount}(""); +// require(success, "Seller transfer failed"); +// } +// } +// } diff --git a/test/royalty-enforcement/RoyaltyMarketplace.t.sol b/test/royalty-enforcement/RoyaltyMarketplace.t.sol new file mode 100644 index 000000000..ee25c059b --- /dev/null +++ b/test/royalty-enforcement/RoyaltyMarketplace.t.sol @@ -0,0 +1,116 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity >=0.8.19 <0.8.29; + +import "forge-std/Test.sol"; +import {ImmutableERC721MintByID} from "../../contracts/token/erc721/preset/ImmutableERC721MintByID.sol"; +import {MockMarketplace} from "./MockMarketplace.sol"; +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; +import {DeployOperatorAllowlist} from "../utils/DeployAllowlistProxy.sol"; + + +contract RoyaltyMarketplaceTest is Test { + ImmutableERC721MintByID public erc721; + OperatorAllowlistUpgradeable public operatorAllowlist; + MockMarketplace public mockMarketplace; + + address public owner; + address public minter; + address public registrar; + address public royaltyRecipient; + address public buyer; + address public seller; + + string public constant baseURI = "https://baseURI.com/"; + string public constant contractURI = "https://contractURI.com"; + string public constant name = "ERC721Preset"; + string public constant symbol = "EP"; + uint96 public constant royalty = 2000; // 20% + + function setUp() public { + // Set up accounts + owner = makeAddr("owner"); + minter = makeAddr("minter"); + registrar = makeAddr("registrar"); + royaltyRecipient = makeAddr("royaltyRecipient"); + buyer = makeAddr("buyer"); + seller = makeAddr("seller"); + + // Deploy operator Allowlist + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); + address proxyAddr = deployScript.run(owner, owner, registrar); + operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); + + // Deploy ERC721 contract + vm.prank(owner); + erc721 = new ImmutableERC721MintByID( + owner, + name, + symbol, + baseURI, + contractURI, + address(operatorAllowlist), + royaltyRecipient, + royalty + ); + + // Deploy mock marketplace + mockMarketplace = new MockMarketplace(address(erc721)); + + // Set up roles + vm.prank(owner); + erc721.grantMinterRole(minter); + } + + function test_AllowlistMarketplace() public { + address[] memory marketPlaces = new address[](1); + marketPlaces[0] = address(mockMarketplace); + + vm.prank(registrar); + operatorAllowlist.addAddressesToAllowlist(marketPlaces); + + assertTrue(operatorAllowlist.isAllowlisted(address(mockMarketplace))); + } + + function test_EnforceRoyalties() public { + // Add the market place to the operator allow list. + test_AllowlistMarketplace(); + + uint256 tokenId = 1; + + // Get royalty info + uint256 salePrice = 1 ether; + (, uint256 royaltyAmount) = erc721.royaltyInfo(tokenId, salePrice); + + // Mint NFT to seller + vm.prank(minter); + erc721.mint(seller, tokenId); + + // Approve marketplace + vm.prank(seller); + erc721.setApprovalForAll(address(mockMarketplace), true); + + // Get pre-trade balances + uint256 recipientBal = royaltyRecipient.balance; + uint256 sellerBal = seller.balance; + + // Execute trade + vm.deal(buyer, salePrice); + vm.prank(buyer); + mockMarketplace.executeTransferRoyalties{value: salePrice}( + seller, + buyer, + tokenId, + salePrice + ); + + // Check if buyer received NFT + assertEq(erc721.ownerOf(tokenId), buyer, "Buyer does not have NFT"); + + // Check if royalty recipient has increased balance + assertEq(royaltyRecipient.balance, recipientBal + royaltyAmount, "Royalty receiver balance not correct"); + + // Check if seller has increased balance + assertEq(seller.balance, sellerBal + (salePrice - royaltyAmount), "Seller balance not correct"); + } +} \ No newline at end of file diff --git a/test/royalty-enforcement/RoyaltyMarketplace.test.ts b/test/royalty-enforcement/RoyaltyMarketplace.test.ts deleted file mode 100644 index e63f48261..000000000 --- a/test/royalty-enforcement/RoyaltyMarketplace.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { - ImmutableERC721MintByID__factory, - ImmutableERC721MintByID, - OperatorAllowlist, - OperatorAllowlist__factory, - MockMarketplace__factory, - MockMarketplace, -} from "../../typechain-types"; - -describe("Marketplace Royalty Enforcement", function () { - this.timeout(300_000); // 5 min - - let erc721: ImmutableERC721MintByID; - let operatorAllowlist: OperatorAllowlist; - let mockMarketplace: MockMarketplace; - let owner: SignerWithAddress; - let minter: SignerWithAddress; - let registrar: SignerWithAddress; - let royaltyRecipient: SignerWithAddress; - let buyer: SignerWithAddress; - let seller: SignerWithAddress; - - const baseURI = "https://baseURI.com/"; - const contractURI = "https://contractURI.com"; - const name = "ERC721Preset"; - const symbol = "EP"; - const royalty = ethers.BigNumber.from("2000"); - - before(async function () { - // Retrieve accounts - [owner, minter, registrar, royaltyRecipient, buyer, seller] = await ethers.getSigners(); - // Deploy operator Allowlist - const operatorAllowlistFactory = (await ethers.getContractFactory( - "OperatorAllowlist" - )) as OperatorAllowlist__factory; - operatorAllowlist = await operatorAllowlistFactory.deploy(owner.address); - - // Deploy ERC721 contract - const erc721PresetFactory = (await ethers.getContractFactory( - "ImmutableERC721MintByID" - )) as ImmutableERC721MintByID__factory; - - erc721 = await erc721PresetFactory.deploy( - owner.address, - name, - symbol, - baseURI, - contractURI, - operatorAllowlist.address, - royaltyRecipient.address, - royalty - ); - - // Deploy mock marketplace - const mockMarketplaceFactory = (await ethers.getContractFactory("MockMarketplace")) as MockMarketplace__factory; - mockMarketplace = await mockMarketplaceFactory.deploy(erc721.address); - - // Set up roles - await erc721.connect(owner).grantMinterRole(minter.address); - await operatorAllowlist.connect(owner).grantRegistrarRole(registrar.address); - }); - - describe("Royalties", function () { - it("Should allow a marketplace contract to be Allowlisted", async function () { - await operatorAllowlist.connect(registrar).addAddressToAllowlist([mockMarketplace.address]); - expect(await operatorAllowlist.isAllowlisted(mockMarketplace.address)).to.be.equal(true); - }); - - it("Should enforce royalties on a marketplace trade", async function () { - // Get royalty info - const salePrice = ethers.utils.parseEther("1"); - const tokenInfo = await erc721.royaltyInfo(2, salePrice); - // Mint Nft to seller - await erc721.connect(minter).mint(seller.address, 1); - // Approve marketplace - await erc721.connect(seller).setApprovalForAll(mockMarketplace.address, true); - // Get pre-trade balances - const recipientBal = await ethers.provider.getBalance(royaltyRecipient.address); - const sellerBal = await ethers.provider.getBalance(seller.address); - // Execute trade - await mockMarketplace.connect(buyer).executeTransferRoyalties(seller.address, buyer.address, 1, salePrice, { - value: salePrice, - }); - // Check if buyer recieved NFT - expect(await erc721.ownerOf(1)).to.be.equal(buyer.address); - // Check if royalty recipient has increased balance newBal = oldBal + royaltyAmount - expect(await ethers.provider.getBalance(royaltyRecipient.address)).to.equal(recipientBal.add(tokenInfo[1])); - // Check if seller has increased balance newBal = oldBal + (salePrice - royaltyAmount) - expect(await ethers.provider.getBalance(seller.address)).to.equal(sellerBal.add(salePrice.sub(tokenInfo[1]))); - }); - }); -}); diff --git a/test/token/x/Asset.t.sol b/test/token/x/Asset.t.sol new file mode 100644 index 000000000..b671983a1 --- /dev/null +++ b/test/token/x/Asset.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {Asset} from "../../../contracts/token/erc721/x/Asset.sol"; + +contract AssetTest is Test { + Asset public asset; + address public owner; + address public imx; + + function setUp() public { + owner = makeAddr("owner"); + imx = makeAddr("imx"); + vm.startPrank(owner); + asset = new Asset(owner, "Gods Unchained", "GU", imx); + vm.stopPrank(); + } + + function testMintWithValidBlueprint() public { + uint256 tokenID = 123; + string memory tokenIDStr = "123"; + string memory blueprint = "1000"; + bytes memory blob = abi.encodePacked("{", tokenIDStr, "}:{", blueprint, "}"); + + vm.startPrank(imx); + asset.mintFor(owner, 1, blob); + vm.stopPrank(); + + assertEq(asset.ownerOf(tokenID), owner, "Incorrect owner"); + assertEq(asset.blueprints(tokenID).length, bytes(blueprint).length, "Incorrect blueprint length"); + for (uint256 i = 0; i < bytes(blueprint).length; i++) { + assertEq(asset.blueprints(tokenID)[i], bytes(blueprint)[i], "Incorrect blueprint"); + } + } + + function testMintWithEmptyBlueprint() public { + uint256 tokenID = 123; + string memory tokenIDStr = "123"; + string memory blueprint = ""; + bytes memory blob = abi.encodePacked("{", tokenIDStr, "}:{", blueprint, "}"); + + vm.startPrank(imx); + asset.mintFor(owner, 1, blob); + vm.stopPrank(); + + assertEq(asset.ownerOf(tokenID), owner, "Incorrect owner"); + assertEq(asset.blueprints(tokenID).length, 0, "Incorrect blueprint length"); + } + + function testMintWithInvalidBlueprint() public { + bytes memory separator = ":"; + bytes memory blob = separator; + + vm.startPrank(imx); + vm.expectRevert(); + asset.mintFor(owner, 1, blob); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/token/x/Asset.test.ts b/test/token/x/Asset.test.ts deleted file mode 100644 index e3a28a2aa..000000000 --- a/test/token/x/Asset.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -describe("Asset", function () { - it("Should be able to mint successfully with a valid blueprint", async function () { - const [owner] = await ethers.getSigners(); - - const Asset = await ethers.getContractFactory("Asset"); - - const o = owner.address; - const name = "Gods Unchained"; - const symbol = "GU"; - const imx = owner.address; - const mintable = await Asset.deploy(o, name, symbol, imx); - - const tokenID = "123"; - const blueprint = "1000"; - const blob = toHex(`{${tokenID}}:{${blueprint}}`); - - await mintable.mintFor(owner.address, 1, blob); - - const oo = await mintable.ownerOf(tokenID); - - expect(oo).to.equal(owner.address); - - const bp = await mintable.blueprints(tokenID); - - expect(fromHex(bp)).to.equal(blueprint); - }); - - it("Should be able to mint successfully with an empty blueprint", async function () { - const [owner] = await ethers.getSigners(); - - const Asset = await ethers.getContractFactory("Asset"); - - const o = owner.address; - const name = "Gods Unchained"; - const symbol = "GU"; - const imx = owner.address; - const mintable = await Asset.deploy(o, name, symbol, imx); - - const tokenID = "123"; - const blueprint = ""; - const blob = toHex(`{${tokenID}}:{${blueprint}}`); - - await mintable.mintFor(owner.address, 1, blob); - - const bp = await mintable.blueprints(tokenID); - - expect(fromHex(bp)).to.equal(blueprint); - }); - - it("Should not be able to mint successfully with an invalid blueprint", async function () { - const [owner] = await ethers.getSigners(); - - const Asset = await ethers.getContractFactory("Asset"); - - const o = owner.address; - const name = "Gods Unchained"; - const symbol = "GU"; - const imx = owner.address; - const mintable = await Asset.deploy(o, name, symbol, imx); - - const blob = toHex(`:`); - await expect(mintable.mintFor(owner.address, 1, blob)).to.be.reverted; - }); -}); - -function toHex(str: string) { - let result = ""; - for (let i = 0; i < str.length; i++) { - result += str.charCodeAt(i).toString(16); - } - return "0x" + result; -} - -function fromHex(str1: string) { - const hex = str1.toString().substr(2); - let str = ""; - for (let n = 0; n < hex.length; n += 2) { - str += String.fromCharCode(parseInt(hex.substr(n, 2), 16)); - } - return str; -} diff --git a/test/trading/seaport/ImmutableSeaportBase.t.sol b/test/trading/seaport/ImmutableSeaportBase.t.sol new file mode 100644 index 000000000..d093c92ef --- /dev/null +++ b/test/trading/seaport/ImmutableSeaportBase.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ImmutableSeaport} from "../../../contracts/trading/seaport/ImmutableSeaport.sol"; +import {ImmutableSignedZone} from "../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol"; +import {SIP7EventsAndErrors} from "../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol"; + +import {ConduitController} from "seaport-core/src/conduit/ConduitController.sol"; +import {Conduit} from "seaport-core/src/conduit/Conduit.sol"; +import {Consideration} from "seaport-core/src/lib/Consideration.sol"; +import {OrderParameters, OrderComponents, Order, AdvancedOrder, FulfillmentComponent, FulfillmentComponent, CriteriaResolver} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {ReceivedItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + + + + + + +abstract contract ImmutableSeaportBaseTest is Test { + event AllowedZoneSet(address zoneAddress, bool allowed); + + ImmutableSeaport public immutableSeaport; + ImmutableSignedZone public immutableSignedZone; + ConduitController public conduitController; + Conduit public conduit; + bytes32 public conduitKey; + address public conduitAddress; + address public owner; + address public immutableSigner; + uint256 public immutableSignerPkey; + address public buyer; + address public seller; + uint256 public buyerPkey; + uint256 public sellerPkey; + + function setUp() public virtual { + // Set up chain ID + //uint256 chainId = block.chainid; + + // Create test addresses + owner = makeAddr("owner"); + (immutableSigner, immutableSignerPkey) = makeAddrAndKey("immutableSigner"); + (buyer, buyerPkey) = makeAddrAndKey("buyer"); + (seller, sellerPkey) = makeAddrAndKey("seller"); + + // Deploy contracts + immutableSignedZone = new ImmutableSignedZone("ImmutableSignedZone", "", "", owner); + vm.prank(owner); + immutableSignedZone.addSigner(immutableSigner); + + // The conduit key used to deploy the conduit. Note that the first twenty bytes of the conduit key must match the caller of this contract. + conduitKey = bytes32(uint256(uint160(owner)) << (256-160)); + conduitController = new ConduitController(); + vm.prank(owner); + conduitController.createConduit(conduitKey, owner); + bool exists; + (conduitAddress, exists) = conduitController.getConduit(conduitKey); + assertTrue(exists, "Condiut contract does not exist"); + conduit = Conduit(conduitAddress); + + immutableSeaport = new ImmutableSeaport(address(conduitController), owner); + + vm.prank(owner); + immutableSeaport.setAllowedZone(address(immutableSignedZone), true); + vm.prank(owner); + conduitController.updateChannel(conduitAddress, address(immutableSeaport), true); + } +} \ No newline at end of file diff --git a/test/trading/seaport/ImmutableSeaportConfig.t.sol b/test/trading/seaport/ImmutableSeaportConfig.t.sol new file mode 100644 index 000000000..1342c27ba --- /dev/null +++ b/test/trading/seaport/ImmutableSeaportConfig.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ImmutableSeaportBaseTest} from "./ImmutableSeaportBase.t.sol"; + +contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { + + function testEmitsAllowedZoneSetEvent() public { + address zone = makeAddr("zone"); + bool allowed = true; + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit AllowedZoneSet(zone, allowed); + immutableSeaport.setAllowedZone(zone, allowed); + } +} \ No newline at end of file diff --git a/test/trading/seaport/ImmutableSeaportOperational.t.sol b/test/trading/seaport/ImmutableSeaportOperational.t.sol new file mode 100644 index 000000000..f0a4b8f49 --- /dev/null +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ImmutableSeaportBaseTest} from "./ImmutableSeaportBase.t.sol"; + + +import "forge-std/Test.sol"; +import {ImmutableSeaportTestHelper} from "./ImmutableSeaportTestHelper.t.sol"; +import {ImmutableSeaport} from "../../../contracts/trading/seaport/ImmutableSeaport.sol"; +import {ImmutableSignedZone} from "../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol"; +import {SIP7EventsAndErrors} from "../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol"; + +import {ConduitController} from "seaport-core/src/conduit/ConduitController.sol"; +import {Conduit} from "seaport-core/src/conduit/Conduit.sol"; +import {Consideration} from "seaport-core/src/lib/Consideration.sol"; +import {OrderParameters, OrderComponents, Order, AdvancedOrder, FulfillmentComponent, FulfillmentComponent, CriteriaResolver} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {ConsiderationItem, OfferItem, ReceivedItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + + + + +contract TestERC721 is ERC721("Test721", "TST721") { + function mint(address to, uint256 tokenId) public returns (bool) { + _mint(to, tokenId); + return true; + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } +} + +// A wallet rather than an EOA needs to be used for the seller because code in forge detects +// the seller as a contract when created it is created with makeAddr. +contract SellerWallet { + bytes4 private constant SELECTOR_ERC1271_BYTES_BYTES = 0x20c13b0b; + bytes4 private constant SELECTOR_ERC1271_BYTES32_BYTES = 0x1626ba7e; + + function isValidSignature(bytes calldata /*_data */, bytes calldata /*_signatures*/) external pure returns (bytes4) { +// if (_signatureValidationInternal(_subDigest(keccak256(_data)), _signatures)) { + return SELECTOR_ERC1271_BYTES_BYTES; + // } + // return 0; + } + + function isValidSignature(bytes32 /*_hash*/, bytes calldata /*_signatures*/) external pure returns (bytes4) { + // if (_signatureValidationInternal(_subDigest(_hash), _signatures)) { + return SELECTOR_ERC1271_BYTES32_BYTES; + // } + // return 0; + } + + function setApprovalForAll(address _erc721, address _seaport) external { + ERC721(_erc721).setApprovalForAll(_seaport, true); + } + + receive() external payable { } +} + + + + +contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableSeaportTestHelper { + SellerWallet public sellerWallet; + TestERC721 public erc721; + uint256 public nftId; + + function setUp() public override { + super.setUp(); + _setFulfillerAndZone(buyer, address(immutableSignedZone)); + sellerWallet = new SellerWallet(); + nftId = 1; + vm.deal(buyer, 10 ether); + } + + + function testFulfillFullRestrictedOrder() public { + _checkFulfill(OrderType.FULL_RESTRICTED); + } + + function testFulfillPartialRestrictedOrder() public { + _checkFulfill(OrderType.PARTIAL_RESTRICTED); + } + + + function testRejectUnsupportedZones() public { + // Create order with random zone + address randomZone = makeAddr("randomZone"); + AdvancedOrder memory order = _prepareCheckFulfill(randomZone); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.InvalidZone.selector, randomZone)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectFullOpenOrder() public { + AdvancedOrder memory order = _prepareCheckFulfill(OrderType.FULL_OPEN); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectDisabledZone() public { + AdvancedOrder memory order = _prepareCheckFulfill(); + + vm.prank(owner); + immutableSeaport.setAllowedZone(address(immutableSignedZone), false); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.InvalidZone.selector, address(immutableSignedZone))); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectWrongSigner() public { + uint256 wrongSigner = 1; + AdvancedOrder memory order = _prepareCheckFulfill(wrongSigner); + + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // information going in is wrong, then the wrong signer will be derived. + address derivedBadSigner = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectInvalidExtraData() public { + AdvancedOrder memory order = _prepareCheckFulfillWithBadExtraData(); + + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // information going in is wrong, then the wrong signer will be derived. + address derivedBadSigner = 0xcE810B9B83082C93574784f403727369c3FE6955; + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + + function _checkFulfill(OrderType _orderType) internal { + AdvancedOrder memory order = _prepareCheckFulfill(_orderType); + + // Record balances before + uint256 sellerBalanceBefore = address(sellerWallet).balance; + uint256 buyerBalanceBefore = address(buyer).balance; + + // Fulfill order + vm.prank(buyer); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + + // Verify results + assertEq(erc721.ownerOf(nftId), buyer, "Owner of NFT not buyer"); + assertEq(address(sellerWallet).balance, sellerBalanceBefore + 10 ether, "Seller incorrect final balance"); + assertEq(address(buyer).balance, buyerBalanceBefore - 10 ether, "Buyer incorrect final balance"); + } + + function _prepareCheckFulfill() internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), immutableSignerPkey, false); + } + + function _prepareCheckFulfill(OrderType _orderType) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(_orderType, address(immutableSignedZone), immutableSignerPkey, false); + } + + + function _prepareCheckFulfill(address _zone) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, _zone, immutableSignerPkey, false); + } + + function _prepareCheckFulfill(uint256 _signer) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), _signer, false); + } + + function _prepareCheckFulfillWithBadExtraData() internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), immutableSignerPkey, true); + } + + + function _prepareCheckFulfill(OrderType _orderType, address _zone, uint256 _signer, bool _useBaseExtraData) internal returns (AdvancedOrder memory) { + // Deploy test ERC721 + erc721 = new TestERC721(); + erc721.mint(address(sellerWallet), nftId); + sellerWallet.setApprovalForAll(address(erc721), conduitAddress); + uint64 expiration = uint64(block.timestamp + 90); + + // Create order + OrderParameters memory orderParams = OrderParameters({ + offerer: address(sellerWallet), + zone: _zone, + offer: _createOfferItems(address(erc721), nftId), + consideration: _createConsiderationItems(address(sellerWallet), 10 ether), + orderType: _orderType, + startTime: 0, + endTime: expiration, + zoneHash: bytes32(0), + salt: 0, + conduitKey: conduitKey, + totalOriginalConsiderationItems: 1 + }); + + OrderComponents memory orderComponents = OrderComponents({ + offerer: orderParams.offerer, + zone: orderParams.zone, + offer: orderParams.offer, + consideration: orderParams.consideration, + orderType: orderParams.orderType, + startTime: orderParams.startTime, + endTime: orderParams.endTime, + zoneHash: orderParams.zoneHash, + salt: orderParams.salt, + conduitKey: orderParams.conduitKey, + counter: 0 + }); + + bytes32 orderHash = immutableSeaport.getOrderHash(orderComponents); + bytes memory extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); + if (_useBaseExtraData) { + orderParams.consideration[0].recipient = payable(buyer); + extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); + } + bytes memory signature = _signOrder(sellerPkey, orderHash); + + AdvancedOrder memory order = AdvancedOrder(orderParams, 1, 1, signature, extraData); + return order; + } +} \ No newline at end of file diff --git a/test/trading/seaport/ImmutableSeaportTestHelper.t.sol b/test/trading/seaport/ImmutableSeaportTestHelper.t.sol new file mode 100644 index 000000000..80766992a --- /dev/null +++ b/test/trading/seaport/ImmutableSeaportTestHelper.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {ZoneParameters, ConsiderationItem, OfferItem, ReceivedItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + + +abstract contract ImmutableSeaportTestHelper is Test { + bytes internal constant CONSIDERATION_BYTES = + abi.encodePacked("Consideration(", "ReceivedItem[] consideration", ")"); + + bytes internal constant RECEIVED_ITEM_BYTES = + abi.encodePacked( + "ReceivedItem(", + "uint8 itemType,", + "address token,", + "uint256 identifier,", + "uint256 amount,", + "address recipient", + ")" + ); + + bytes32 internal constant RECEIVED_ITEM_TYPEHASH = keccak256(RECEIVED_ITEM_BYTES); + + bytes32 internal constant CONSIDERATION_TYPEHASH = + keccak256(abi.encodePacked(CONSIDERATION_BYTES, RECEIVED_ITEM_BYTES)); + + string public constant ZONE_NAME = "ImmutableSignedZone"; + string public constant VERSION = "1.0"; + + address private theFulfiller; + + address private theZone; + + function _setFulfillerAndZone(address _fulfiller, address _zone) internal { + theFulfiller = _fulfiller; + theZone = _zone; + } + + + // Helper functions + function _createZoneParameters(bytes memory _extraData) internal returns (ZoneParameters memory) { + bytes32 orderHash = keccak256("0x1234"); + return _createZoneParameters(_extraData, orderHash, _createMockConsideration(10)); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash) internal returns (ZoneParameters memory) { + return _createZoneParameters(_extraData, _orderHash, _createMockConsideration(10)); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash, ReceivedItem[] memory _consideration) internal view returns (ZoneParameters memory) { + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = _orderHash; + return _createZoneParameters(_extraData, _orderHash, orderHashes, _consideration); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash, bytes32[] memory _orderHashes, ReceivedItem[] memory _consideration) internal view returns (ZoneParameters memory) { + return ZoneParameters({ + orderHash: _orderHash, + fulfiller: theFulfiller, + offerer: address(0), + offer: new SpentItem[](0), + consideration: _consideration, + extraData: _extraData, + orderHashes: _orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + } + + function _createMockConsideration(uint256 count) internal returns (ReceivedItem[] memory) { + ReceivedItem[] memory consideration = new ReceivedItem[](count); + for (uint256 i = 0; i < count; i++) { + address payable recipient = payable(makeAddr(string(abi.encodePacked("recipient", vm.toString(i))))); + address payable token = payable(makeAddr(string(abi.encodePacked("token", vm.toString(i))))); + consideration[i] = ReceivedItem({ + itemType: ItemType.NATIVE, + token: token, + identifier: 123, + amount: 12, + recipient: recipient + }); + } + return consideration; + } + + function _convertConsiderationToReceivedItem(ConsiderationItem[] memory _items) internal pure returns (ReceivedItem[] memory) { + ReceivedItem[] memory consideration = new ReceivedItem[](_items.length); + for (uint256 i = 0; i < _items.length; i++) { + consideration[i] = ReceivedItem({ + itemType: _items[i].itemType, + token: _items[i].token, + identifier: _items[i].identifierOrCriteria, + amount: _items[i].startAmount, + recipient: _items[i].recipient + }); + } + return consideration; + } + + function _createConsiderationItems(address recipient, uint256 amount) internal pure returns (ConsiderationItem[] memory) { + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.NATIVE, + token: address(0), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount, + recipient: payable(recipient) + }); + return consideration; + } + + function _deriveConsiderationHash(ReceivedItem[] calldata consideration) external pure returns (bytes32) { + uint256 numberOfItems = consideration.length; + bytes32[] memory considerationHashes = new bytes32[](numberOfItems); + for (uint256 i; i < numberOfItems; i++) { + considerationHashes[i] = keccak256( + abi.encode( + RECEIVED_ITEM_TYPEHASH, + consideration[i].itemType, + consideration[i].token, + consideration[i].identifier, + consideration[i].amount, + consideration[i].recipient + ) + ); + } + return keccak256(abi.encode(CONSIDERATION_TYPEHASH, keccak256(abi.encodePacked(considerationHashes)))); + } + + + function _signOrder(uint256 signerPkey, bytes32 orderHash) internal view returns (bytes memory) { + return _signOrder(signerPkey, orderHash, 0, ""); + } + + function _signOrder( + uint256 _signerPkey, + bytes32 orderHash, + uint64 expiration, + bytes memory context + ) internal view returns (bytes memory) { + uint256 chainId = block.chainid; + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(ZONE_NAME)), + keccak256(bytes(VERSION)), + chainId, + theZone + ) + ); + //console.logBytes32(domainSeparator); + + bytes32 structHash = keccak256( + abi.encode( + keccak256("SignedOrder(address fulfiller,uint64 expiration,bytes32 orderHash,bytes context)"), + theFulfiller, + expiration, + orderHash, + keccak256(context) + ) + ); + //console.logBytes32(structHash); + + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + //console.logBytes32(digest); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPkey, digest); + return abi.encodePacked(r, s, v); + } + + function _convertSignatureToEIP2098(bytes calldata signature) external pure returns (bytes memory) { + if (signature.length == 64) { + return signature; + } + if (signature.length != 65) { + revert("Invalid signature length"); + } + return abi.encodePacked(signature[0:64]); + } + + // Helper functions + function _createOfferItems(address token, uint256 tokenId) internal pure returns (OfferItem[] memory) { + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721, + token: token, + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + return offer; + } + + + function _generateSip7Signature(bytes32 orderHash, address fulfiller, uint256 signerPkey, uint64 _expiration, ConsiderationItem[] memory _consideration) internal view returns (bytes memory) { + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + ReceivedItem[] memory consideration = _convertConsiderationToReceivedItem(_consideration); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, _expiration, context); + return abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + _expiration, + this._convertSignatureToEIP2098(signature), + context + ); + } + + function _convertToBytesWithoutArrayLength(bytes32[] memory _orders) internal view returns (bytes memory) { + bytes memory data = abi.encodePacked(_orders); + return this._stripArrayLength(data); + } + function _stripArrayLength(bytes calldata _data) external pure returns (bytes memory) { + return _data[32:_data.length]; + } +} \ No newline at end of file diff --git a/test/trading/seaport/immutableseaport.test.ts b/test/trading/seaport/immutableseaport.test.ts deleted file mode 100644 index ec6ac3b04..000000000 --- a/test/trading/seaport/immutableseaport.test.ts +++ /dev/null @@ -1,438 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { ethers, network } from "hardhat"; -import { randomBytes } from "crypto"; - -import type { ImmutableSeaport, ImmutableSignedZone, TestERC721 } from "../../../typechain-types"; -import { constants } from "ethers"; -import type { Wallet, BigNumber, BigNumberish } from "ethers"; -import { deployImmutableContracts } from "./utils/deploy-immutable-contracts"; -import { faucet } from "./utils/faucet"; -import { buildResolver, getItemETH, toBN, toKey } from "./utils/encoding"; -import { deployERC721, getTestItem721, getTestItem721WithCriteria, mintAndApprove721 } from "./utils/erc721"; -import { createOrder, generateSip7Signature } from "./utils/order"; -import { expect } from "chai"; -import { merkleTree } from "./utils/criteria"; - -const { parseEther } = ethers.utils; - -describe(`ImmutableSeaport and ImmutableZone (Seaport v1.5)`, function () { - const { provider } = ethers; - const owner = new ethers.Wallet(randomBytes(32), provider); - const immutableSigner = new ethers.Wallet(randomBytes(32), provider); - - let immutableSignedZone: ImmutableSignedZone; - let immutableSeaport: ImmutableSeaport; - let conduitKey: string; - let conduitAddress: string; - - function getEthBalance(userAddress: string): Promise { - return provider.getBalance(userAddress); - } - - async function userIsOwnerOfNft(erc721: TestERC721, tokenId: BigNumberish, userAddress: string): Promise { - const ownerOf = await erc721.ownerOf(tokenId); - return ownerOf === userAddress; - } - - after(async () => { - await network.provider.request({ - method: "hardhat_reset", - }); - }); - - before(async () => { - await faucet(owner.address, provider); - const immutableContracts = await deployImmutableContracts(immutableSigner.address); - immutableSeaport = immutableContracts.immutableSeaport; - immutableSignedZone = immutableContracts.immutableSignedZone; - conduitKey = immutableContracts.conduitKey; - conduitAddress = immutableContracts.conduitAddress; - }); - - let buyer: Wallet; - let seller: Wallet; - - beforeEach(async () => { - buyer = new ethers.Wallet(randomBytes(32), provider); - seller = new ethers.Wallet(randomBytes(32), provider); - await faucet(buyer.address, provider); - await faucet(seller.address, provider); - }); - - describe("Events", () => { - it("Emits AllowedZoneSet event", async () => { - const zone = new ethers.Wallet(randomBytes(32)).address; - const allowed = true; - expect( - await immutableSeaport - .connect((await ethers.getSigners())[0]) // use default deployer (admin) - .setAllowedZone(zone, allowed) - ) - .to.emit(immutableSeaport, "AllowedZoneSet") - .withArgs(zone, allowed); - }); - }); - - describe("Order fulfillment", () => { - it("ImmutableSeaport can fulfill an Immutable-signed FULL_RESTRICTED advanced order", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, conduitAddress); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 2, // FULL_RESTRICTED - undefined, - undefined, - undefined, - conduitKey - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - const sellerBalanceBefore = await getEthBalance(seller.address); - const buyerBalanceBefore = await getEthBalance(seller.address); - - const tx = await immutableSeaport.connect(buyer).fulfillAdvancedOrder(order, [], conduitKey, buyer.address, { - value, - }); - - await tx.wait(); - - expect(await userIsOwnerOfNft(erc721, nftId, buyer.address)).to.be.true; - expect(await userIsOwnerOfNft(erc721, nftId, seller.address)).to.be.false; - expect(await getEthBalance(seller.address)).to.equal(sellerBalanceBefore.add(parseEther("10"))); - - const currentBalance = await getEthBalance(buyer.address); - const expectedBalance = buyerBalanceBefore.sub(parseEther("10")); - - // Balance is less than 10 because of gas fees - // Chai doesn't seem to like ethers.BigNumber comparisons - expect(currentBalance.lt(expectedBalance)).to.be.true; - }); - - it("ImmutableSeaport can fulfill an Immutable-signed PARTIAL_RESTRICTED advanced order", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 3 // PARTIAL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - const sellerBalanceBefore = await getEthBalance(seller.address); - const buyerBalanceBefore = await getEthBalance(seller.address); - - const tx = await immutableSeaport.connect(buyer).fulfillAdvancedOrder(order, [], toKey(0), buyer.address, { - value, - }); - - await tx.wait(); - - expect(await userIsOwnerOfNft(erc721, nftId, buyer.address)).to.be.true; - expect(await userIsOwnerOfNft(erc721, nftId, seller.address)).to.be.false; - expect(await getEthBalance(seller.address)).to.equal(sellerBalanceBefore.add(parseEther("10"))); - // Balance is less than 10 because of gas fees - expect((await getEthBalance(buyer.address)).lt(buyerBalanceBefore.sub(parseEther("10")))).to.be.true; - }); - - it("ImmutableSeaport rejects unsupported zones", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - // Random address for zone - new ethers.Wallet(randomBytes(32)).address, - [offer], - consideration, - 2 // FULL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - await expect( - immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(0), ethers.constants.AddressZero, { - value, - }) - .then((tx) => tx.wait()) - ).to.be.revertedWith("InvalidZone"); - }); - - it("ImmutableSeaport rejects an Immutable-signed FULL_OPEN advanced order", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 0 // FULL_OPEN - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - await expect( - immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(0), ethers.constants.AddressZero, { - value, - }) - .then((tx) => tx.wait()) - ).to.be.revertedWith("OrderNotRestricted"); - }); - - it("ImmutableSeaport can fulfill an Immutable-signed FULL_RESTRICTED advanced order with criteria", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - - const { root, proofs } = merkleTree([nftId]); - - const offer = [getTestItem721WithCriteria(erc721.address, root, toBN(1), toBN(1))]; - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const criteriaResolvers = [buildResolver(0, 0, 0, nftId, proofs[nftId.toString()])]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - offer, - consideration, - 2 // FULL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - const sellerBalanceBefore = await getEthBalance(seller.address); - const buyerBalanceBefore = await getEthBalance(seller.address); - - const tx = await immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(0), buyer.address, { - value, - }); - - await tx.wait(); - - expect(await userIsOwnerOfNft(erc721, nftId, buyer.address)).to.be.true; - expect(await userIsOwnerOfNft(erc721, nftId, seller.address)).to.be.false; - expect(await getEthBalance(seller.address)).to.equal(sellerBalanceBefore.add(parseEther("10"))); - // Balance is less than 10 because of gas fees - expect((await getEthBalance(buyer.address)).lt(buyerBalanceBefore.sub(parseEther("10")))).to.be.true; - }); - - it("ImmutableSeaport can fulfill an Immutable-signed PARTIAL_RESTRICTED advanced order", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 3 // PARTIAL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - const sellerBalanceBefore = await getEthBalance(seller.address); - const buyerBalanceBefore = await getEthBalance(seller.address); - - const tx = await immutableSeaport.connect(buyer).fulfillAdvancedOrder(order, [], toKey(0), buyer.address, { - value, - }); - - await tx.wait(); - - expect(await userIsOwnerOfNft(erc721, nftId, buyer.address)).to.be.true; - expect(await userIsOwnerOfNft(erc721, nftId, seller.address)).to.be.false; - expect(await getEthBalance(seller.address)).to.equal(sellerBalanceBefore.add(parseEther("10"))); - // Balance is less than 10 because of gas fees - expect((await getEthBalance(buyer.address)).lt(buyerBalanceBefore.sub(parseEther("10")))).to.be.true; - }); - - it("Orders submitted against a zone that has been disabled are rejected", async () => { - const contracts = await deployImmutableContracts(immutableSigner.address); - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, contracts.immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - - // Disable the zone - await contracts.immutableSeaport.setAllowedZone(contracts.immutableSignedZone.address, false); - - const { order, orderHash, value } = await createOrder( - contracts.immutableSeaport, - seller, - contracts.immutableSignedZone, - [offer], - consideration, - 3 // PARTIAL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - await expect( - immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(0), ethers.constants.AddressZero, { - value, - }) - .then((tx) => tx.wait()) - ).to.be.revertedWith("InvalidZone"); - }); - - it("Orders with extraData signed by the wrong signer are rejected", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, orderHash, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 3 // PARTIAL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - orderHash, - buyer.address, - immutableSignedZone.address, - // Random signer - new ethers.Wallet(randomBytes(32), provider) - ); - - order.extraData = extraData; - - await expect( - immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(0), ethers.constants.AddressZero, { - value, - }) - .then((tx) => tx.wait()) - ).to.be.revertedWith("SignerNotActive"); - }); - - it("Orders with invalid extraData are rejected", async () => { - const erc721 = await deployERC721(); - const nftId = await mintAndApprove721(erc721, seller, immutableSeaport.address); - const offer = await getTestItem721(erc721.address, nftId); - const consideration = [getItemETH(parseEther("10"), parseEther("10"), seller.address)]; - const { order, value } = await createOrder( - immutableSeaport, - seller, - immutableSignedZone, - [offer], - consideration, - 3 // PARTIAL_RESTRICTED - ); - - const extraData = await generateSip7Signature( - consideration, - // Bad order hash - constants.HashZero, - buyer.address, - immutableSignedZone.address, - immutableSigner - ); - - // sign the orderHash with immutableSigner - order.extraData = extraData; - - await expect( - immutableSeaport - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(0), ethers.constants.AddressZero, { - value, - }) - .then((tx) => tx.wait()) - ).to.be.revertedWith("SubstandardViolation"); - }); - }); -}); diff --git a/test/trading/seaport/utils/criteria.ts b/test/trading/seaport/utils/criteria.ts deleted file mode 100644 index 63fd3c294..000000000 --- a/test/trading/seaport/utils/criteria.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ethers } from "ethers"; - -const { keccak256 } = ethers.utils; - -type BufferElementPositionIndex = { [key: string]: number }; - -export const merkleTree = (tokenIds: ethers.BigNumber[]) => { - const elements = tokenIds - .map((tokenId) => Buffer.from(tokenId.toHexString().slice(2).padStart(64, "0"), "hex")) - .sort(Buffer.compare) - .filter((el, idx, arr) => { - return idx === 0 || !arr[idx - 1].equals(el); - }); - - const bufferElementPositionIndex = elements.reduce((memo: BufferElementPositionIndex, el, index) => { - memo["0x" + el.toString("hex")] = index; - return memo; - }, {}); - - // Create layers - const layers = getLayers(elements); - - const root = "0x" + layers[layers.length - 1][0].toString("hex"); - - const proofs = Object.fromEntries( - elements.map((el) => [ethers.BigNumber.from(el).toString(), getHexProof(el, bufferElementPositionIndex, layers)]) - ); - - const maxProofLength = Math.max(...Object.values(proofs).map((i) => i.length)); - - return { - root, - proofs, - maxProofLength, - }; -}; - -const getLayers = (elements: Buffer[]) => { - if (elements.length === 0) { - throw new Error("empty tree"); - } - - const layers = []; - layers.push(elements.map((el) => Buffer.from(keccak256(el).slice(2), "hex"))); - - // Get next layer until we reach the root - while (layers[layers.length - 1].length > 1) { - layers.push(getNextLayer(layers[layers.length - 1])); - } - - return layers; -}; - -const getNextLayer = (elements: Buffer[]) => { - return elements.reduce((layer: Buffer[], el, idx, arr) => { - if (idx % 2 === 0) { - // Hash the current element with its pair element - layer.push(combinedHash(el, arr[idx + 1])); - } - - return layer; - }, []); -}; - -const combinedHash = (first: Buffer, second: Buffer) => { - if (!first) { - return second; - } - if (!second) { - return first; - } - - return Buffer.from(keccak256(Buffer.concat([first, second].sort(Buffer.compare))).slice(2), "hex"); -}; - -const getHexProof = (el: Buffer, bufferElementPositionIndex: BufferElementPositionIndex, layers: Buffer[][]) => { - let idx = bufferElementPositionIndex["0x" + el.toString("hex")]; - - if (typeof idx !== "number") { - throw new Error("Element does not exist in Merkle tree"); - } - - const proofBuffer = layers.reduce((proof: Buffer[], layer) => { - const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; - const pairElement = pairIdx < layer.length ? layer[pairIdx] : null; - - if (pairElement) { - proof.push(pairElement); - } - - idx = Math.floor(idx / 2); - - return proof; - }, []); - - return proofBuffer.map((el) => "0x" + el.toString("hex")); -}; diff --git a/test/trading/seaport/utils/deploy-immutable-contracts.ts b/test/trading/seaport/utils/deploy-immutable-contracts.ts deleted file mode 100644 index 08d23653d..000000000 --- a/test/trading/seaport/utils/deploy-immutable-contracts.ts +++ /dev/null @@ -1,66 +0,0 @@ -import hre from "hardhat"; -import { ImmutableSeaport, ImmutableSignedZone } from "../../../../typechain-types"; - -// Deploy the Immutable ecosystem contracts, returning the contract -// references -export async function deployImmutableContracts(serverSignerAddress: string): Promise<{ - immutableSeaport: ImmutableSeaport; - immutableSignedZone: ImmutableSignedZone; - conduitKey: string; - conduitAddress: string; -}> { - const accounts = await hre.ethers.getSigners(); - const seaportConduitControllerContractFactory = await hre.ethers.getContractFactory("ConduitController"); - const seaportConduitControllerContract = await seaportConduitControllerContractFactory.deploy(); - await seaportConduitControllerContract.deployed(); - - const readOnlyValidatorFactory = await hre.ethers.getContractFactory("ReadOnlyOrderValidator"); - const readOnlyValidatorContract = await readOnlyValidatorFactory.deploy(); - - const validatorHelperFactory = await hre.ethers.getContractFactory("SeaportValidatorHelper"); - const validatorHelperContract = await validatorHelperFactory.deploy(); - - const seaportValidatorFactory = await hre.ethers.getContractFactory("SeaportValidator"); - await seaportValidatorFactory.deploy( - readOnlyValidatorContract.address, - validatorHelperContract.address, - seaportConduitControllerContract.address - ); - - const immutableSignedZoneFactory = await hre.ethers.getContractFactory("ImmutableSignedZone"); - const immutableSignedZoneContract = (await immutableSignedZoneFactory.deploy( - "ImmutableSignedZone", - "", - "", - accounts[0].address - )) as ImmutableSignedZone; - await immutableSignedZoneContract.deployed(); - - const tx = await immutableSignedZoneContract.addSigner(serverSignerAddress); - await tx.wait(1); - - // conduit key: The conduit key used to deploy the conduit. Note that the first twenty bytes of the conduit key must match the caller of this contract. - const conduitKey = `${accounts[0].address}000000000000000000000000`; - - await (await seaportConduitControllerContract.createConduit(conduitKey, accounts[0].address)).wait(1); - - const { conduit: conduitAddress } = await seaportConduitControllerContract.getConduit(conduitKey); - - const seaportContractFactory = await hre.ethers.getContractFactory("ImmutableSeaport"); - const seaportContract = (await seaportContractFactory.deploy( - seaportConduitControllerContract.address, - accounts[0].address - )) as ImmutableSeaport; - await seaportContract.deployed(); - - // add ImmutableZone - await (await seaportContract.connect(accounts[0]).setAllowedZone(immutableSignedZoneContract.address, true)).wait(1); - await (await seaportConduitControllerContract.updateChannel(conduitAddress, seaportContract.address, true)).wait(1); - - return { - immutableSeaport: seaportContract, - immutableSignedZone: immutableSignedZoneContract, - conduitKey, - conduitAddress, - }; -} diff --git a/test/trading/seaport/utils/eip712/Eip712MerkleTree.ts b/test/trading/seaport/utils/eip712/Eip712MerkleTree.ts deleted file mode 100644 index 6222f36dd..000000000 --- a/test/trading/seaport/utils/eip712/Eip712MerkleTree.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { _TypedDataEncoder as TypedDataEncoder } from "@ethersproject/hash"; -import { expect } from "chai"; -import { defaultAbiCoder, hexConcat, keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import { MerkleTree } from "merkletreejs"; - -import { DefaultGetter } from "./defaults"; -import { bufferKeccak, bufferToHex, chunk, fillArray, getRoot, hexToBuffer } from "./utils"; - -import type { OrderComponents } from "../types"; -import type { EIP712TypeDefinitions } from "./defaults"; - -type BulkOrderElements = [OrderComponents, OrderComponents] | [BulkOrderElements, BulkOrderElements]; - -const getTree = (leaves: string[], defaultLeafHash: string) => - new MerkleTree(leaves.map(hexToBuffer), bufferKeccak, { - complete: true, - sort: false, - hashLeaves: false, - fillDefaultHash: hexToBuffer(defaultLeafHash), - }); - -const encodeProof = (key: number, proof: string[], signature = `0x${"ff".repeat(64)}`) => { - return hexConcat([ - signature, - `0x${key.toString(16).padStart(6, "0")}`, - defaultAbiCoder.encode([`uint256[${proof.length}]`], [proof]), - ]); -}; - -export class Eip712MerkleTree = any> { - tree: MerkleTree; - private leafHasher: (value: any) => string; - defaultNode: BaseType; - defaultLeaf: string; - encoder: TypedDataEncoder; - - get completedSize() { - return Math.pow(2, this.depth); - } - - /** Returns the array of elements in the tree, padded to the complete size with empty items. */ - getCompleteElements() { - const elements = this.elements; - return fillArray([...elements], this.completedSize, this.defaultNode); - } - - /** Returns the array of leaf nodes in the tree, padded to the complete size with default hashes. */ - getCompleteLeaves() { - const leaves = this.elements.map(this.leafHasher); - return fillArray([...leaves], this.completedSize, this.defaultLeaf); - } - - get root() { - return this.tree.getHexRoot(); - } - - getProof(i: number) { - const leaves = this.getCompleteLeaves(); - const leaf = leaves[i]; - const proof = this.tree.getHexProof(leaf, i); - const root = this.tree.getHexRoot(); - return { leaf, proof, root }; - } - - getEncodedProofAndSignature(i: number, signature: string) { - const { proof } = this.getProof(i); - return encodeProof(i, proof, signature); - } - - getDataToSign(): BulkOrderElements { - let layer = this.getCompleteElements() as any; - while (layer.length > 2) { - layer = chunk(layer, 2); - } - return layer; - } - - add(element: BaseType) { - this.elements.push(element); - } - - getBulkOrderHash() { - const structHash = this.encoder.hashStruct("BulkOrder", { - tree: this.getDataToSign(), - }); - const leaves = this.getCompleteLeaves().map(hexToBuffer); - const rootHash = bufferToHex(getRoot(leaves, false)); - const typeHash = keccak256(toUtf8Bytes(this.encoder._types.BulkOrder)); - const bulkOrderHash = keccak256(hexConcat([typeHash, rootHash])); - - expect(bulkOrderHash, "derived bulk order hash should match").to.equal(structHash); - - return structHash; - } - - constructor( - public types: EIP712TypeDefinitions, - public rootType: string, - public leafType: string, - public elements: BaseType[], - public depth: number - ) { - const encoder = TypedDataEncoder.from(types); - this.encoder = encoder; - this.leafHasher = (leaf: BaseType) => encoder.hashStruct(leafType, leaf); - this.defaultNode = DefaultGetter.from(types, leafType); - this.defaultLeaf = this.leafHasher(this.defaultNode); - this.tree = getTree(this.getCompleteLeaves(), this.defaultLeaf); - } -} diff --git a/test/trading/seaport/utils/eip712/bulk-orders.ts b/test/trading/seaport/utils/eip712/bulk-orders.ts deleted file mode 100644 index dba562fe0..000000000 --- a/test/trading/seaport/utils/eip712/bulk-orders.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { _TypedDataEncoder, keccak256, toUtf8Bytes } from "ethers/lib/utils"; - -import { Eip712MerkleTree } from "./Eip712MerkleTree"; -import { DefaultGetter } from "./defaults"; -import { fillArray } from "./utils"; - -import type { OrderComponents } from "../types"; -import type { EIP712TypeDefinitions } from "./defaults"; - -const bulkOrderType = { - BulkOrder: [{ name: "tree", type: "OrderComponents[2][2][2][2][2][2][2]" }], - OrderComponents: [ - { name: "offerer", type: "address" }, - { name: "zone", type: "address" }, - { name: "offer", type: "OfferItem[]" }, - { name: "consideration", type: "ConsiderationItem[]" }, - { name: "orderType", type: "uint8" }, - { name: "startTime", type: "uint256" }, - { name: "endTime", type: "uint256" }, - { name: "zoneHash", type: "bytes32" }, - { name: "salt", type: "uint256" }, - { name: "conduitKey", type: "bytes32" }, - { name: "counter", type: "uint256" }, - ], - OfferItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - ], - ConsiderationItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - { name: "recipient", type: "address" }, - ], -}; -function getBulkOrderTypes(height: number): EIP712TypeDefinitions { - const types = { ...bulkOrderType }; - types.BulkOrder = [{ name: "tree", type: `OrderComponents${`[2]`.repeat(height)}` }]; - return types; -} - -export function getBulkOrderTreeHeight(length: number): number { - return Math.max(Math.ceil(Math.log2(length)), 1); -} - -export function getBulkOrderTree( - orderComponents: OrderComponents[], - startIndex = 0, - height = getBulkOrderTreeHeight(orderComponents.length + startIndex) -) { - const types = getBulkOrderTypes(height); - const defaultNode = DefaultGetter.from(types, "OrderComponents"); - let elements = [...orderComponents]; - - if (startIndex > 0) { - elements = [...fillArray([] as OrderComponents[], startIndex, defaultNode), ...orderComponents]; - } - const tree = new Eip712MerkleTree(types, "BulkOrder", "OrderComponents", elements, height); - return tree; -} - -export function getBulkOrderTypeHash(height: number): string { - const types = getBulkOrderTypes(height); - const encoder = _TypedDataEncoder.from(types); - const typeString = toUtf8Bytes(encoder._types.BulkOrder); - return keccak256(typeString); -} - -export function getBulkOrderTypeHashes(maxHeight: number): string[] { - const typeHashes: string[] = []; - for (let i = 0; i < maxHeight; i++) { - typeHashes.push(getBulkOrderTypeHash(i + 1)); - } - return typeHashes; -} diff --git a/test/trading/seaport/utils/eip712/defaults.ts b/test/trading/seaport/utils/eip712/defaults.ts deleted file mode 100644 index 6a46d4469..000000000 --- a/test/trading/seaport/utils/eip712/defaults.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable no-dupe-class-members */ -/* eslint-disable no-unused-vars */ -import { Logger } from "@ethersproject/logger"; -import { hexZeroPad } from "ethers/lib/utils"; - -import type { TypedDataField } from "@ethersproject/abstract-signer"; - -const logger = new Logger("defaults"); - -const baseDefaults: Record = { - integer: 0, - address: hexZeroPad("0x", 20), - bool: false, - bytes: "0x", - string: "", -}; - -const isNullish = (value: any): boolean => { - if (value === undefined) return false; - - return ( - value !== undefined && - value !== null && - ((["string", "number"].includes(typeof value) && BigInt(value) === BigInt(0)) || - (Array.isArray(value) && value.every(isNullish)) || - (typeof value === "object" && Object.values(value).every(isNullish)) || - (typeof value === "boolean" && value === false)) - ); -}; - -function getDefaultForBaseType(type: string): any { - // bytesXX - const [, width] = type.match(/^bytes(\d+)$/) ?? []; - if (width) return hexZeroPad("0x", parseInt(width)); - - if (type.match(/^(u?)int(\d*)$/)) type = "integer"; - - return baseDefaults[type]; -} - -export type EIP712TypeDefinitions = Record; - -type DefaultMap = { - [K in keyof T]: any; -}; - -export class DefaultGetter { - defaultValues: DefaultMap = {} as DefaultMap; - - constructor(protected types: Types) { - for (const name in types) { - const defaultValue = this.getDefaultValue(name); - this.defaultValues[name] = defaultValue; - if (!isNullish(defaultValue)) { - logger.throwError(`Got non-empty value for type ${name} in default generator: ${defaultValue}`); - } - } - } - - static from(types: Types): DefaultMap; - - static from(types: Types, type: keyof Types): any; - - static from(types: Types, type?: keyof Types): DefaultMap { - const { defaultValues } = new DefaultGetter(types); - if (type) return defaultValues[type]; - return defaultValues; - } - - getDefaultValue(type: string): any { - if (this.defaultValues[type]) return this.defaultValues[type]; - // Basic type (address, bool, uint256, etc) - const basic = getDefaultForBaseType(type); - if (basic !== undefined) return basic; - - // Array - const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); - if (match) { - const subtype = match[1]; - const length = parseInt(match[3]); - if (length > 0) { - const baseValue = this.getDefaultValue(subtype); - return Array(length).fill(baseValue); - } - return []; - } - - // Struct - const fields = this.types[type]; - if (fields) { - return fields.reduce( - (obj, { name, type }) => ({ - ...obj, - [name]: this.getDefaultValue(type), - }), - {} - ); - } - - return logger.throwArgumentError(`unknown type: ${type}`, "type", type); - } -} diff --git a/test/trading/seaport/utils/eip712/utils.ts b/test/trading/seaport/utils/eip712/utils.ts deleted file mode 100644 index f88fde656..000000000 --- a/test/trading/seaport/utils/eip712/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { hexConcat, hexlify, keccak256 } from "ethers/lib/utils"; - -import type { BytesLike } from "ethers"; - -export const makeArray = (len: number, getValue: (i: number) => T) => - Array(len) - .fill(0) - .map((_, i) => getValue(i)); - -export const chunk = (array: T[], size: number) => { - return makeArray(Math.ceil(array.length / size), (i) => array.slice(i * size, (i + 1) * size)); -}; - -export const bufferToHex = (buf: Buffer) => hexlify(buf); - -export const hexToBuffer = (value: string) => Buffer.from(value.slice(2), "hex"); - -export const bufferKeccak = (value: BytesLike) => hexToBuffer(keccak256(value)); - -export const hashConcat = (arr: BytesLike[]) => bufferKeccak(hexConcat(arr)); - -export const fillArray = (arr: T[], length: number, value: T) => { - if (length > arr.length) arr.push(...Array(length - arr.length).fill(value)); - return arr; -}; - -export const getRoot = (elements: (Buffer | string)[], hashLeaves = true) => { - if (elements.length === 0) throw new Error("empty tree"); - - const leaves = elements.map((e) => { - const leaf = Buffer.isBuffer(e) ? e : hexToBuffer(e); - return hashLeaves ? bufferKeccak(leaf) : leaf; - }); - - const layers: Buffer[][] = [leaves]; - - // Get next layer until we reach the root - while (layers[layers.length - 1].length > 1) { - layers.push(getNextLayer(layers[layers.length - 1])); - } - - return layers[layers.length - 1][0]; -}; - -export const getNextLayer = (elements: Buffer[]) => { - return chunk(elements, 2).map(hashConcat); - // return elements.reduce((layer: Buffer[], el, idx, arr) => { - // if (idx % 2 === 0) layer.push(hashConcat(el, arr[idx + 1])); - // return layer; - // }, []); -}; diff --git a/test/trading/seaport/utils/encoding.ts b/test/trading/seaport/utils/encoding.ts deleted file mode 100644 index 0b0bb4a29..000000000 --- a/test/trading/seaport/utils/encoding.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { randomBytes } from "crypto"; -import { BigNumber, constants, utils } from "ethers"; -import type { BigNumberish } from "ethers"; -import { ConsiderationItem, CriteriaResolver, OfferItem, OrderComponents } from "./types"; -import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import { expect } from "chai"; - -export const randomHex = (bytes = 32) => `0x${randomBytes(bytes).toString("hex")}`; -export const toBN = (n: BigNumberish) => BigNumber.from(toHex(n)); -export const randomBN = (bytes: number = 16) => toBN(randomHex(bytes)); -export const toKey = (n: BigNumberish) => toHex(n, 32); -export const toHex = (n: BigNumberish, numBytes: number = 0) => { - const asHexString = BigNumber.isBigNumber(n) - ? n.toHexString().slice(2) - : typeof n === "string" - ? hexRegex.test(n) - ? n.replace(/0x/, "") - : Number(n).toString(16) - : Number(n).toString(16); - return `0x${asHexString.padStart(numBytes * 2, "0")}`; -}; - -const hexRegex = /[A-Fa-fx]/g; - -export const getItemETH = (startAmount: BigNumberish = 1, endAmount: BigNumberish = 1, recipient?: string) => - getOfferOrConsiderationItem(0, constants.AddressZero, 0, toBN(startAmount), toBN(endAmount), recipient); - -export const getItem721 = ( - token: string, - identifierOrCriteria: BigNumberish, - startAmount: number = 1, - endAmount: number = 1, - recipient?: string -) => getOfferOrConsiderationItem(2, token, identifierOrCriteria, startAmount, endAmount, recipient); - -export const getOfferOrConsiderationItem = ( - itemType: number = 0, - token: string = constants.AddressZero, - identifierOrCriteria: BigNumberish = 0, - startAmount: BigNumberish = 1, - endAmount: BigNumberish = 1, - recipient?: RecipientType -): RecipientType extends string ? ConsiderationItem : OfferItem => { - const offerItem: OfferItem = { - itemType, - token, - identifierOrCriteria: toBN(identifierOrCriteria), - startAmount: toBN(startAmount), - endAmount: toBN(endAmount), - }; - if (typeof recipient === "string") { - return { - ...offerItem, - recipient: recipient as string, - } as ConsiderationItem; - } - return offerItem as any; -}; - -export const convertSignatureToEIP2098 = (signature: string) => { - if (signature.length === 130) { - return signature; - } - - expect(signature.length, "signature must be 64 or 65 bytes").to.eq(132); - - return utils.splitSignature(signature).compact; -}; - -export const calculateOrderHash = (orderComponents: OrderComponents) => { - const offerItemTypeString = - "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)"; - const considerationItemTypeString = - "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)"; - const orderComponentsPartialTypeString = - "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)"; - const orderTypeString = `${orderComponentsPartialTypeString}${considerationItemTypeString}${offerItemTypeString}`; - - const offerItemTypeHash = keccak256(toUtf8Bytes(offerItemTypeString)); - const considerationItemTypeHash = keccak256(toUtf8Bytes(considerationItemTypeString)); - const orderTypeHash = keccak256(toUtf8Bytes(orderTypeString)); - - const offerHash = keccak256( - "0x" + - orderComponents.offer - .map((offerItem) => { - return keccak256( - "0x" + - [ - offerItemTypeHash.slice(2), - offerItem.itemType.toString().padStart(64, "0"), - offerItem.token.slice(2).padStart(64, "0"), - toBN(offerItem.identifierOrCriteria).toHexString().slice(2).padStart(64, "0"), - toBN(offerItem.startAmount).toHexString().slice(2).padStart(64, "0"), - toBN(offerItem.endAmount).toHexString().slice(2).padStart(64, "0"), - ].join("") - ).slice(2); - }) - .join("") - ); - - const considerationHash = keccak256( - "0x" + - orderComponents.consideration - .map((considerationItem) => { - return keccak256( - "0x" + - [ - considerationItemTypeHash.slice(2), - considerationItem.itemType.toString().padStart(64, "0"), - considerationItem.token.slice(2).padStart(64, "0"), - toBN(considerationItem.identifierOrCriteria).toHexString().slice(2).padStart(64, "0"), - toBN(considerationItem.startAmount).toHexString().slice(2).padStart(64, "0"), - toBN(considerationItem.endAmount).toHexString().slice(2).padStart(64, "0"), - considerationItem.recipient.slice(2).padStart(64, "0"), - ].join("") - ).slice(2); - }) - .join("") - ); - - const derivedOrderHash = keccak256( - "0x" + - [ - orderTypeHash.slice(2), - orderComponents.offerer.slice(2).padStart(64, "0"), - orderComponents.zone.slice(2).padStart(64, "0"), - offerHash.slice(2), - considerationHash.slice(2), - orderComponents.orderType.toString().padStart(64, "0"), - toBN(orderComponents.startTime).toHexString().slice(2).padStart(64, "0"), - toBN(orderComponents.endTime).toHexString().slice(2).padStart(64, "0"), - orderComponents.zoneHash.slice(2), - orderComponents.salt.slice(2).padStart(64, "0"), - orderComponents.conduitKey.slice(2).padStart(64, "0"), - toBN(orderComponents.counter).toHexString().slice(2).padStart(64, "0"), - ].join("") - ); - - return derivedOrderHash; -}; - -export const buildResolver = ( - orderIndex: number, - side: 0 | 1, - index: number, - identifier: BigNumber, - criteriaProof: string[] -): CriteriaResolver => ({ - orderIndex, - side, - index, - identifier, - criteriaProof, -}); diff --git a/test/trading/seaport/utils/erc721.ts b/test/trading/seaport/utils/erc721.ts deleted file mode 100644 index 37cd83f5a..000000000 --- a/test/trading/seaport/utils/erc721.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { expect } from "chai"; -import hre from "hardhat"; - -import { getOfferOrConsiderationItem, randomBN } from "./encoding"; - -import type { TestERC721 } from "../../../../typechain-types"; -import type { BigNumberish, BigNumber, Contract, Wallet } from "ethers"; - -export async function deployERC721(): Promise { - const erc721Factory = await hre.ethers.getContractFactory("TestERC721"); - const erc721 = (await erc721Factory.deploy()) as TestERC721; - return erc721.deployed(); -} - -export async function mint721(erc721: TestERC721, signer: Wallet | Contract): Promise { - const nftId = randomBN(); - await erc721.mint(signer.address, nftId); - return nftId; -} - -export async function set721ApprovalForAll(erc721: TestERC721, signer: Wallet, spender: string, approved = true) { - return expect(erc721.connect(signer).setApprovalForAll(spender, approved)) - .to.emit(erc721, "ApprovalForAll") - .withArgs(signer.address, spender, approved); -} - -export async function mintAndApprove721(erc721: TestERC721, signer: Wallet, spender: string): Promise { - await set721ApprovalForAll(erc721, signer, spender, true); - return mint721(erc721, signer); -} - -export async function getTestItem721( - tokenAddress: string, - identifierOrCriteria: BigNumberish, - startAmount: BigNumberish = 1, - endAmount: BigNumberish = 1, - recipient?: string -) { - return getOfferOrConsiderationItem(2, tokenAddress, identifierOrCriteria, startAmount, endAmount, recipient); -} -export function getTestItem721WithCriteria( - tokenAddress: string, - identifierOrCriteria: BigNumberish, - startAmount: BigNumberish = 1, - endAmount: BigNumberish = 1, - recipient?: string -) { - return getOfferOrConsiderationItem(4, tokenAddress, identifierOrCriteria, startAmount, endAmount, recipient); -} diff --git a/test/trading/seaport/utils/faucet.ts b/test/trading/seaport/utils/faucet.ts deleted file mode 100644 index 0b6e91f15..000000000 --- a/test/trading/seaport/utils/faucet.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { parseEther } from "@ethersproject/units"; -import { ethers } from "hardhat"; - -import { randomHex } from "./encoding"; - -import type { JsonRpcProvider } from "@ethersproject/providers"; - -const TEN_THOUSAND_ETH = parseEther("10000").toHexString().replace("0x0", "0x"); - -export const faucet = async (address: string, provider: JsonRpcProvider) => { - await provider.send("hardhat_setBalance", [address, TEN_THOUSAND_ETH]); -}; - -export const getWalletWithEther = async () => { - const wallet = new ethers.Wallet(randomHex(32), ethers.provider); - await faucet(wallet.address, ethers.provider); - return wallet; -}; diff --git a/test/trading/seaport/utils/order.ts b/test/trading/seaport/utils/order.ts deleted file mode 100644 index 9affe7d70..000000000 --- a/test/trading/seaport/utils/order.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { expect } from "chai"; -import { constants, utils } from "ethers"; -import { keccak256, recoverAddress } from "ethers/lib/utils"; -import { ethers } from "hardhat"; - -import { calculateOrderHash, convertSignatureToEIP2098, randomHex, toBN } from "./encoding"; - -import type { ConsiderationItem, OfferItem, OrderComponents } from "./types"; -import type { Contract, Wallet } from "ethers"; -import { ImmutableSeaport, TestZone } from "../../../../typechain-types"; -import { getBulkOrderTree } from "./eip712/bulk-orders"; -import { ReceivedItemStruct } from "../../../../typechain-types/contracts/trading/seaport/ImmutableSeaport"; -import { CONSIDERATION_EIP712_TYPE, EIP712_DOMAIN, SIGNED_ORDER_EIP712_TYPE, getCurrentTimeStamp } from "./signedZone"; - -const orderType = { - OrderComponents: [ - { name: "offerer", type: "address" }, - { name: "zone", type: "address" }, - { name: "offer", type: "OfferItem[]" }, - { name: "consideration", type: "ConsiderationItem[]" }, - { name: "orderType", type: "uint8" }, - { name: "startTime", type: "uint256" }, - { name: "endTime", type: "uint256" }, - { name: "zoneHash", type: "bytes32" }, - { name: "salt", type: "uint256" }, - { name: "conduitKey", type: "bytes32" }, - { name: "counter", type: "uint256" }, - ], - OfferItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - ], - ConsiderationItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - { name: "recipient", type: "address" }, - ], -}; - -export async function getAndVerifyOrderHash(marketplace: ImmutableSeaport, orderComponents: OrderComponents) { - const orderHash = await marketplace.getOrderHash(orderComponents); - const derivedOrderHash = calculateOrderHash(orderComponents); - expect(orderHash).to.equal(derivedOrderHash); - return orderHash; -} - -export function getDomainData(marketplaceAddress: string, chainId: string) { - return { - name: "ImmutableSeaport", - version: "1.5", - chainId, - verifyingContract: marketplaceAddress, - }; -} - -export async function signOrder( - marketplace: ImmutableSeaport, - orderComponents: OrderComponents, - signer: Wallet | Contract -) { - const chainId = await signer.getChainId(); - const signature = await signer._signTypedData( - { ...getDomainData(marketplace.address, chainId), verifyingContract: marketplace.address }, - orderType, - orderComponents - ); - - const orderHash = await getAndVerifyOrderHash(marketplace, orderComponents); - - const { domainSeparator } = await marketplace.information(); - const digest = keccak256(`0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}`); - const recoveredAddress = recoverAddress(digest, signature); - - expect(recoveredAddress).to.equal(signer.address); - - return signature; -} - -const signBulkOrder = async ( - marketplace: ImmutableSeaport, - orderComponents: OrderComponents[], - signer: Wallet | Contract, - startIndex = 0, - height?: number, - extraCheap?: boolean -) => { - const chainId = await signer.getChainId(); - const tree = getBulkOrderTree(orderComponents, startIndex, height); - const bulkOrderType = tree.types; - const chunks = tree.getDataToSign(); - let signature = await signer._signTypedData(getDomainData(marketplace.address, chainId), bulkOrderType, { - tree: chunks, - }); - - if (extraCheap) { - signature = convertSignatureToEIP2098(signature); - } - - const proofAndSignature = tree.getEncodedProofAndSignature(startIndex, signature); - - const orderHash = tree.getBulkOrderHash(); - - const { domainSeparator } = await marketplace.information(); - const digest = keccak256(`0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}`); - const recoveredAddress = recoverAddress(digest, signature); - - expect(recoveredAddress).to.equal(signer.address); - - // Verify each individual order - for (const components of orderComponents) { - const individualOrderHash = await getAndVerifyOrderHash(marketplace, components); - const digest = keccak256(`0x1901${domainSeparator.slice(2)}${individualOrderHash.slice(2)}`); - const individualOrderSignature = await signer._signTypedData( - getDomainData(marketplace.address, chainId), - orderType, - components - ); - const recoveredAddress = recoverAddress(digest, individualOrderSignature); - expect(recoveredAddress).to.equal(signer.address); - } - - return proofAndSignature; -}; - -export async function createOrder( - marketplace: ImmutableSeaport, - offerer: Wallet | Contract, - zone: TestZone | Wallet | undefined | string = undefined, - offer: OfferItem[], - consideration: ConsiderationItem[], - orderType: number, - timeFlag?: string | null, - signer?: Wallet, - zoneHash = constants.HashZero, - conduitKey = constants.HashZero, - extraCheap = false, - useBulkSignature = false, - bulkSignatureIndex?: number, - bulkSignatureHeight?: number -) { - const counter = await marketplace.getCounter(offerer.address); - - const salt = !extraCheap ? randomHex() : constants.HashZero; - const startTime = timeFlag !== "NOT_STARTED" ? 0 : toBN("0xee00000000000000000000000000"); - const endTime = timeFlag !== "EXPIRED" ? toBN("0xff00000000000000000000000000") : 1; - - const orderParameters = { - offerer: offerer.address, - zone: !extraCheap ? (zone as Wallet).address ?? zone : constants.AddressZero, - offer, - consideration, - totalOriginalConsiderationItems: consideration.length, - orderType, - zoneHash, - salt, - conduitKey, - startTime, - endTime, - }; - - const orderComponents = { - ...orderParameters, - counter, - }; - - const orderHash = await getAndVerifyOrderHash(marketplace, orderComponents); - - const { isValidated, isCancelled, totalFilled, totalSize } = await marketplace.getOrderStatus(orderHash); - - expect(isCancelled).to.equal(false); - - const orderStatus = { - isValidated, - isCancelled, - totalFilled, - totalSize, - }; - - const flatSig = await signOrder(marketplace, orderComponents, signer ?? offerer); - - const order = { - parameters: orderParameters, - signature: !extraCheap ? flatSig : convertSignatureToEIP2098(flatSig), - numerator: 1, // only used for advanced orders - denominator: 1, // only used for advanced orders - extraData: "0x", // only used for advanced orders - }; - - if (useBulkSignature) { - order.signature = await signBulkOrder( - marketplace, - [orderComponents], - signer ?? offerer, - bulkSignatureIndex, - bulkSignatureHeight, - extraCheap - ); - - // Verify bulk signature length - expect(order.signature.slice(2).length / 2, "bulk signature length should be valid (98 < length < 837)") - .to.be.gt(98) - .and.lt(837); - expect( - (order.signature.slice(2).length / 2 - 67) % 32, - "bulk signature length should be valid ((length - 67) % 32 < 2)" - ).to.be.lt(2); - } - - // How much ether (at most) needs to be supplied when fulfilling the order - const value = offer - .map((x) => (x.itemType === 0 ? (x.endAmount.gt(x.startAmount) ? x.endAmount : x.startAmount) : toBN(0))) - .reduce((a, b) => a.add(b), toBN(0)) - .add( - consideration - .map((x) => (x.itemType === 0 ? (x.endAmount.gt(x.startAmount) ? x.endAmount : x.startAmount) : toBN(0))) - .reduce((a, b) => a.add(b), toBN(0)) - ); - - return { - order, - orderHash, - value, - orderStatus, - orderComponents, - startTime, - endTime, - }; -} - -export async function generateSip7Signature( - consideration: ConsiderationItem[], - orderHash: string, - fulfillerAddress: string, - immutableSignedZoneAddress: string, - immutableSigner: Wallet -) { - const considerationAsReceivedItem: ReceivedItemStruct[] = consideration.map((item) => { - return { - amount: item.startAmount, - identifier: item.identifierOrCriteria, - itemType: item.itemType, - recipient: item.recipient, - token: item.token, - }; - }); - - const expiration = (await getCurrentTimeStamp()) + 100; - const considerationHash = utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration: considerationAsReceivedItem, - }); - - const context = utils.solidityPack(["bytes", "bytes[]"], [considerationHash, [orderHash]]); - - const signedOrder = { - fulfiller: fulfillerAddress, - expiration, - orderHash, - context, - }; - - const chainId = (await ethers.provider.getNetwork()).chainId; - const signature = await immutableSigner._signTypedData( - EIP712_DOMAIN(chainId, immutableSignedZoneAddress), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - return utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfillerAddress, expiration, convertSignatureToEIP2098(signature), context] - ); -} diff --git a/test/trading/seaport/utils/signedZone.ts b/test/trading/seaport/utils/signedZone.ts deleted file mode 100644 index 2ac2bdc43..000000000 --- a/test/trading/seaport/utils/signedZone.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { utils } from "ethers"; -import { ethers } from "hardhat"; - -import type { BigNumber } from "ethers"; - -export const A_NON_ZERO_ADDRESS = "0x1234000000000000000000000000000000000000"; -export const SECONDS_IN_A_DAY = 24 * 60 * 60; - -export async function balance(address: string): Promise { - return await ethers.provider.getBalance(address); -} - -export async function getCurrentTimeStamp(): Promise { - const blockNumber = await ethers.provider.getBlockNumber(); - return (await ethers.provider.getBlock(blockNumber)).timestamp; -} - -export async function getCurrentBlockNumber(): Promise { - return await ethers.provider.getBlockNumber(); -} - -export async function advanceBlockAtTime(time: number): Promise { - await ethers.provider.send("evm_mine", [time]); -} - -export async function advanceBlockBySeconds(secondsToAdd: number): Promise { - const newTimestamp = (await getCurrentTimeStamp()) + secondsToAdd; - await ethers.provider.send("evm_mine", [newTimestamp]); -} - -export async function advanceTimeByDays(daysToAdd: number): Promise { - const secondsToAdd = daysToAdd * SECONDS_IN_A_DAY; - await ethers.provider.send("evm_increaseTime", [secondsToAdd]); - mine(); -} - -export async function mineBlocks(blocksToMineInHex: string = "0x100"): Promise { - await ethers.provider.send("hardhat_mine", [blocksToMineInHex]); -} - -export async function mine(): Promise { - await ethers.provider.send("evm_mine", []); -} - -export async function manualMining(): Promise { - await ethers.provider.send("evm_setAutomine", [false]); - await ethers.provider.send("evm_setIntervalMining", [0]); -} -export async function autoMining(): Promise { - await ethers.provider.send("evm_setAutomine", [true]); -} - -// Quick hack to remove array subobject from ethers results -export function cleanResult(result: { [key: string]: any }): any { - const clean = {} as { - [key: string]: any; - }; - Object.keys(result) - .filter((key) => isNaN(parseFloat(key))) - .reduce((obj, key) => { - clean[key] = result[key]; - return obj; - }, {}); - return clean; -} - -export const convertSignatureToEIP2098 = (signature: string) => { - if (signature.length === 130) { - return signature; - } - - if (signature.length !== 132) { - throw Error("invalid signature length (must be 64 or 65 bytes)"); - } - - return utils.splitSignature(signature).compact; -}; - -export const EIP712_DOMAIN = (chainId: number, contract: string) => ({ - name: "ImmutableSignedZone", - version: "1.0", - chainId, - verifyingContract: contract, -}); - -export const SIGNED_ORDER_EIP712_TYPE = { - SignedOrder: [ - { name: "fulfiller", type: "address" }, - { name: "expiration", type: "uint64" }, - { name: "orderHash", type: "bytes32" }, - { name: "context", type: "bytes" }, - ], -}; - -export const CONSIDERATION_EIP712_TYPE = { - Consideration: [{ name: "consideration", type: "ReceivedItem[]" }], - ReceivedItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifier", type: "uint256" }, - { name: "amount", type: "uint256" }, - { name: "recipient", type: "address" }, - ], -}; diff --git a/test/trading/seaport/utils/types.ts b/test/trading/seaport/utils/types.ts deleted file mode 100644 index 699e81586..000000000 --- a/test/trading/seaport/utils/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BigNumber } from "ethers"; - -export type ConsiderationItem = { - itemType: number; - token: string; - identifierOrCriteria: BigNumber; - startAmount: BigNumber; - endAmount: BigNumber; - recipient: string; -}; -export type OfferItem = { - itemType: number; - token: string; - identifierOrCriteria: BigNumber; - startAmount: BigNumber; - endAmount: BigNumber; -}; - -export type OrderParameters = { - offerer: string; - zone: string; - offer: OfferItem[]; - consideration: ConsiderationItem[]; - orderType: number; - startTime: string | BigNumber | number; - endTime: string | BigNumber | number; - zoneHash: string; - salt: string; - conduitKey: string; - totalOriginalConsiderationItems: string | BigNumber | number; -}; - -export type CriteriaResolver = { - orderIndex: number; - side: 0 | 1; - index: number; - identifier: BigNumber; - criteriaProof: string[]; -}; - -export type OrderComponents = Omit & { - counter: BigNumber; -}; diff --git a/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOrderValidation.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOrderValidation.t.sol new file mode 100644 index 000000000..63e34715a --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOrderValidation.t.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ImmutableSeaportTestHelper} from "../../../ImmutableSeaportTestHelper.t.sol"; +import {ImmutableSignedZone} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol"; +import {SIP7EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol"; +import {SIP6EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP6EventsAndErrors.sol"; +import {ZoneParameters, ReceivedItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; + + +contract ImmutableSignedZoneOrderValidationTest is Test, ImmutableSeaportTestHelper { + ImmutableSignedZone public zone; + address public owner; + address public signer; + uint256 public signerPkey; + address public fulfiller; + uint256 public chainId; + + function setUp() public { + // Create test addresses + owner = makeAddr("owner"); + (signer, signerPkey) = makeAddrAndKey("signer"); + fulfiller = makeAddr("fulfiller"); + + // Deploy contract + vm.startPrank(owner); + zone = new ImmutableSignedZone(ZONE_NAME, "", "", owner); + zone.addSigner(signer); + vm.stopPrank(); + + _setFulfillerAndZone(fulfiller, address(zone)); + } + + function testValidatesCorrectSignatureWithContext() public { + bytes32 orderHash = keccak256("0x1234"); + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + uint64 expiration = uint64(block.timestamp + 100); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, consideration); + bytes4 selector = zone.validateOrder(params); + assertEq(selector, bytes4(keccak256("validateOrder((bytes32,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256,address)[],bytes,bytes32[],uint256,uint256,bytes32))"))); + } + + function testValidateOrderWithMultipleOrderHashes() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + + // Create array of order hashes + bytes32[] memory orderHashes = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + orderHashes[i] = keccak256(abi.encodePacked("order", i)); + } + + // Create context with consideration hash and order hashes + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, orderHashes, consideration); + + bytes4 selector = zone.validateOrder(params); + assertEq(selector, bytes4(keccak256("validateOrder((bytes32,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256,address)[],bytes,bytes32[],uint256,uint256,bytes32))"))); + } + + function testValidateOrderWithoutExtraData() public { + bytes memory extraData = ""; + ZoneParameters memory params = _createZoneParameters(extraData); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.InvalidExtraData.selector, + "extraData is empty", params.orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithInvalidExtraData() public { + bytes memory extraData = abi.encodePacked(uint8(1), uint8(2), uint8(3)); + ZoneParameters memory params = _createZoneParameters(extraData); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.InvalidExtraData.selector, "extraData length must be at least 93 bytes", params.orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithExpiredTimestamp() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp); + bytes memory context = abi.encodePacked(keccak256("context")); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + signature, + context + ); + + // Advance time past expiration + uint64 timeNow = uint64(block.timestamp + 100); + vm.warp(uint256(timeNow)); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignatureExpired.selector, timeNow, expiration, orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithInvalidFulfiller() public { + address invalidFulfiller = makeAddr("invalidFulfiller"); + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 100); + bytes memory context = abi.encodePacked(keccak256("context")); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + invalidFulfiller, + expiration, + signature, + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.InvalidFulfiller.selector, invalidFulfiller, fulfiller, orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithNonZeroSIP6Version() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 100); + bytes memory context = abi.encodePacked(keccak256("context")); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(1), // Non-zero SIP6 version + fulfiller, + expiration, + signature, + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash); + vm.expectRevert(abi.encodeWithSelector(SIP6EventsAndErrors.UnsupportedExtraDataVersion.selector, 1)); + zone.validateOrder(params); + } + + function testValidateOrderWithNoContext() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 100); + bytes memory context = ""; + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + signature, + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.InvalidExtraData.selector, + "invalid context, expecting consideration hash followed by order hashes", params.orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithWrongConsideration() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 100); + bytes memory context = abi.encodePacked(keccak256("context")); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + signature, + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash); + params.consideration = _createMockConsideration(10); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SubstandardViolation.selector, + 3, "invalid consideration hash", orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderRevertsAfterExpiration() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + bytes memory context = abi.encodePacked(considerationHash, orderHash); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData); + + // First validate should succeed + bytes4 selector = zone.validateOrder(params); + assertEq(selector, bytes4(keccak256("validateOrder((bytes32,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256,address)[],bytes,bytes32[],uint256,uint256,bytes32))"))); + + // Advance time past expiration + vm.warp(block.timestamp + 900); + + // Second validate should fail + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignatureExpired.selector, uint64(block.timestamp), expiration, orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWithPartialOrderHashes() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + + // Create array of order hashes + bytes32[] memory orderHashes = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + orderHashes[i] = keccak256(abi.encodePacked("order", i)); + } + + // Create partial array of order hashes (first 2) + bytes32[] memory partialOrderHashes = new bytes32[](2); + partialOrderHashes[0] = orderHashes[0]; + partialOrderHashes[1] = orderHashes[1]; + + // Create context with consideration hash and partial order hashes + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(partialOrderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, orderHashes, consideration); + + bytes4 selector = zone.validateOrder(params); + assertEq(selector, bytes4(keccak256("validateOrder((bytes32,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256,address)[],bytes,bytes32[],uint256,uint256,bytes32))"))); + } + + function testValidateOrderWhenNotAllExpectedOrdersAreZoneParameters() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + + // Create array of order hashes + bytes32[] memory orderHashes = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + orderHashes[i] = keccak256(abi.encodePacked("order", i)); + } + + // Create context with consideration hash and full order hashes + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + // Create partial array of order hashes (first 2) + bytes32[] memory partialOrderHashes = new bytes32[](2); + partialOrderHashes[0] = orderHashes[0]; + partialOrderHashes[1] = orderHashes[1]; + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, partialOrderHashes, consideration); + + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SubstandardViolation.selector, + 4, "invalid order hashes", orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderWhenNotAllExpectedOrdersAreZoneParametersVariation() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + + // Create array of order hashes + bytes32[] memory orderHashes = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + orderHashes[i] = keccak256(abi.encodePacked("order", i)); + } + + // Create context with consideration hash and full order hashes + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + bytes memory signature = _signOrder(signerPkey, orderHash, expiration, context); + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + // Create array with first 2 order hashes and 2 random hashes + bytes32[] memory mixedOrderHashes = new bytes32[](4); + mixedOrderHashes[0] = orderHashes[0]; + mixedOrderHashes[1] = orderHashes[1]; + mixedOrderHashes[2] = keccak256("0x55"); + mixedOrderHashes[3] = keccak256("0x66"); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, mixedOrderHashes, consideration); + + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SubstandardViolation.selector, + 4, "invalid order hashes", orderHash)); + zone.validateOrder(params); + } + + function testValidateOrderRevertsWithIncorrectlySignedSignature() public { + bytes32 orderHash = keccak256("0x1234"); + uint64 expiration = uint64(block.timestamp + 90); + ReceivedItem[] memory consideration = _createMockConsideration(10); + bytes32 considerationHash = this._deriveConsiderationHash(consideration); + + // Create array of order hashes + bytes32[] memory orderHashes = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + orderHashes[i] = keccak256(abi.encodePacked("order", i)); + } + + // Create context with consideration hash and order hashes + bytes memory context = abi.encodePacked(considerationHash, _convertToBytesWithoutArrayLength(orderHashes)); + + // Sign with wrong signer + (address wrongSigner, uint256 wrongSignerPkey) = makeAddrAndKey("wrongSigner"); + bytes memory signature = _signOrder(wrongSignerPkey, orderHash, expiration, context); + + bytes memory extraData = abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + expiration, + this._convertSignatureToEIP2098(signature), + context + ); + + ZoneParameters memory params = _createZoneParameters(extraData, orderHash, orderHashes, consideration); + + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, wrongSigner)); + zone.validateOrder(params); + } +} \ No newline at end of file diff --git a/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOwnership.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOwnership.t.sol new file mode 100644 index 000000000..74533aefd --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneOwnership.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ImmutableSignedZone} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol"; +import {SIP7EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol"; + + + +contract ImmutableSignedZoneOwnershipTest is Test { + ImmutableSignedZone public zone; + address public owner; + address public user; + + function setUp() public { + // Create test addresses + owner = makeAddr("owner"); + user = makeAddr("user"); + + // Deploy contract + vm.startPrank(owner); + zone = new ImmutableSignedZone("ImmutableSignedZone", "", "", owner); + vm.stopPrank(); + } + + function testDeployerBecomesOwner() public { + assertEq(zone.owner(), owner); + } + + function testTransferOwnership() public { + address newOwner = makeAddr("newOwner"); + + vm.startPrank(owner); + zone.transferOwnership(newOwner); + vm.stopPrank(); + + assertEq(zone.owner(), newOwner); + } + + function testNonOwnerCannotTransferOwnership() public { + address newOwner = makeAddr("newOwner"); + + vm.startPrank(user); + vm.expectRevert("Ownable: caller is not the owner"); + zone.transferOwnership(newOwner); + vm.stopPrank(); + } + + function testNonOwnerCannotAddSigner() public { + vm.startPrank(user); + vm.expectRevert("Ownable: caller is not the owner"); + zone.addSigner(user); + vm.stopPrank(); + } + + function testNonOwnerCannotRemoveSigner() public { + vm.startPrank(user); + vm.expectRevert("Ownable: caller is not the owner"); + zone.removeSigner(user); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneSignerManagement.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneSignerManagement.t.sol new file mode 100644 index 000000000..ca7861ab7 --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZoneSignerManagement.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ImmutableSignedZone} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol"; +import {SIP7EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol"; + +contract ImmutableSignedZoneSignerManagementTest is Test { + ImmutableSignedZone public zone; + address public owner; + address public signer; + + function setUp() public { + // Create test addresses + owner = makeAddr("owner"); + signer = makeAddr("signer"); + + // Deploy contract + vm.startPrank(owner); + zone = new ImmutableSignedZone("ImmutableSignedZone", "", "", owner); + vm.stopPrank(); + } + + function testOwnerCanAddActiveSigner() public { + vm.startPrank(owner); + vm.expectEmit(true, true, true, true); + emit SIP7EventsAndErrors.SignerAdded(signer); + zone.addSigner(signer); + vm.stopPrank(); + } + + function testOwnerCanAddAndRemoveActiveSigner() public { + vm.startPrank(owner); + zone.addSigner(signer); + + vm.expectEmit(true, true, true, true); + emit SIP7EventsAndErrors.SignerRemoved(signer); + zone.removeSigner(signer); + vm.stopPrank(); + } + + function testCannotAddDeactivatedSigner() public { + vm.startPrank(owner); + zone.addSigner(signer); + zone.removeSigner(signer); + + // Try to add deactivated signer + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerCannotBeReauthorized.selector, signer)); + zone.addSigner(signer); + vm.stopPrank(); + } + + function testAlreadyActiveSignerCannotBeAdded() public { + vm.startPrank(owner); + zone.addSigner(signer); + + // Try to add same signer again + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerAlreadyActive.selector, signer)); + zone.addSigner(signer); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts b/test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts deleted file mode 100644 index 808e0b2ab..000000000 --- a/test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts +++ /dev/null @@ -1,617 +0,0 @@ -/* eslint-disable camelcase */ -import assert from "assert"; -import { expect } from "chai"; -import { Wallet, constants } from "ethers"; -import { keccak256 } from "ethers/lib/utils"; -import { ethers } from "hardhat"; - -import { ImmutableSignedZone__factory } from "../../../../../../typechain-types"; - -import { - CONSIDERATION_EIP712_TYPE, - EIP712_DOMAIN, - SIGNED_ORDER_EIP712_TYPE, - advanceBlockBySeconds, - autoMining, - convertSignatureToEIP2098, - getCurrentTimeStamp, -} from "../../../utils/signedZone"; - -import type { ImmutableSignedZone } from "../../../../../../typechain-types"; -import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import type { BytesLike } from "ethers"; -import { ReceivedItemStruct } from "../../../../../../typechain-types/contracts/trading/seaport/ImmutableSeaport"; -import { ZoneParametersStruct } from "../../../../../../typechain-types/contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone"; - -describe("ImmutableSignedZone", function () { - let deployer: SignerWithAddress; - let users: SignerWithAddress[]; - let contract: ImmutableSignedZone; - let chainId: number; - - beforeEach(async () => { - // automine ensure time based tests will work - await autoMining(); - chainId = (await ethers.provider.getNetwork()).chainId; - users = await ethers.getSigners(); - deployer = users[0]; - const factory = await ethers.getContractFactory("ImmutableSignedZone"); - const tx = await factory.connect(deployer).deploy("ImmutableSignedZone", "", "", deployer.address); - - const address = (await tx.deployed()).address; - - contract = ImmutableSignedZone__factory.connect(address, deployer); - }); - - describe("Ownership", async function () { - it("deployer becomes owner", async () => { - assert((await contract.owner()) === deployer.address); - }); - - it("transferOwnership works", async () => { - assert((await contract.owner()) === deployer.address); - const transferTx = await contract.connect(deployer).transferOwnership(users[2].address); - await transferTx.wait(1); - - assert((await contract.owner()) === users[2].address); - }); - - it("non owner cannot transfer ownership", async () => { - assert((await contract.owner()) === deployer.address); - await expect(contract.connect(users[1]).transferOwnership(users[1].address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - }); - - it("non owner cannot add signer", async () => { - await expect(contract.connect(users[1]).addSigner(users[1].address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - }); - - it("non owner cannot remove signer", async () => { - await expect(contract.connect(users[1]).removeSigner(users[1].address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - }); - }); - - describe("Signer management", async () => { - it("owner can add and remove active signer", async () => { - assert((await contract.owner()) === deployer.address); - const tx = await contract.connect(deployer).addSigner(users[1].address); - await tx.wait(1); - - await expect(contract.removeSigner(users[1].address)); - }); - - it("cannot add deactivated signer", async () => { - assert((await contract.owner()) === deployer.address); - const tx = await contract.connect(deployer).addSigner(users[1].address); - await tx.wait(1); - - (await contract.removeSigner(users[1].address)).wait(1); - - await expect(contract.addSigner(users[1].address)).to.be.revertedWith("SignerCannotBeReauthorized"); - }); - - it("already active signer cannot be added", async () => { - assert((await contract.owner()) === deployer.address); - const tx = await contract.connect(deployer).addSigner(users[1].address); - await tx.wait(1); - - await expect(contract.addSigner(users[1].address)).to.be.revertedWith("SignerAlreadyActive"); - }); - }); - - describe("Order Validation", async function () { - let signer: Wallet; - - beforeEach(async () => { - signer = ethers.Wallet.createRandom(); - // wait 1 block for all TXs - await (await contract.addSigner(signer.address)).wait(1); - }); - - it("validateOrder reverts without extraData", async function () { - await expect(contract.validateOrder(mockZoneParameter([]))).to.be.revertedWith("InvalidExtraData"); - }); - - it("validateOrder reverts with invalid extraData", async function () { - await expect(contract.validateOrder(mockZoneParameter([1, 2, 3]))).to.be.revertedWith("InvalidExtraData"); - }); - - it("validateOrder reverts with expired timestamp", async function () { - const orderHash = keccak256("0x1234"); - const expiration = await getCurrentTimeStamp(); - const fulfiller = constants.AddressZero; - const context = ethers.utils.randomBytes(32); - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [ - 0, // SIP6 version - fulfiller, - expiration, - convertSignatureToEIP2098(signature), - context, - ] - ); - - await advanceBlockBySeconds(100); - await expect(contract.validateOrder(mockZoneParameter(extraData))).to.be.revertedWith("SignatureExpired"); - }); - - it("validateOrder reverts with invalid fulfiller", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = Wallet.createRandom().address; - const context = ethers.utils.randomBytes(32); - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [ - 0, // SIP6 version - fulfiller, - expiration, - convertSignatureToEIP2098(signature), - context, - ] - ); - - await expect(contract.validateOrder(mockZoneParameter(extraData))).to.be.revertedWith("InvalidFulfiller"); - }); - - it("validateOrder reverts with non 0 SIP6 version", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = constants.AddressZero; - const context = ethers.utils.randomBytes(32); - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [ - 1, // SIP6 version - fulfiller, - expiration, - convertSignatureToEIP2098(signature), - context, - ] - ); - - await expect(contract.validateOrder(mockZoneParameter(extraData))).to.be.revertedWith( - "UnsupportedExtraDataVersion" - ); - }); - - it("validateOrder reverts with no context", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = constants.AddressZero; - const context: BytesLike = []; - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [ - 0, // SIP6 version - fulfiller, - expiration, - convertSignatureToEIP2098(signature), - context, - ] - ); - - await expect(contract.validateOrder(mockZoneParameter(extraData))).to.be.revertedWith("InvalidExtraData"); - }); - - it("validateOrder reverts with wrong consideration", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const context: BytesLike = ethers.utils.solidityPack(["bytes"], [constants.HashZero]); - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [ - 0, // SIP6 version - fulfiller, - expiration, - convertSignatureToEIP2098(signature), - context, - ] - ); - - await expect(contract.validateOrder(mockZoneParameter(extraData, consideration))).to.be.revertedWith( - "SubstandardViolation" - ); - }); - - it("validates correct signature with context", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - const context: BytesLike = ethers.utils.solidityPack(["bytes", "bytes[]"], [considerationHash, [orderHash]]); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - // esimate gas - // console.log( - // await contract.estimateGas.validateOrder( - // mockZoneParameter(extraData, consideration) - // ) - // ); - - expect(await contract.validateOrder(mockZoneParameter(extraData, consideration))).to.be.equal("0x17b1f942"); // ZoneInterface.validateOrder.selector - }); - - it("validateOrder reverts a valid order after expiration time passes ", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 90; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - const context: BytesLike = ethers.utils.solidityPack(["bytes", "bytes[]"], [considerationHash, [orderHash]]); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - const selector = await contract.validateOrder(mockZoneParameter(extraData, consideration)); - - expect(selector).to.equal("0x17b1f942"); // ZoneInterface.validateOrder.selector - - await advanceBlockBySeconds(900); - - expect(contract.validateOrder(mockZoneParameter(extraData, consideration))).to.be.revertedWith( - "SignatureExpired" - ); // ZoneInterface.validateOrder.selector - }); - - it("validateOrder validates correct context with multiple order hashes - equal arrays", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 90; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - const context: BytesLike = ethers.utils.solidityPack( - ["bytes", "bytes[]"], - [considerationHash, mockBulkOrderHashes()] - ); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - // gas estimation - // console.log( - // await contract.estimateGas.validateOrder( - // mockZoneParameter(extraData, consideration, mockBulkOrderHashes()) - // ) - // ); - - expect( - await contract.validateOrder(mockZoneParameter(extraData, consideration, mockBulkOrderHashes())) - ).to.be.equal("0x17b1f942"); // ZoneInterface.validateOrder.selector - }); - - it("validateOrder validates correct context with multiple order hashes - partial arrays", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 90; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - const context: BytesLike = ethers.utils.solidityPack( - ["bytes", "bytes[]"], - [considerationHash, mockBulkOrderHashes().splice(0, 2)] - ); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - expect( - await contract.validateOrder(mockZoneParameter(extraData, consideration, mockBulkOrderHashes())) - ).to.be.equal("0x17b1f942"); // ZoneInterface.validateOrder.selector - }); - - it("validateOrder reverts when not all expected order hashes are in zone parameters", async function () { - // this triggers the early break in contract's array helper - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 90; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - // context with 10 order hashes expected - const context: BytesLike = ethers.utils.solidityPack( - ["bytes", "bytes[]"], - [considerationHash, mockBulkOrderHashes()] - ); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - await expect( - contract.validateOrder( - // only 8 order hashes actually filled - mockZoneParameter(extraData, consideration, mockBulkOrderHashes().splice(0, 2)) - ) - ).to.be.revertedWith("SubstandardViolation"); - }); - - it("validateOrder reverts when not all expected order hashes are in zone parameters variation", async function () { - // this doesn't trigger the early break in contract's array helper - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 90; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - // context with 10 order hashes expected - const context: BytesLike = ethers.utils.solidityPack( - ["bytes", "bytes[]"], - [considerationHash, mockBulkOrderHashes()] - ); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - const signature = await signer._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - // remove two and add two random order hashes - const mockActualOrderHashes = mockBulkOrderHashes().splice(0, 2); - mockActualOrderHashes.push(keccak256("0x55"), keccak256("0x66")); - - await expect( - contract.validateOrder( - // only 8 order hashes actually filled - mockZoneParameter(extraData, consideration, mockActualOrderHashes) - ) - ).to.be.revertedWith("SubstandardViolation"); - }); - - it("validateOrder reverts incorrectly signed signature with context", async function () { - const orderHash = keccak256("0x1234"); - const expiration = (await getCurrentTimeStamp()) + 100; - const fulfiller = constants.AddressZero; - const consideration = mockConsideration(); - const considerationHash = ethers.utils._TypedDataEncoder.hashStruct("Consideration", CONSIDERATION_EIP712_TYPE, { - consideration, - }); - - const context: BytesLike = ethers.utils.solidityPack(["bytes"], [considerationHash]); - - const signedOrder = { - fulfiller, - expiration, - orderHash, - context, - }; - - // sign with random user - const signature = await users[4]._signTypedData( - EIP712_DOMAIN(chainId, contract.address), - SIGNED_ORDER_EIP712_TYPE, - signedOrder - ); - - const extraData = ethers.utils.solidityPack( - ["bytes1", "address", "uint64", "bytes", "bytes"], - [0, fulfiller, expiration, convertSignatureToEIP2098(signature), context] - ); - - expect(contract.validateOrder(mockZoneParameter(extraData, consideration))).to.be.revertedWith("SignerNotActive"); - }); - }); -}); - -function mockConsideration(howMany: number = 10): ReceivedItemStruct[] { - const consideration: ReceivedItemStruct[] = []; - for (let i = 0; i < howMany; i++) { - consideration.push({ - itemType: 0, - token: Wallet.createRandom().address, - identifier: 123, - amount: 12, - recipient: Wallet.createRandom().address, - }); - } - - return consideration; -} - -function mockBulkOrderHashes(howMany: number = 10): string[] { - const hashes: string[] = []; - for (let i = 0; i < howMany; i++) { - hashes.push(keccak256(`0x123${i >= 10 ? i + "0" : i}`)); - } - return hashes; -} - -function mockZoneParameter( - extraData: BytesLike, - consideration: ReceivedItemStruct[] = [], - orderHashes: string[] = [keccak256("0x1234")] -): ZoneParametersStruct { - return { - // fix order hash for testing (zone doesn't validate its actual validity) - orderHash: keccak256("0x1234"), - fulfiller: constants.AddressZero, - // zero address - also does not get validated in zone - offerer: constants.AddressZero, - // empty offer - no validation in zone - offer: [], - consideration, - extraData, - orderHashes, - startTime: 0, - endTime: 0, - // we do not use zone hash - zoneHash: constants.HashZero, - }; -} diff --git a/test/utils/DeployHybridFixtures.ts b/test/utils/DeployHybridFixtures.ts deleted file mode 100644 index f4f99b3b7..000000000 --- a/test/utils/DeployHybridFixtures.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ethers, artifacts } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { defaultAbiCoder } from "ethers/lib/utils"; -import { - OperatorAllowlist__factory, - ImmutableERC721__factory, - ImmutableERC721, - MockFactory__factory, - MockFactory, - MockMarketplace__factory, - MockMarketplace, - MockWalletFactory, - MockWalletFactory__factory, - MockEIP1271Wallet, - MockEIP1271Wallet__factory, -} from "../../typechain-types"; - -// Helper function to deploy all required contracts for Allowlist testing. Deploys: -// - ERC721 -// - Mock factory -// - Mock wallet factory -// - Allowlist registry -// - Mock market place -export const AllowlistFixture = async (owner: SignerWithAddress) => { - const operatorAllowlistFactory = (await ethers.getContractFactory("OperatorAllowlist")) as OperatorAllowlist__factory; - const operatorAllowlist = await operatorAllowlistFactory.deploy(owner.address); - // ERC721 - const erc721PresetFactory = (await ethers.getContractFactory("ImmutableERC721")) as ImmutableERC721__factory; - const erc721: ImmutableERC721 = await erc721PresetFactory.deploy( - owner.address, - "ERC721Preset", - "EP", - "https://baseURI.com/", - "https://contractURI.com", - operatorAllowlist.address, - owner.address, - ethers.BigNumber.from("200") - ); - - // Mock Wallet factory - const WalletFactory = (await ethers.getContractFactory("MockWalletFactory")) as MockWalletFactory__factory; - const walletFactory = await WalletFactory.deploy(); - - // Mock factory - const Factory = (await ethers.getContractFactory("MockFactory")) as MockFactory__factory; - const factory = await Factory.deploy(); - - // Mock market place - const mockMarketplaceFactory = (await ethers.getContractFactory("MockMarketplace")) as MockMarketplace__factory; - const marketPlace: MockMarketplace = await mockMarketplaceFactory.deploy(erc721.address); - - // Mock EIP1271 Wallet - const mockEIP1271Wallet = (await ethers.getContractFactory("MockEIP1271Wallet")) as MockEIP1271Wallet__factory; - const eip1271Wallet: MockEIP1271Wallet = await mockEIP1271Wallet.deploy(owner.address); - - return { - erc721, - walletFactory, - factory, - operatorAllowlist, - marketPlace, - eip1271Wallet, - }; -}; - -// Helper function to deploy SC wallet via CREATE2 and return deterministic address -export const walletSCFixture = async (walletDeployer: SignerWithAddress, mockWalletFactory: MockWalletFactory) => { - // Deploy the implementation contract or wallet module - const Module = await ethers.getContractFactory("MockWallet"); - - const module = await Module.connect(walletDeployer).deploy(); - - const moduleAddress = module.address; - - // Calculate salt - const salt = ethers.utils.keccak256("0x1234"); - - // Deploy wallet via factory - await mockWalletFactory.connect(walletDeployer).deploy(module.address, salt); - - const deployedAddr = await mockWalletFactory.getAddress(module.address, salt); - - return { deployedAddr, moduleAddress }; -}; - -// Helper function to return required artifacts to deploy disguised EOA via CREATE2 -export const disguidedEOAFixture = async (erc721Addr: string, MockFactory: MockFactory, saltInput: string) => { - // Encode the constructor params - const encodedParams = defaultAbiCoder.encode(["address"], [erc721Addr]).slice(2); - - // Calculate salt - const salt = ethers.utils.keccak256(saltInput); - - // Get the artifact for bytecode - const mockDisguisedEOAArtifact = await artifacts.readArtifact("MockDisguisedEOA"); - - // Append bytecode and constructor params - const constructorByteCode = `${mockDisguisedEOAArtifact.bytecode}${encodedParams}`; - - // Calulate address of deployed contract - const deployedAddr = await MockFactory.computeAddress(salt, ethers.utils.keccak256(constructorByteCode)); - - return { deployedAddr, salt, constructorByteCode }; -}; diff --git a/test/utils/DeployRegularFixtures.ts b/test/utils/DeployRegularFixtures.ts deleted file mode 100644 index 2ebb2ed3e..000000000 --- a/test/utils/DeployRegularFixtures.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ethers, artifacts } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { defaultAbiCoder } from "ethers/lib/utils"; -import { - OperatorAllowlist__factory, - ImmutableERC721MintByID__factory, - ImmutableERC721MintByID, - MockFactory__factory, - MockFactory, - MockMarketplace__factory, - MockMarketplace, - MockWalletFactory, - MockWalletFactory__factory, - MockEIP1271Wallet, - MockEIP1271Wallet__factory, -} from "../../typechain-types"; - -// Helper function to deploy all required contracts for Allowlist testing. Deploys: -// - ERC721 -// - Mock factory -// - Mock wallet factory -// - Allowlist registry -// - Mock market place -export const RegularAllowlistFixture = async (owner: SignerWithAddress) => { - const operatorAllowlistFactory = (await ethers.getContractFactory("OperatorAllowlist")) as OperatorAllowlist__factory; - const operatorAllowlist = await operatorAllowlistFactory.deploy(owner.address); - // ERC721 - const erc721PresetFactory = (await ethers.getContractFactory( - "ImmutableERC721MintByID" - )) as ImmutableERC721MintByID__factory; - const erc721: ImmutableERC721MintByID = await erc721PresetFactory.deploy( - owner.address, - "ERC721Preset", - "EP", - "https://baseURI.com/", - "https://contractURI.com", - operatorAllowlist.address, - owner.address, - ethers.BigNumber.from("200") - ); - - // Mock Wallet factory - const WalletFactory = (await ethers.getContractFactory("MockWalletFactory")) as MockWalletFactory__factory; - const walletFactory = await WalletFactory.deploy(); - - // Mock factory - const Factory = (await ethers.getContractFactory("MockFactory")) as MockFactory__factory; - const factory = await Factory.deploy(); - - // Mock market place - const mockMarketplaceFactory = (await ethers.getContractFactory("MockMarketplace")) as MockMarketplace__factory; - const marketPlace: MockMarketplace = await mockMarketplaceFactory.deploy(erc721.address); - - // Mock EIP1271 Wallet - const mockEIP1271Wallet = (await ethers.getContractFactory("MockEIP1271Wallet")) as MockEIP1271Wallet__factory; - const eip1271Wallet: MockEIP1271Wallet = await mockEIP1271Wallet.deploy(owner.address); - - return { - erc721, - walletFactory, - factory, - operatorAllowlist, - marketPlace, - eip1271Wallet, - }; -}; - -// Helper function to deploy SC wallet via CREATE2 and return deterministic address -export const walletSCFixture = async (walletDeployer: SignerWithAddress, mockWalletFactory: MockWalletFactory) => { - // Deploy the implementation contract or wallet module - const Module = await ethers.getContractFactory("MockWallet"); - - const module = await Module.connect(walletDeployer).deploy(); - - const moduleAddress = module.address; - - // Calculate salt - const salt = ethers.utils.keccak256("0x1234"); - - // Deploy wallet via factory - await mockWalletFactory.connect(walletDeployer).deploy(module.address, salt); - - const deployedAddr = await mockWalletFactory.getAddress(module.address, salt); - - return { deployedAddr, moduleAddress }; -}; - -// Helper function to return required artifacts to deploy disguised EOA via CREATE2 -export const disguidedEOAFixture = async (erc721Addr: string, MockFactory: MockFactory, saltInput: string) => { - // Encode the constructor params - const encodedParams = defaultAbiCoder.encode(["address"], [erc721Addr]).slice(2); - - // Calculate salt - const salt = ethers.utils.keccak256(saltInput); - - // Get the artifact for bytecode - const mockDisguisedEOAArtifact = await artifacts.readArtifact("MockDisguisedEOA"); - - // Append bytecode and constructor params - const constructorByteCode = `${mockDisguisedEOAArtifact.bytecode}${encodedParams}`; - - // Calulate address of deployed contract - const deployedAddr = await MockFactory.computeAddress(salt, ethers.utils.keccak256(constructorByteCode)); - - return { deployedAddr, salt, constructorByteCode }; -}; diff --git a/test/utils/proxyArtifact.json b/test/utils/proxyArtifact.json deleted file mode 100644 index 71a06c411..000000000 --- a/test/utils/proxyArtifact.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "Proxy", - "sourceName": "contracts/Proxy.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "_implementation", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "Received", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" - }, - { - "inputs": [], - "name": "PROXY_getImplementation", - "outputs": [ - { - "internalType": "address", - "name": "implementation", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } - ], - "bytecode": "0x608060405234801561001057600080fd5b5060405161029f38038061029f8339818101604052810190610032919061009e565b803055506100cb565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061006b82610040565b9050919050565b61007b81610060565b811461008657600080fd5b50565b60008151905061009881610072565b92915050565b6000602082840312156100b4576100b361003b565b5b60006100c284828501610089565b91505092915050565b6101c5806100da6000396000f3fe6080604052600436106100225760003560e01c806390611127146100a857610076565b36610076573373ffffffffffffffffffffffffffffffffffffffff16347f606834f57405380c4fb88d1f4850326ad3885f014bab3b568dfbf7a041eef73860405161006c90610113565b60405180910390a3005b60006100806100d3565b90503660008037600080366000845af43d6000803e80600081146100a3573d6000f35b3d6000fd5b3480156100b457600080fd5b506100bd6100d3565b6040516100ca9190610174565b60405180910390f35b60003054905090565b600082825260208201905092915050565b50565b60006100fd6000836100dc565b9150610108826100ed565b600082019050919050565b6000602082019050818103600083015261012c816100f0565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061015e82610133565b9050919050565b61016e81610153565b82525050565b60006020820190506101896000830184610165565b9291505056fea2646970667358221220bb5fd504ad5f375705288b7da3d7d1740cfc44f1f8c1af839177a5603158c7ea64736f6c63430008110033", - "deployedBytecode": "0x6080604052600436106100225760003560e01c806390611127146100a857610076565b36610076573373ffffffffffffffffffffffffffffffffffffffff16347f606834f57405380c4fb88d1f4850326ad3885f014bab3b568dfbf7a041eef73860405161006c90610113565b60405180910390a3005b60006100806100d3565b90503660008037600080366000845af43d6000803e80600081146100a3573d6000f35b3d6000fd5b3480156100b457600080fd5b506100bd6100d3565b6040516100ca9190610174565b60405180910390f35b60003054905090565b600082825260208201905092915050565b50565b60006100fd6000836100dc565b9150610108826100ed565b600082019050919050565b6000602082019050818103600083015261012c816100f0565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061015e82610133565b9050919050565b61016e81610153565b82525050565b60006020820190506101896000830184610165565b9291505056fea2646970667358221220bb5fd504ad5f375705288b7da3d7d1740cfc44f1f8c1af839177a5603158c7ea64736f6c63430008110033", - "linkReferences": {}, - "deployedLinkReferences": {} -}