From e6d172b55405f8f0e8d58f1f8fa22fc378db6be9 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 6 Apr 2023 10:54:22 +1200 Subject: [PATCH 01/24] Remove contracts build task as no artifacts in git --- .github/workflows/ci.yml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7df1351..4d8b53aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,28 +131,6 @@ jobs: - name: Run tests run: FOUNDRY_FUZZ_RUNS=1024 forge test -vvv - build: - name: Contracts built - runs-on: ubuntu-latest - needs: [install] - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - uses: actions/setup-node@v1 - with: - node-version: 18 - - uses: actions/cache@master - id: yarn-cache - with: - path: | - node_modules - */*/node_modules - key: ${{ runner.os }}-lerna-${{ hashFiles('**/package.json', '**/yarn.lock') }} - - run: yarn build - - run: yarn format:ts - - run: git diff --exit-code - # coverage: # name: Coverage # runs-on: ubuntu-latest From 8b911462f9e645ae22357e3e90a628c22337e63d Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 23 Mar 2023 11:19:03 +1300 Subject: [PATCH 02/24] Add ERC721FloorWrapper --- src/contracts/interfaces/IERC721.sol | 18 ++ .../interfaces/IERC721FloorWrapper.sol | 28 ++ src/contracts/mocks/ERC721Mock.sol | 23 ++ src/contracts/utils/ERC721FloorWrapper.sol | 87 ++++++ src/package.json | 1 + tests_foundry/ERC721FloorWrapper.test.sol | 268 ++++++++++++++++++ yarn.lock | 5 + 7 files changed, 430 insertions(+) create mode 100644 src/contracts/interfaces/IERC721.sol create mode 100644 src/contracts/interfaces/IERC721FloorWrapper.sol create mode 100644 src/contracts/mocks/ERC721Mock.sol create mode 100644 src/contracts/utils/ERC721FloorWrapper.sol create mode 100644 tests_foundry/ERC721FloorWrapper.test.sol diff --git a/src/contracts/interfaces/IERC721.sol b/src/contracts/interfaces/IERC721.sol new file mode 100644 index 00000000..d7dbb387 --- /dev/null +++ b/src/contracts/interfaces/IERC721.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IERC721 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + function ownerOf(uint256 tokenId) external view returns (address owner); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function getApproved(uint256 tokenId) external view returns (address operator); + function setApprovalForAll(address operator, bool _approved) external; + function isApprovedForAll(address owner, address operator) external view returns (bool); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol new file mode 100644 index 00000000..3986f0bd --- /dev/null +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; + +interface IERC721FloorWrapper is IERC1155 { + event TokensDeposited(address indexed tokenAddr, uint256[] tokenIds); + + event TokensWithdrawn(address indexed tokenAddr, uint256[] tokenIds); + + /** + * Deposit and wrap ERC-721 tokens. + * @param tokenAddr The address of the ERC-721 tokens. + * @param tokenIds The ERC-721 token ids to deposit. + * @param recipient The recipient of the wrapped tokens. + * @notice Users must first approve this contract address on the ERC-721 contract. + * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. + */ + function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient) external; + + /** + * Unwrap and withdraw ERC-721 tokens. + * @param tokenAddr The address of the ERC-721 tokens. + * @param tokenIds The ERC-721 token ids to withdraw. + * @param recipient The recipient of the unwrapped tokens. + */ + function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient) external; +} diff --git a/src/contracts/mocks/ERC721Mock.sol b/src/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000..4a909cb0 --- /dev/null +++ b/src/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +/** + * This is a test ERC721 contract with unlimited free minting. + */ +contract ERC721Mock is ERC721 { + uint256 public minted; + + constructor() ERC721("ERC721Mock", "") {} // solhint-disable-line no-empty-blocks + + /** + * Public and unlimited mint function + */ + function mintMock(address to, uint256 amount) external { + for (uint256 i; i < amount; i++) { + _safeMint(to, minted + i); + } + minted += amount; + } +} diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/utils/ERC721FloorWrapper.sol new file mode 100644 index 00000000..618ac530 --- /dev/null +++ b/src/contracts/utils/ERC721FloorWrapper.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; +import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; +import {IERC721} from "../interfaces/IERC721.sol"; +import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; + +// Errors +error UnsupportedMethod(); + +/** + * @notice Allows users to wrap any amount of any ERC-721 token with a 1:1 ratio + * of corresponding ERC-1155 tokens with native metaTransaction methods. + * Each ERC-721 within a collection is treated as if fungible. + */ +contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155, ERC1155MintBurn { + /** + * Prevent invalid method calls. + */ + fallback() external { + revert UnsupportedMethod(); + } + + /** + * Deposit and wrap ERC-721 tokens. + * @param tokenAddr The address of the ERC-721 tokens. + * @param tokenIds The ERC-721 token ids to deposit. + * @param recipient The recipient of the wrapped tokens. + * @notice Users must first approve this contract address on the ERC-721 contract. + */ + function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient) external { + for (uint256 i; i < tokenIds.length; i++) { + //FIXME Gas optimisation + // Intentionally unsafe transfer + IERC721(tokenAddr).transferFrom(msg.sender, address(this), tokenIds[i]); + } + _mint(recipient, convertAddressToUint256(tokenAddr), tokenIds.length, ""); + emit TokensDeposited(tokenAddr, tokenIds); + } + + /** + * Unwrap and withdraw ERC-721 tokens. + * @param tokenAddr The address of the ERC-721 tokens. + * @param tokenIds The ERC-721 token ids to withdraw. + * @param recipient The recipient of the unwrapped tokens. + */ + function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient) external { + _burn(msg.sender, convertAddressToUint256(tokenAddr), tokenIds.length); + for (uint256 i; i < tokenIds.length; i++) { + //FIXME Gas optimisation + IERC721(tokenAddr).safeTransferFrom(address(this), recipient, tokenIds[i]); + } + emit TokensWithdrawn(tokenAddr, tokenIds); + } + + /** + * Convert an address into a uint256. + * @param input The address to convert. + * @return output The resulting uint256. + */ + function convertAddressToUint256(address input) public pure returns (uint256 output) { + return uint256(uint160(input)); + } + + /** + * Convert a uint256 into an address. + * @param input The uint256 to convert. + * @return output The resulting address. + * @dev As uint256 is larger than address, this may result in collisions. + */ + function convertUint256ToAddress(uint256 input) external pure returns (address output) { + return address(uint160(input)); + } + + /** + * Query if a contract supports an interface. + * @param interfaceId The interfaceId to test. + * @return supported Whether the interfaceId is supported. + */ + function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId + || super.supportsInterface(interfaceId); + } +} diff --git a/src/package.json b/src/package.json index dca00f47..7f227eb5 100644 --- a/src/package.json +++ b/src/package.json @@ -28,5 +28,6 @@ "@uniswap/lib": "^4.0.1-alpha" }, "devDependencies": { + "@openzeppelin/contracts": "^4.8.2" } } diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol new file mode 100644 index 00000000..029b9ab0 --- /dev/null +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {ERC721FloorWrapper} from "src/contracts/utils/ERC721FloorWrapper.sol"; +import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; +import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; + +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; + +import {console} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +contract ERC721FloorWrapperTest is TestHelperBase { + // Redeclare events + event TokensDeposited(address indexed tokenAddr, uint256[] tokenIds); + event TokensWithdrawn(address indexed tokenAddr, uint256[] tokenIds); + + ERC721FloorWrapper private wrapper; + address private wrapperAddr; + ERC721Mock private erc721; + address private erc721Addr; + uint256 private erc721Uint256; // The Uint256 value of erc721Addr + + function setUp() external { + wrapper = new ERC721FloorWrapper(); + wrapperAddr = address(wrapper); + erc721 = new ERC721Mock(); + erc721Addr = address(erc721); + erc721Uint256 = wrapper.convertAddressToUint256(erc721Addr); + + // Give tokens + erc721.mintMock(USER, 5); + erc721.mintMock(OPERATOR, 5); + + // Approvals + vm.prank(USER); + erc721.setApprovalForAll(wrapperAddr, true); + vm.prank(OPERATOR); + erc721.setApprovalForAll(wrapperAddr, true); + } + + // + // Deposit + // + function test_deposit_happyPath() public { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = erc721.ownerOf(0) == USER ? 0 : 1; // Use 0 or 1 + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc721Addr, tokenIds); + wrapper.deposit(erc721Addr, tokenIds, USER); + + assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + } + + function test_deposit_toRecipient() public { + uint256 beforeERC1155OperatorBal = wrapper.balanceOf(OPERATOR, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc721Addr, tokenIds); + wrapper.deposit(erc721Addr, tokenIds, OPERATOR); + + assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + } + + function test_deposit_twice() external { + test_deposit_happyPath(); + test_deposit_happyPath(); + } + + function test_deposit_two() external { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc721Addr, tokenIds); + wrapper.deposit(erc721Addr, tokenIds, USER); + + assertEq(beforeERC1155UserBal + 2, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); + } + + function test_deposit_duplicateFails() external { + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 0; + + vm.prank(USER); + vm.expectRevert("ERC721: transfer from incorrect owner"); + wrapper.deposit(erc721Addr, tokenIds, USER); + } + + function test_deposit_wrongOwner() external { + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 5; + + vm.prank(USER); + vm.expectRevert("ERC721: transfer from incorrect owner"); + wrapper.deposit(erc721Addr, tokenIds, USER); + } + + function test_deposit_invalidTokenAddr() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectRevert(); + wrapper.deposit(address(1), tokenIds, USER); + } + + // + // Withdraw + // + function test_withdraw_happyPath() public withDeposit { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc721Addr, tokenIds); + wrapper.withdraw(erc721Addr, tokenIds, USER); + + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal + 1, erc721.balanceOf(USER)); + } + + function test_withdraw_toRecipient() external withDeposit { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721OperatorBal = erc721.balanceOf(OPERATOR); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc721Addr, tokenIds); + wrapper.withdraw(erc721Addr, tokenIds, OPERATOR); + + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721OperatorBal + 1, erc721.balanceOf(OPERATOR)); + } + + function test_withdraw_undepositedToken() external withDeposit { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + vm.prank(USER); + vm.expectRevert("ERC721: transfer from incorrect owner"); + wrapper.withdraw(erc721Addr, tokenIds, USER); + } + + function test_withdraw_twice() external { + test_withdraw_happyPath(); + + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721OperatorBal = erc721.balanceOf(OPERATOR); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc721Addr, tokenIds); + wrapper.withdraw(erc721Addr, tokenIds, OPERATOR); + + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721OperatorBal + 1, erc721.balanceOf(OPERATOR)); + } + + function test_withdraw_two() external withDeposit { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc721Addr, tokenIds); + wrapper.withdraw(erc721Addr, tokenIds, USER); + + assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal - 2, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal + 2, erc721.balanceOf(USER)); + } + + function test_withdraw_invalidTokenAddr() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectRevert(); + wrapper.withdraw(address(1), tokenIds, USER); + } + + function test_withdraw_insufficientBalance() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.withdraw(erc721Addr, tokenIds, USER); + } + + // + // Helpers + // + modifier withDeposit() { + uint256[] memory tokenIds = new uint256[](2); + + tokenIds[0] = 0; + tokenIds[1] = 1; + vm.prank(USER); + wrapper.deposit(erc721Addr, tokenIds, USER); + + tokenIds[0] = 5; + tokenIds[1] = 6; + vm.prank(OPERATOR); + wrapper.deposit(erc721Addr, tokenIds, OPERATOR); + _; + } + + /** + * Skip a test. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } +} diff --git a/yarn.lock b/yarn.lock index 832f136a..6cc024da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -973,6 +973,11 @@ ethers "^4.0.0-beta.1" source-map-support "^0.5.19" +"@openzeppelin/contracts@^4.8.2": + version "4.8.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.2.tgz#d815ade0027b50beb9bcca67143c6bcc3e3923d6" + integrity sha512-kEUOgPQszC0fSYWpbh2kT94ltOJwj1qfT2DWo+zVttmGmf97JZ99LspePNaeeaLhCImaHVeBbjaQFZQn7+Zc5g== + "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" From b7254c9f58ae917b64a5630646d455edbaa51474 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 23 Mar 2023 13:33:48 +1300 Subject: [PATCH 03/24] Add ERC1155FloorWrapper --- .../interfaces/IERC1155FloorWrapper.sol | 31 ++ src/contracts/interfaces/IERC721.sol | 2 +- .../interfaces/IERC721FloorWrapper.sol | 2 +- src/contracts/mocks/ERC721Mock.sol | 2 +- src/contracts/utils/AddressConverter.sol | 23 ++ src/contracts/utils/ERC1155FloorWrapper.sol | 122 ++++++ src/contracts/utils/ERC721FloorWrapper.sol | 24 +- tests_foundry/AddressConverter.test.sol | 40 ++ tests_foundry/ERC1155FloorWrapper.test.sol | 350 ++++++++++++++++++ tests_foundry/ERC721FloorWrapper.test.sol | 3 +- 10 files changed, 573 insertions(+), 26 deletions(-) create mode 100644 src/contracts/interfaces/IERC1155FloorWrapper.sol create mode 100644 src/contracts/utils/AddressConverter.sol create mode 100644 src/contracts/utils/ERC1155FloorWrapper.sol create mode 100644 tests_foundry/AddressConverter.test.sol create mode 100644 tests_foundry/ERC1155FloorWrapper.test.sol diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol new file mode 100644 index 00000000..23d42a56 --- /dev/null +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; + +interface IERC1155FloorWrapper is IERC1155 { + event TokensDeposited(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + + event TokensWithdrawn(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + + /** + * Deposit and wrap ERC-1155 tokens. + * @param tokenAddr The address of the ERC-1155 tokens. + * @param tokenIds The ERC-1155 token ids to deposit. + * @param tokenAmounts The amount of each token to deposit. + * @param recipient The recipient of the wrapped tokens. + * @notice Users must first approve this contract address on the ERC-1155 contract. + */ + function deposit(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) + external; + + /** + * Unwrap and withdraw ERC-1155 tokens. + * @param tokenAddr The address of the ERC-1155 tokens. + * @param tokenIds The ERC-1155 token ids to withdraw. + * @param tokenAmounts The amount of each token to deposit. + * @param recipient The recipient of the unwrapped tokens. + */ + function withdraw(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) + external; +} diff --git a/src/contracts/interfaces/IERC721.sol b/src/contracts/interfaces/IERC721.sol index d7dbb387..b8cb933d 100644 --- a/src/contracts/interfaces/IERC721.sol +++ b/src/contracts/interfaces/IERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.4; interface IERC721 { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index 3986f0bd..3e87e852 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.4; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; diff --git a/src/contracts/mocks/ERC721Mock.sol b/src/contracts/mocks/ERC721Mock.sol index 4a909cb0..ff97a07d 100644 --- a/src/contracts/mocks/ERC721Mock.sol +++ b/src/contracts/mocks/ERC721Mock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.4; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/src/contracts/utils/AddressConverter.sol b/src/contracts/utils/AddressConverter.sol new file mode 100644 index 00000000..3002c7e9 --- /dev/null +++ b/src/contracts/utils/AddressConverter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +contract AddressConverter { + /** + * Convert an address into a uint256. + * @param input The address to convert. + * @return output The resulting uint256. + */ + function convertAddressToUint256(address input) public pure returns (uint256 output) { + return uint256(uint160(input)); + } + + /** + * Convert a uint256 into an address. + * @param input The uint256 to convert. + * @return output The resulting address. + * @dev As uint256 is larger than address, this may result in collisions. + */ + function convertUint256ToAddress(uint256 input) public pure returns (address output) { + return address(uint160(input)); + } +} diff --git a/src/contracts/utils/ERC1155FloorWrapper.sol b/src/contracts/utils/ERC1155FloorWrapper.sol new file mode 100644 index 00000000..5ca61300 --- /dev/null +++ b/src/contracts/utils/ERC1155FloorWrapper.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol"; +import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; +import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; +import {IERC1155FloorWrapper} from "../interfaces/IERC1155FloorWrapper.sol"; +import {AddressConverter} from "./AddressConverter.sol"; + +// Errors +error UnsupportedMethod(); +error InvalidERC1155Received(); + +/** + * Allows an all token ids within an ERC-1155 contract to be wrapped and + * treated as a single ERC-1155 token id. + */ +contract ERC1155FloorWrapper is + IERC1155FloorWrapper, + ERC1155, + ERC1155MintBurn, + IERC1155TokenReceiver, + AddressConverter +{ + bool private isDepositing; + + modifier onlyDepositing() { + if (!isDepositing) { + revert InvalidERC1155Received(); + } + delete isDepositing; + _; + } + + /** + * Prevent invalid method calls. + */ + fallback() external { + revert UnsupportedMethod(); + } + + /** + * Deposit and wrap ERC-1155 tokens. + * @param tokenAddr The address of the ERC-1155 tokens. + * @param tokenIds The ERC-1155 token ids to deposit. + * @param tokenAmounts The amount of each token to deposit. + * @param recipient The recipient of the wrapped tokens. + * @notice Users must first approve this contract address on the ERC-1155 contract. + */ + function deposit(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) + external + { + isDepositing = true; + IERC1155(tokenAddr).safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); + delete isDepositing; + + uint256 total; + for (uint256 i = 0; i < tokenIds.length; i++) { + total += tokenAmounts[i]; + } + _mint(recipient, convertAddressToUint256(tokenAddr), total, ""); + + emit TokensDeposited(tokenAddr, tokenIds, tokenAmounts); + } + + /** + * Unwrap and withdraw ERC-1155 tokens. + * @param tokenAddr The address of the ERC-1155 tokens. + * @param tokenIds The ERC-1155 token ids to withdraw. + * @param tokenAmounts The amount of each token to deposit. + * @param recipient The recipient of the unwrapped tokens. + */ + function withdraw(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) + external + { + uint256 total; + for (uint256 i = 0; i < tokenIds.length; i++) { + total += tokenAmounts[i]; + } + _burn(msg.sender, convertAddressToUint256(tokenAddr), total); + + IERC1155(tokenAddr).safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, ""); + + emit TokensWithdrawn(tokenAddr, tokenIds, tokenAmounts); + } + + /** + * Handle the receipt of a single ERC-1155 token type. + * @dev This function can only be called when deposits are in progress. + */ + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + onlyDepositing + returns (bytes4) + { + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + /** + * Handle the receipt of multiple ERC-1155 token types. + * @dev This function can only be called when deposits are in progress. + */ + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + onlyDepositing + returns (bytes4) + { + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + /** + * Query if a contract supports an interface. + * @param interfaceId The interfaceId to test. + * @return supported Whether the interfaceId is supported. + */ + function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId + || super.supportsInterface(interfaceId); + } +} diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/utils/ERC721FloorWrapper.sol index 618ac530..660e9dfe 100644 --- a/src/contracts/utils/ERC721FloorWrapper.sol +++ b/src/contracts/utils/ERC721FloorWrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.4; import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; @@ -7,6 +7,7 @@ import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; import {IERC721} from "../interfaces/IERC721.sol"; import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; +import {AddressConverter} from "./AddressConverter.sol"; // Errors error UnsupportedMethod(); @@ -16,7 +17,7 @@ error UnsupportedMethod(); * of corresponding ERC-1155 tokens with native metaTransaction methods. * Each ERC-721 within a collection is treated as if fungible. */ -contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155, ERC1155MintBurn { +contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155, ERC1155MintBurn, AddressConverter { /** * Prevent invalid method calls. */ @@ -56,25 +57,6 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155, ERC1155MintBurn { emit TokensWithdrawn(tokenAddr, tokenIds); } - /** - * Convert an address into a uint256. - * @param input The address to convert. - * @return output The resulting uint256. - */ - function convertAddressToUint256(address input) public pure returns (uint256 output) { - return uint256(uint160(input)); - } - - /** - * Convert a uint256 into an address. - * @param input The uint256 to convert. - * @return output The resulting address. - * @dev As uint256 is larger than address, this may result in collisions. - */ - function convertUint256ToAddress(uint256 input) external pure returns (address output) { - return address(uint160(input)); - } - /** * Query if a contract supports an interface. * @param interfaceId The interfaceId to test. diff --git a/tests_foundry/AddressConverter.test.sol b/tests_foundry/AddressConverter.test.sol new file mode 100644 index 00000000..bea580d3 --- /dev/null +++ b/tests_foundry/AddressConverter.test.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {AddressConverter} from "src/contracts/utils/AddressConverter.sol"; + +import {Test, console} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +contract AddressConverterTest is Test { + AddressConverter private converter; + + function setUp() external { + converter = new AddressConverter(); + } + + function test_convertParity() external { + assertEq(address(0), addrUint256Addr(address(0))); + assertEq(address(1), addrUint256Addr(address(1))); + assertEq(address(2), addrUint256Addr(address(2))); + } + + // + // Helpers + // + function addrUint256Addr(address input) public view returns (address output) { + return converter.convertUint256ToAddress(converter.convertAddressToUint256(input)); + } + + /** + * Skip a test. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } +} diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol new file mode 100644 index 00000000..5fcb02d8 --- /dev/null +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {ERC1155FloorWrapper, InvalidERC1155Received} from "src/contracts/utils/ERC1155FloorWrapper.sol"; +import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; + +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; + +import {console} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +contract ERC1155FloorWrapperTest is TestHelperBase { + // Redeclare events + event TokensDeposited(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + event TokensWithdrawn(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + + ERC1155FloorWrapper private wrapper; + address private wrapperAddr; + ERC1155Mock private erc1155; + address private erc1155Addr; + uint256 private erc1155Uint256; // The Uint256 value of erc1155Addr + + function setUp() external { + wrapper = new ERC1155FloorWrapper(); + wrapperAddr = address(wrapper); + erc1155 = new ERC1155Mock(); + erc1155Addr = address(erc1155); + erc1155Uint256 = wrapper.convertAddressToUint256(erc1155Addr); + + // Give tokens + uint256[] memory tokenIds = new uint256[](3); + tokenIds[0] = 0; + tokenIds[1] = 1; + tokenIds[2] = 2; + uint256[] memory tokenAmounts = new uint256[](3); + tokenAmounts[0] = 5; + tokenAmounts[1] = 5; + tokenAmounts[2] = 5; + erc1155.batchMintMock(USER, tokenIds, tokenAmounts, ""); + erc1155.batchMintMock(OPERATOR, tokenIds, tokenAmounts, ""); + + // Approvals + vm.prank(USER); + erc1155.setApprovalForAll(wrapperAddr, true); + vm.prank(OPERATOR); + erc1155.setApprovalForAll(wrapperAddr, true); + } + + // + // Deposit + // + function test_deposit_happyPath() public { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal + 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal - 1, erc1155.balanceOf(USER, 0)); + } + + function test_deposit_toRecipient() public { + uint256 beforeWrapperOperatorBal = wrapper.balanceOf(OPERATOR, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + + assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal - 1, erc1155.balanceOf(USER, 0)); + } + + function test_deposit_twice() external { + test_deposit_happyPath(); + test_deposit_happyPath(); + } + + function test_deposit_two() external { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 2; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); + } + + function test_deposit_twoDiffTokens() external { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal0 = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal0 = erc1155.balanceOf(USER, 0); + uint256 beforeERC1155WrapperBal1 = erc1155.balanceOf(wrapperAddr, 1); + uint256 beforeERC1155UserBal1 = erc1155.balanceOf(USER, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + uint256[] memory tokenAmounts = new uint256[](2); + tokenAmounts[0] = 1; + tokenAmounts[1] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal0 + 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal0 - 1, erc1155.balanceOf(USER, 0)); + assertEq(beforeERC1155WrapperBal1 + 1, erc1155.balanceOf(wrapperAddr, 1)); + assertEq(beforeERC1155UserBal1 - 1, erc1155.balanceOf(USER, 1)); + } + + function test_deposit_wrongOwner() external { + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 5; + uint256[] memory tokenAmounts = new uint256[](2); + tokenAmounts[0] = 1; + tokenAmounts[1] = 1; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + } + + function test_deposit_invalidTokenAddr() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(); + wrapper.deposit(address(1), tokenIds, tokenAmounts, USER); + } + + // + // Withdraw + // + function test_withdraw_happyPath() public withDeposit { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal + 1, erc1155.balanceOf(USER, 0)); + } + + function test_withdraw_toRecipient() external withDeposit { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155OperatorBal = erc1155.balanceOf(OPERATOR, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155OperatorBal + 1, erc1155.balanceOf(OPERATOR, 0)); + } + + function test_withdraw_undepositedToken() external withDeposit { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 2; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + } + + function test_withdraw_twice() external { + test_withdraw_happyPath(); + + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155OperatorBal = erc1155.balanceOf(OPERATOR, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155OperatorBal + 1, erc1155.balanceOf(OPERATOR, 0)); + } + + function test_withdraw_two() external withDeposit { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 2; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal - 2, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal + 2, erc1155.balanceOf(USER, 0)); + } + + function test_withdraw_twoDiffTokens() external withDeposit { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal0 = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155WrapperBal1 = erc1155.balanceOf(wrapperAddr, 1); + uint256 beforeERC1155UserBal0 = erc1155.balanceOf(USER, 0); + uint256 beforeERC1155UserBal1 = erc1155.balanceOf(USER, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + uint256[] memory tokenAmounts = new uint256[](2); + tokenAmounts[0] = 1; + tokenAmounts[1] = 1; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + + assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal0 - 1, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155WrapperBal1 - 1, erc1155.balanceOf(wrapperAddr, 1)); + assertEq(beforeERC1155UserBal0 + 1, erc1155.balanceOf(USER, 0)); + assertEq(beforeERC1155UserBal1 + 1, erc1155.balanceOf(USER, 1)); + } + + function test_withdraw_invalidTokenAddr() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(); + wrapper.withdraw(address(1), tokenIds, tokenAmounts, USER); + } + + function test_withdraw_insufficientBalance() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + } + + // + // Transfers + // + function test_transfers_invalidTransfer() external { + vm.prank(USER); + vm.expectRevert(InvalidERC1155Received.selector); + erc1155.safeTransferFrom(USER, wrapperAddr, 0, 1, ""); + } + + // + // Helpers + // + modifier withDeposit() { + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + uint256[] memory tokenAmounts = new uint256[](2); + tokenAmounts[0] = 2; + tokenAmounts[1] = 2; + + vm.prank(USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + vm.prank(OPERATOR); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + + _; + } + + /** + * Skip a test. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } +} diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 029b9ab0..be94c6c7 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.4; import {ERC721FloorWrapper} from "src/contracts/utils/ERC721FloorWrapper.sol"; -import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; From 447496c43f3a6834025c0420378b4c7e9edd53a5 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 24 Mar 2023 11:41:41 +1300 Subject: [PATCH 04/24] Add metadata to wrappers --- src/contracts/utils/ERC1155FloorWrapper.sol | 6 +++++- src/contracts/utils/ERC721FloorWrapper.sol | 6 +++++- tests_foundry/ERC1155FloorWrapper.test.sol | 2 +- tests_foundry/ERC721FloorWrapper.test.sol | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/contracts/utils/ERC1155FloorWrapper.sol b/src/contracts/utils/ERC1155FloorWrapper.sol index 5ca61300..1e79c9aa 100644 --- a/src/contracts/utils/ERC1155FloorWrapper.sol +++ b/src/contracts/utils/ERC1155FloorWrapper.sol @@ -8,6 +8,7 @@ import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; import {IERC1155FloorWrapper} from "../interfaces/IERC1155FloorWrapper.sol"; import {AddressConverter} from "./AddressConverter.sol"; +import {ERC1155MetadataPrefix} from "./ERC1155MetadataPrefix.sol"; // Errors error UnsupportedMethod(); @@ -19,13 +20,16 @@ error InvalidERC1155Received(); */ contract ERC1155FloorWrapper is IERC1155FloorWrapper, - ERC1155, ERC1155MintBurn, + ERC1155MetadataPrefix, IERC1155TokenReceiver, AddressConverter { bool private isDepositing; + // solhint-disable-next-line no-empty-blocks + constructor(string memory _prefix, address _admin) ERC1155MetadataPrefix(_prefix, false, _admin) {} + modifier onlyDepositing() { if (!isDepositing) { revert InvalidERC1155Received(); diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/utils/ERC721FloorWrapper.sol index 660e9dfe..41b771da 100644 --- a/src/contracts/utils/ERC721FloorWrapper.sol +++ b/src/contracts/utils/ERC721FloorWrapper.sol @@ -8,6 +8,7 @@ import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC import {IERC721} from "../interfaces/IERC721.sol"; import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; import {AddressConverter} from "./AddressConverter.sol"; +import {ERC1155MetadataPrefix} from "./ERC1155MetadataPrefix.sol"; // Errors error UnsupportedMethod(); @@ -17,7 +18,10 @@ error UnsupportedMethod(); * of corresponding ERC-1155 tokens with native metaTransaction methods. * Each ERC-721 within a collection is treated as if fungible. */ -contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155, ERC1155MintBurn, AddressConverter { +contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC1155MintBurn, AddressConverter { + // solhint-disable-next-line no-empty-blocks + constructor(string memory _prefix, address _admin) ERC1155MetadataPrefix(_prefix, false, _admin) {} + /** * Prevent invalid method calls. */ diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 5fcb02d8..99691d8f 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -21,7 +21,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { uint256 private erc1155Uint256; // The Uint256 value of erc1155Addr function setUp() external { - wrapper = new ERC1155FloorWrapper(); + wrapper = new ERC1155FloorWrapper("", address(this)); wrapperAddr = address(wrapper); erc1155 = new ERC1155Mock(); erc1155Addr = address(erc1155); diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index be94c6c7..4d3d7f7e 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -21,7 +21,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { uint256 private erc721Uint256; // The Uint256 value of erc721Addr function setUp() external { - wrapper = new ERC721FloorWrapper(); + wrapper = new ERC721FloorWrapper("", address(this)); wrapperAddr = address(wrapper); erc721 = new ERC721Mock(); erc721Addr = address(erc721); From 2ff8a7fe14dc106c707213e7dd075cdf9174acf0 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 24 Mar 2023 12:27:06 +1300 Subject: [PATCH 05/24] Wrapper swap tests --- .../interfaces/IERC1155FloorWrapper.sol | 20 +- .../interfaces/IERC721FloorWrapper.sol | 6 +- src/contracts/utils/ERC1155FloorWrapper.sol | 32 ++- src/contracts/utils/ERC721FloorWrapper.sol | 12 +- tests_foundry/ERC1155FloorWrapper.test.sol | 177 +++++++++++++-- tests_foundry/ERC721FloorWrapper.test.sol | 175 +++++++++++++-- tests_foundry/NiftyswapExchange.test.sol | 206 +++++++++++++++--- tests_foundry/NiftyswapExchange20.test.sol | 130 ++++++++--- tests_foundry/WrapAndNiftySwap.test.sol | 19 +- tests_foundry/utils/Constants.test.sol | 28 ++- .../utils/Niftyswap20TestHelper.test.sol | 4 +- .../utils/NiftyswapTestHelper.test.sol | 5 +- tests_foundry/utils/TestHelperBase.test.sol | 3 +- 13 files changed, 667 insertions(+), 150 deletions(-) diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol index 23d42a56..6303cafc 100644 --- a/src/contracts/interfaces/IERC1155FloorWrapper.sol +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -14,10 +14,16 @@ interface IERC1155FloorWrapper is IERC1155 { * @param tokenIds The ERC-1155 token ids to deposit. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the wrapped tokens. + * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) - external; + function deposit( + address tokenAddr, + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + address recipient, + bytes calldata data + ) external; /** * Unwrap and withdraw ERC-1155 tokens. @@ -25,7 +31,13 @@ interface IERC1155FloorWrapper is IERC1155 { * @param tokenIds The ERC-1155 token ids to withdraw. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the unwrapped tokens. + * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) - external; + function withdraw( + address tokenAddr, + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + address recipient, + bytes calldata data + ) external; } diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index 3e87e852..a7b1b8e0 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -13,16 +13,18 @@ interface IERC721FloorWrapper is IERC1155 { * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to deposit. * @param recipient The recipient of the wrapped tokens. + * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient) external; + function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external; /** * Unwrap and withdraw ERC-721 tokens. * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to withdraw. * @param recipient The recipient of the unwrapped tokens. + * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient) external; + function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external; } diff --git a/src/contracts/utils/ERC1155FloorWrapper.sol b/src/contracts/utils/ERC1155FloorWrapper.sol index 1e79c9aa..a9f93e7e 100644 --- a/src/contracts/utils/ERC1155FloorWrapper.sol +++ b/src/contracts/utils/ERC1155FloorWrapper.sol @@ -51,22 +51,27 @@ contract ERC1155FloorWrapper is * @param tokenIds The ERC-1155 token ids to deposit. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the wrapped tokens. + * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) - external - { + function deposit( + address tokenAddr, + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + address recipient, + bytes calldata data + ) external { isDepositing = true; IERC1155(tokenAddr).safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); delete isDepositing; + emit TokensDeposited(tokenAddr, tokenIds, tokenAmounts); + uint256 total; for (uint256 i = 0; i < tokenIds.length; i++) { total += tokenAmounts[i]; } - _mint(recipient, convertAddressToUint256(tokenAddr), total, ""); - - emit TokensDeposited(tokenAddr, tokenIds, tokenAmounts); + _mint(recipient, convertAddressToUint256(tokenAddr), total, data); } /** @@ -75,19 +80,24 @@ contract ERC1155FloorWrapper is * @param tokenIds The ERC-1155 token ids to withdraw. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the unwrapped tokens. + * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient) - external - { + function withdraw( + address tokenAddr, + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + address recipient, + bytes calldata data + ) external { uint256 total; for (uint256 i = 0; i < tokenIds.length; i++) { total += tokenAmounts[i]; } _burn(msg.sender, convertAddressToUint256(tokenAddr), total); - IERC1155(tokenAddr).safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, ""); - emit TokensWithdrawn(tokenAddr, tokenIds, tokenAmounts); + + IERC1155(tokenAddr).safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, data); } /** diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/utils/ERC721FloorWrapper.sol index 41b771da..979ad298 100644 --- a/src/contracts/utils/ERC721FloorWrapper.sol +++ b/src/contracts/utils/ERC721FloorWrapper.sol @@ -34,16 +34,17 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to deposit. * @param recipient The recipient of the wrapped tokens. + * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient) external { + function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { for (uint256 i; i < tokenIds.length; i++) { //FIXME Gas optimisation // Intentionally unsafe transfer IERC721(tokenAddr).transferFrom(msg.sender, address(this), tokenIds[i]); } - _mint(recipient, convertAddressToUint256(tokenAddr), tokenIds.length, ""); emit TokensDeposited(tokenAddr, tokenIds); + _mint(recipient, convertAddressToUint256(tokenAddr), tokenIds.length, data); } /** @@ -51,14 +52,15 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to withdraw. * @param recipient The recipient of the unwrapped tokens. + * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient) external { + function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { _burn(msg.sender, convertAddressToUint256(tokenAddr), tokenIds.length); + emit TokensWithdrawn(tokenAddr, tokenIds); for (uint256 i; i < tokenIds.length; i++) { //FIXME Gas optimisation - IERC721(tokenAddr).safeTransferFrom(address(this), recipient, tokenIds[i]); + IERC721(tokenAddr).safeTransferFrom(address(this), recipient, tokenIds[i], data); } - emit TokensWithdrawn(tokenAddr, tokenIds); } /** diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 99691d8f..4af9f23c 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -3,8 +3,16 @@ pragma solidity ^0.8.4; import {ERC1155FloorWrapper, InvalidERC1155Received} from "src/contracts/utils/ERC1155FloorWrapper.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; - +import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; +import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; +import {INiftyswapExchange20} from "src/contracts/interfaces/INiftyswapExchange20.sol"; +import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; +import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; + +import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; +import {Niftyswap20TestHelper} from "./utils/Niftyswap20TestHelper.test.sol"; import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; +import {USER, OPERATOR} from "./utils/Constants.test.sol"; import {console} from "forge-std/Test.sol"; import {stdError} from "forge-std/StdError.sol"; @@ -62,7 +70,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal + 1, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -82,7 +90,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, erc1155Uint256)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -107,7 +115,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); @@ -131,7 +139,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal0 + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -150,7 +158,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); } function test_deposit_invalidTokenAddr() external { @@ -161,7 +169,89 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(); - wrapper.deposit(address(1), tokenIds, tokenAmounts, USER); + wrapper.deposit(address(1), tokenIds, tokenAmounts, USER, ""); + } + + function test_deposit_andSell() public withDeposit { + // Niftyswap + ERC1155Mock currency = new ERC1155Mock(); + address currencyAddr = address(currency); + NiftyswapFactory factory = new NiftyswapFactory(); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0); + uint256[] memory types = new uint256[](1); + types[0] = erc1155Uint256; + withERC1155Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 2; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER, 0); + + // Deposit and sell + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + vm.prank(USER); + wrapper.deposit( + erc1155Addr, + tokenIds, + sellAmounts, + exchangeAddr, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) + ); + + // After bals + assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER, 0)); + } + + function test_deposit_andSell20() public withDeposit { + // Niftyswap + ERC20TokenMock currency = new ERC20TokenMock(); + address currencyAddr = address(currency); + NiftyswapFactory20 factory = new NiftyswapFactory20(address(this)); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0, 0); + uint256[] memory types = new uint256[](1); + types[0] = erc1155Uint256; + withERC20Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 2; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); + uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER); + + // Deposit and sell + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + vm.prank(USER); + wrapper.deposit( + erc1155Addr, + tokenIds, + sellAmounts, + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], new address[](0), new uint256[](0), block.timestamp) + ); + + // After bals + assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); + assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); } // @@ -180,7 +270,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -200,7 +290,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -215,7 +305,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); } function test_withdraw_twice() external { @@ -233,7 +323,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -253,7 +343,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal - 2, erc1155.balanceOf(wrapperAddr, 0)); @@ -277,7 +367,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); assertEq(beforeERC1155WrapperBal0 - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -294,7 +384,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(); - wrapper.withdraw(address(1), tokenIds, tokenAmounts, USER); + wrapper.withdraw(address(1), tokenIds, tokenAmounts, USER, ""); } function test_withdraw_insufficientBalance() external { @@ -305,7 +395,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); } // @@ -329,13 +419,66 @@ contract ERC1155FloorWrapperTest is TestHelperBase { tokenAmounts[1] = 2; vm.prank(USER); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); vm.prank(OPERATOR); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR); + wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); _; } + function withERC1155Liquidity(ERC1155Mock erc1155Mock, address exchangeAddr, uint256[] memory types) private { + // Mint tokens + erc1155Mock.mintMock(OPERATOR, 0, 1000000001, ""); + erc1155Mock.mintMock(USER, 0, 1000000001, ""); + + // Approvals + vm.prank(OPERATOR); + erc1155Mock.setApprovalForAll(exchangeAddr, true); + vm.prank(USER); + erc1155Mock.setApprovalForAll(exchangeAddr, true); + + // Liquidity + uint256[] memory currencyToAdd = new uint256[](1); + currencyToAdd[0] = 1000000001; + uint256[] memory tokensToAdd = new uint256[](1); + tokensToAdd[0] = 2; + vm.prank(OPERATOR); + wrapper.safeBatchTransferFrom( + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) + ); + } + + function withERC20Liquidity(ERC20TokenMock currency, address exchangeAddr, uint256[] memory types) private { + uint256 tokenAmt = 1000000001; + // Mint tokens + currency.mockMint(OPERATOR, tokenAmt); + currency.mockMint(USER, tokenAmt); + + // Approvals + vm.prank(OPERATOR); + currency.approve(exchangeAddr, tokenAmt); + vm.prank(USER); + currency.approve(exchangeAddr, tokenAmt); + + // Liquidity + uint256[] memory currencyToAdd = new uint256[](1); + currencyToAdd[0] = tokenAmt; + uint256[] memory tokensToAdd = new uint256[](1); + tokensToAdd[0] = 2; + vm.prank(OPERATOR); + wrapper.safeBatchTransferFrom( + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) + ); + } + /** * Skip a test. */ diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 4d3d7f7e..15f012a6 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -3,7 +3,16 @@ pragma solidity ^0.8.4; import {ERC721FloorWrapper} from "src/contracts/utils/ERC721FloorWrapper.sol"; import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; - +import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; +import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; +import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; +import {INiftyswapExchange20} from "src/contracts/interfaces/INiftyswapExchange20.sol"; +import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; +import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; + +import {USER, OPERATOR} from "./utils/Constants.test.sol"; +import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; +import {Niftyswap20TestHelper} from "./utils/Niftyswap20TestHelper.test.sol"; import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; import {console} from "forge-std/Test.sol"; @@ -18,7 +27,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { address private wrapperAddr; ERC721Mock private erc721; address private erc721Addr; - uint256 private erc721Uint256; // The Uint256 value of erc721Addr + uint256 private erc721Uint256; // Uint256 value of erc721Addr function setUp() external { wrapper = new ERC721FloorWrapper("", address(this)); @@ -52,7 +61,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc721Addr, tokenIds); - wrapper.deposit(erc721Addr, tokenIds, USER); + wrapper.deposit(erc721Addr, tokenIds, USER, ""); assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); @@ -70,7 +79,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc721Addr, tokenIds); - wrapper.deposit(erc721Addr, tokenIds, OPERATOR); + wrapper.deposit(erc721Addr, tokenIds, OPERATOR, ""); assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, erc721Uint256)); assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); @@ -94,7 +103,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc721Addr, tokenIds); - wrapper.deposit(erc721Addr, tokenIds, USER); + wrapper.deposit(erc721Addr, tokenIds, USER, ""); assertEq(beforeERC1155UserBal + 2, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); @@ -108,7 +117,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.deposit(erc721Addr, tokenIds, USER); + wrapper.deposit(erc721Addr, tokenIds, USER, ""); } function test_deposit_wrongOwner() external { @@ -118,7 +127,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.deposit(erc721Addr, tokenIds, USER); + wrapper.deposit(erc721Addr, tokenIds, USER, ""); } function test_deposit_invalidTokenAddr() external { @@ -127,7 +136,86 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(); - wrapper.deposit(address(1), tokenIds, USER); + wrapper.deposit(address(1), tokenIds, USER, ""); + } + + function test_deposit_andSell() public withDeposit { + // Niftyswap + ERC1155Mock currency = new ERC1155Mock(); + address currencyAddr = address(currency); + NiftyswapFactory factory = new NiftyswapFactory(); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0); + uint256[] memory types = new uint256[](1); + types[0] = erc721Uint256; + withERC1155Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 2; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER, 0); + + // Deposit and sell + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 2; + tokenIds[1] = 3; + vm.prank(USER); + wrapper.deposit( + erc721Addr, tokenIds, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) + ); + + // After bals + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER, 0)); + } + + function test_deposit_andSell20() public withDeposit { + // Niftyswap + ERC20TokenMock currency = new ERC20TokenMock(); + address currencyAddr = address(currency); + NiftyswapFactory20 factory = new NiftyswapFactory20(address(this)); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0, 0); + uint256[] memory types = new uint256[](1); + types[0] = erc721Uint256; + withERC20Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 2; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER); + + // Deposit and sell + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 2; + tokenIds[1] = 3; + vm.prank(USER); + wrapper.deposit( + erc721Addr, + tokenIds, + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], new address[](0), new uint256[](0), block.timestamp) + ); + + // After bals + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); } // @@ -144,7 +232,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, USER); + wrapper.withdraw(erc721Addr, tokenIds, USER, ""); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); @@ -162,7 +250,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, OPERATOR); + wrapper.withdraw(erc721Addr, tokenIds, OPERATOR, ""); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); @@ -175,7 +263,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.withdraw(erc721Addr, tokenIds, USER); + wrapper.withdraw(erc721Addr, tokenIds, USER, ""); } function test_withdraw_twice() external { @@ -191,7 +279,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, OPERATOR); + wrapper.withdraw(erc721Addr, tokenIds, OPERATOR, ""); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); @@ -210,7 +298,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, USER); + wrapper.withdraw(erc721Addr, tokenIds, USER, ""); assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal - 2, erc721.balanceOf(wrapperAddr)); @@ -223,7 +311,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(); - wrapper.withdraw(address(1), tokenIds, USER); + wrapper.withdraw(address(1), tokenIds, USER, ""); } function test_withdraw_insufficientBalance() external { @@ -232,7 +320,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc721Addr, tokenIds, USER); + wrapper.withdraw(erc721Addr, tokenIds, USER, ""); } // @@ -244,15 +332,68 @@ contract ERC721FloorWrapperTest is TestHelperBase { tokenIds[0] = 0; tokenIds[1] = 1; vm.prank(USER); - wrapper.deposit(erc721Addr, tokenIds, USER); + wrapper.deposit(erc721Addr, tokenIds, USER, ""); tokenIds[0] = 5; tokenIds[1] = 6; vm.prank(OPERATOR); - wrapper.deposit(erc721Addr, tokenIds, OPERATOR); + wrapper.deposit(erc721Addr, tokenIds, OPERATOR, ""); _; } + function withERC1155Liquidity(ERC1155Mock erc1155Mock, address exchangeAddr, uint256[] memory types) private { + // Mint tokens + erc1155Mock.mintMock(OPERATOR, 0, 1000000001, ""); + erc1155Mock.mintMock(USER, 0, 1000000001, ""); + + // Approvals + vm.prank(OPERATOR); + erc1155Mock.setApprovalForAll(exchangeAddr, true); + vm.prank(USER); + erc1155Mock.setApprovalForAll(exchangeAddr, true); + + // Liquidity + uint256[] memory currencyToAdd = new uint256[](1); + currencyToAdd[0] = 1000000001; + uint256[] memory tokensToAdd = new uint256[](1); + tokensToAdd[0] = 2; + vm.prank(OPERATOR); + wrapper.safeBatchTransferFrom( + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) + ); + } + + function withERC20Liquidity(ERC20TokenMock currency, address exchangeAddr, uint256[] memory types) private { + uint256 tokenAmt = 1000000001; + // Mint tokens + currency.mockMint(OPERATOR, tokenAmt); + currency.mockMint(USER, tokenAmt); + + // Approvals + vm.prank(OPERATOR); + currency.approve(exchangeAddr, tokenAmt); + vm.prank(USER); + currency.approve(exchangeAddr, tokenAmt); + + // Liquidity + uint256[] memory currencyToAdd = new uint256[](1); + currencyToAdd[0] = tokenAmt; + uint256[] memory tokensToAdd = new uint256[](1); + tokensToAdd[0] = 2; + vm.prank(OPERATOR); + wrapper.safeBatchTransferFrom( + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) + ); + } + /** * Skip a test. */ diff --git a/tests_foundry/NiftyswapExchange.test.sol b/tests_foundry/NiftyswapExchange.test.sol index 2235ebf3..5eeee940 100644 --- a/tests_foundry/NiftyswapExchange.test.sol +++ b/tests_foundry/NiftyswapExchange.test.sol @@ -9,13 +9,15 @@ import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.so import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; +import {USER, OPERATOR} from "./utils/Constants.test.sol"; +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; import {console} from "forge-std/Test.sol"; import {stdError} from "forge-std/StdError.sol"; interface IERC1155Exchange is INiftyswapExchange, IERC1155 {} -contract NiftyswapExchangeTest is NiftyswapTestHelper { +contract NiftyswapExchangeTest is TestHelperBase { // Events can't be imported // IERC1155 event TransferSingle( @@ -151,7 +153,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { emit LiquidityAdded(OPERATOR, types, tokensToAdd, currencyToAdd); vm.prank(OPERATOR); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); // Check balances @@ -188,7 +194,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { // Add liq vm.prank(OPERATOR); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); // Reserves rounds up @@ -220,7 +230,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.expectRevert("NE#23"); // Using contract A instead of B erc1155AMock.safeBatchTransferFrom( - OPERATOR, excAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + excAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -240,7 +254,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#12"); erc1155BMock.safeBatchTransferFrom( - OPERATOR, excAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + excAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -255,7 +273,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#09"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp - 1) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp - 1) ); } @@ -273,7 +295,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#10"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -291,7 +317,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#11"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -315,15 +345,27 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.startPrank(OPERATOR); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types1, tokens2, encodeAddLiquidity(currencies2, block.timestamp) + OPERATOR, + exchangeAddr, + types1, + tokens2, + NiftyswapTestHelper.encodeAddLiquidity(currencies2, block.timestamp) ); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens1, encodeAddLiquidity(currencies2, block.timestamp) + OPERATOR, + exchangeAddr, + types2, + tokens1, + NiftyswapTestHelper.encodeAddLiquidity(currencies2, block.timestamp) ); vm.expectRevert(stdError.indexOOBError); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens2, encodeAddLiquidity(currencies1, block.timestamp) + OPERATOR, + exchangeAddr, + types2, + tokens2, + NiftyswapTestHelper.encodeAddLiquidity(currencies1, block.timestamp) ); vm.stopPrank(); } @@ -342,7 +384,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#29"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -357,7 +403,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#13"); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + NiftyswapTestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -375,7 +425,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert(stdError.arithmeticError); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -392,7 +446,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#16"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, liquidity, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + liquidity, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -410,7 +468,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, liquidity, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + liquidity, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); // Check balances @@ -428,7 +490,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#17"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -443,7 +509,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#18"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -461,7 +531,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); vm.expectRevert("NE#29"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -485,11 +559,19 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.startPrank(OPERATOR); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types1, tokens2, encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types1, + tokens2, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) ); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens1, encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types2, + tokens1, + NiftyswapTestHelper.encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) ); vm.stopPrank(); } @@ -516,7 +598,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.expectEmit(true, true, true, true, exchangeAddr); emit CurrencyPurchase(USER, USER, types, sellAmounts, prices); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) ); // Check balances @@ -540,7 +626,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#06"); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) ); } @@ -556,7 +646,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) ); } @@ -570,7 +664,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#05"); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp - 1) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp - 1) ); } @@ -585,7 +683,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#07"); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) ); } @@ -601,7 +703,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#05"); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types, sellAmounts, encodeSellTokens(USER, prices[0], block.timestamp - 1) + USER, + exchangeAddr, + types, + sellAmounts, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp - 1) ); } @@ -618,7 +724,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.startPrank(USER); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); erc1155AMock.safeBatchTransferFrom( - USER, exchangeAddr, types1, sellAmounts2, encodeSellTokens(USER, prices[0], block.timestamp - 1) + USER, + exchangeAddr, + types1, + sellAmounts2, + NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp - 1) ); vm.stopPrank(); } @@ -645,7 +755,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.expectEmit(true, true, true, true, exchangeAddr); emit TokensPurchase(USER, USER, types, tokens, prices); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp) ); // Check balances @@ -669,7 +783,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#03"); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp) ); } @@ -686,7 +804,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp) ); } @@ -700,7 +822,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#02"); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp - 1) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp - 1) ); } @@ -715,7 +841,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp) ); } @@ -731,7 +861,11 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(USER); vm.expectRevert("NE#29"); erc1155BMock.safeTransferFrom( - USER, exchangeAddr, CURRENCY_ID, prices[0], encodeBuyTokens(USER, types, tokens, block.timestamp) + USER, + exchangeAddr, + CURRENCY_ID, + prices[0], + NiftyswapTestHelper.encodeBuyTokens(USER, types, tokens, block.timestamp) ); } @@ -748,7 +882,7 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { vm.prank(OPERATOR); erc1155AMock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeAddLiquidity(currencies, block.timestamp) + OPERATOR, exchangeAddr, types, tokens, NiftyswapTestHelper.encodeAddLiquidity(currencies, block.timestamp) ); vm.expectRevert(stdError.divisionError); @@ -765,7 +899,7 @@ contract NiftyswapExchangeTest is NiftyswapTestHelper { exchangeAddr, TOKEN_TYPES, TOKEN_AMTS_TO_ADD, - encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) + NiftyswapTestHelper.encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) ); _; } diff --git a/tests_foundry/NiftyswapExchange20.test.sol b/tests_foundry/NiftyswapExchange20.test.sol index 35891446..87778793 100644 --- a/tests_foundry/NiftyswapExchange20.test.sol +++ b/tests_foundry/NiftyswapExchange20.test.sol @@ -10,13 +10,15 @@ import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol" import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; +import {USER, OPERATOR} from "./utils/Constants.test.sol"; +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; import {Niftyswap20TestHelper} from "./utils/Niftyswap20TestHelper.test.sol"; import {console} from "forge-std/Test.sol"; import {stdError} from "forge-std/StdError.sol"; interface IERC1155Exchange is INiftyswapExchange20, IERC1155 {} -contract NiftyswapExchange20Test is Niftyswap20TestHelper { +contract NiftyswapExchange20Test is TestHelperBase { // Events can't be imported // INiftyswapExchange20 event TokensPurchase( @@ -156,7 +158,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { emit LiquidityAdded(OPERATOR, types, tokensToAdd, currencyToAdd); vm.prank(OPERATOR); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); // Check balances @@ -193,7 +199,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { // Add liq vm.prank(OPERATOR); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); // Reserves rounds up @@ -221,7 +231,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#10"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp - 1) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp - 1) ); } @@ -239,7 +253,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#11"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -257,7 +275,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#12"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -281,15 +303,27 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.startPrank(OPERATOR); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types1, tokens2, encodeAddLiquidity(currencies2, block.timestamp) + OPERATOR, + exchangeAddr, + types1, + tokens2, + Niftyswap20TestHelper.encodeAddLiquidity(currencies2, block.timestamp) ); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens1, encodeAddLiquidity(currencies2, block.timestamp) + OPERATOR, + exchangeAddr, + types2, + tokens1, + Niftyswap20TestHelper.encodeAddLiquidity(currencies2, block.timestamp) ); vm.expectRevert(stdError.indexOOBError); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens2, encodeAddLiquidity(currencies1, block.timestamp) + OPERATOR, + exchangeAddr, + types2, + tokens2, + Niftyswap20TestHelper.encodeAddLiquidity(currencies1, block.timestamp) ); vm.stopPrank(); } @@ -308,7 +342,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#32"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -323,7 +361,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#13"); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokensToAdd, encodeAddLiquidity(currencyToAdd, block.timestamp) + OPERATOR, + exchangeAddr, + types, + tokensToAdd, + Niftyswap20TestHelper.encodeAddLiquidity(currencyToAdd, block.timestamp) ); } @@ -341,7 +383,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert(stdError.arithmeticError); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -358,7 +404,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#16"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, liquidity, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + liquidity, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -376,7 +426,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, liquidity, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + liquidity, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); // Check balances @@ -394,7 +448,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#17"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -409,7 +467,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#18"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -427,7 +489,11 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); vm.expectRevert("NE20#32"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types, + tokens, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies, tokens, block.timestamp + 1) ); } @@ -451,11 +517,19 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.startPrank(OPERATOR); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types1, tokens2, encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types1, + tokens2, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) ); vm.expectRevert("ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); exchange.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types2, tokens1, encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) + OPERATOR, + exchangeAddr, + types2, + tokens1, + Niftyswap20TestHelper.encodeRemoveLiquidity(currencies2, tokens2, block.timestamp + 1) ); vm.stopPrank(); } @@ -486,7 +560,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) ); // Check balances @@ -514,7 +588,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) ); } @@ -534,7 +608,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) ); } @@ -552,7 +626,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp - 1) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp - 1) ); } @@ -571,7 +645,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) ); } @@ -591,7 +665,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types, sellAmounts, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp) ); } @@ -612,7 +686,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, types1, sellAmounts2, - encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp - 1) + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], NO_ADDRESSES, NO_FEES, block.timestamp - 1) ); vm.stopPrank(); } @@ -743,7 +817,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { vm.prank(OPERATOR); erc1155Mock.safeBatchTransferFrom( - OPERATOR, exchangeAddr, types, tokens, encodeAddLiquidity(currencies, block.timestamp) + OPERATOR, exchangeAddr, types, tokens, Niftyswap20TestHelper.encodeAddLiquidity(currencies, block.timestamp) ); vm.expectRevert(stdError.divisionError); @@ -760,7 +834,7 @@ contract NiftyswapExchange20Test is Niftyswap20TestHelper { exchangeAddr, TOKEN_TYPES, TOKEN_AMTS_TO_ADD, - encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) + Niftyswap20TestHelper.encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) ); _; } diff --git a/tests_foundry/WrapAndNiftySwap.test.sol b/tests_foundry/WrapAndNiftySwap.test.sol index b20598c7..35936f85 100644 --- a/tests_foundry/WrapAndNiftySwap.test.sol +++ b/tests_foundry/WrapAndNiftySwap.test.sol @@ -8,10 +8,12 @@ import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; import {ERC20WrapperMock} from "src/contracts/mocks/ERC20WrapperMock.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; +import {USER, OPERATOR} from "./utils/Constants.test.sol"; import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; import {console} from "forge-std/Test.sol"; -contract WrapAndNiftySwapTest is NiftyswapTestHelper { +contract WrapAndNiftySwapTest is TestHelperBase { // Events can't be imported event NewExchange( address indexed token, address indexed currency, uint256 indexed salt, uint256 lpFee, address exchange @@ -82,7 +84,7 @@ contract WrapAndNiftySwapTest is NiftyswapTestHelper { exchangeAddr, TOKEN_TYPES, TOKEN_AMTS_TO_ADD, - encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) + NiftyswapTestHelper.encodeAddLiquidity(CURRENCIES_PER_TYPE, block.timestamp) ); } @@ -96,8 +98,9 @@ contract WrapAndNiftySwapTest is NiftyswapTestHelper { uint256[] memory exchangeBalBefore = getBalances(exchangeAddr, TOKEN_TYPES, erc1155); uint256[] memory userBalBefore = getBalances(USER, TOKEN_TYPES, erc1155); - // Encode request - bytes memory data = encodeBuyTokens(address(0), TOKEN_TYPES, TOKENS_TO_SWAP, block.timestamp); + // NiftyswapTestHelper.encode request + bytes memory data = + NiftyswapTestHelper.encodeBuyTokens(address(0), TOKEN_TYPES, TOKENS_TO_SWAP, block.timestamp); uint256[] memory costs = exchange.getPrice_currencyToToken(TOKEN_TYPES, TOKENS_TO_SWAP); uint256 total = getTotal(costs); @@ -122,7 +125,7 @@ contract WrapAndNiftySwapTest is NiftyswapTestHelper { } function test_wrapAndSwap_badRecipient() external { - bytes memory data = encodeBuyTokens(USER, TOKEN_TYPES, TOKENS_TO_SWAP, block.timestamp); + bytes memory data = NiftyswapTestHelper.encodeBuyTokens(USER, TOKEN_TYPES, TOKENS_TO_SWAP, block.timestamp); uint256[] memory costs = exchange.getPrice_currencyToToken(TOKEN_TYPES, TOKENS_TO_SWAP); uint256 total = getTotal(costs); @@ -140,10 +143,10 @@ contract WrapAndNiftySwapTest is NiftyswapTestHelper { uint256[] memory exchangeBalBefore = getBalances(exchangeAddr, TOKEN_TYPES, erc1155); uint256[] memory userBalBefore = getBalances(USER, TOKEN_TYPES, erc1155); - // Encode request + // NiftyswapTestHelper.encode request uint256[] memory costs = exchange.getPrice_tokenToCurrency(TOKEN_TYPES, TOKENS_TO_SWAP); uint256 total = getTotal(costs); - bytes memory data = encodeSellTokens(address(0), total, block.timestamp); + bytes memory data = NiftyswapTestHelper.encodeSellTokens(address(0), total, block.timestamp); // Make request vm.prank(USER); @@ -168,7 +171,7 @@ contract WrapAndNiftySwapTest is NiftyswapTestHelper { function test_swapAndUnwrap_badRecipient() external { uint256[] memory costs = exchange.getPrice_tokenToCurrency(TOKEN_TYPES, TOKENS_TO_SWAP); uint256 total = getTotal(costs); - bytes memory data = encodeSellTokens(USER, total, block.timestamp); + bytes memory data = NiftyswapTestHelper.encodeSellTokens(USER, total, block.timestamp); // Make request vm.prank(USER); diff --git a/tests_foundry/utils/Constants.test.sol b/tests_foundry/utils/Constants.test.sol index d558d281..dedbfccd 100644 --- a/tests_foundry/utils/Constants.test.sol +++ b/tests_foundry/utils/Constants.test.sol @@ -1,20 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -contract Constants { - // Niftyswap Exchange - bytes4 public constant BUYTOKENS_SIG = 0xb2d81047; - bytes4 public constant SELLTOKENS_SIG = 0xdb08ec97; - bytes4 public constant ADDLIQUIDITY_SIG = 0x82da2b73; - bytes4 public constant REMOVELIQUIDITY_SIG = 0x5c0bf259; - bytes4 public constant DEPOSIT_SIG = 0xc8c323f9; +// Niftyswap Exchange +bytes4 constant BUYTOKENS_SIG = 0xb2d81047; +bytes4 constant SELLTOKENS_SIG = 0xdb08ec97; +bytes4 constant ADDLIQUIDITY_SIG = 0x82da2b73; +bytes4 constant REMOVELIQUIDITY_SIG = 0x5c0bf259; +bytes4 constant DEPOSIT_SIG = 0xc8c323f9; - // Niftyswap Exchange 20 - bytes4 internal constant SELLTOKENS20_SIG = 0xade79c7a; - bytes4 internal constant ADDLIQUIDITY20_SIG = 0x82da2b73; - bytes4 internal constant REMOVELIQUIDITY20_SIG = 0x5c0bf259; - bytes4 internal constant DEPOSIT20_SIG = 0xc8c323f9; +// Niftyswap Exchange 20 +bytes4 constant SELLTOKENS20_SIG = 0xade79c7a; +bytes4 constant ADDLIQUIDITY20_SIG = 0x82da2b73; +bytes4 constant REMOVELIQUIDITY20_SIG = 0x5c0bf259; +bytes4 constant DEPOSIT20_SIG = 0xc8c323f9; - address internal constant OPERATOR = address(1); - address internal constant USER = address(2); -} +address constant OPERATOR = address(1); +address constant USER = address(2); diff --git a/tests_foundry/utils/Niftyswap20TestHelper.test.sol b/tests_foundry/utils/Niftyswap20TestHelper.test.sol index c20b0923..d7c4f110 100644 --- a/tests_foundry/utils/Niftyswap20TestHelper.test.sol +++ b/tests_foundry/utils/Niftyswap20TestHelper.test.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.4; import {INiftyswapExchange20} from "src/contracts/interfaces/INiftyswapExchange20.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; -import {TestHelperBase} from "./TestHelperBase.test.sol"; +import {ADDLIQUIDITY20_SIG, REMOVELIQUIDITY20_SIG, SELLTOKENS20_SIG} from "./Constants.test.sol"; -abstract contract Niftyswap20TestHelper is TestHelperBase { +library Niftyswap20TestHelper { // // Niftyswap20 data encodings // diff --git a/tests_foundry/utils/NiftyswapTestHelper.test.sol b/tests_foundry/utils/NiftyswapTestHelper.test.sol index 911b617e..80f05420 100644 --- a/tests_foundry/utils/NiftyswapTestHelper.test.sol +++ b/tests_foundry/utils/NiftyswapTestHelper.test.sol @@ -3,10 +3,9 @@ pragma solidity ^0.8.4; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {ADDLIQUIDITY_SIG, REMOVELIQUIDITY_SIG, BUYTOKENS_SIG, SELLTOKENS_SIG} from "./Constants.test.sol"; -import {TestHelperBase} from "./TestHelperBase.test.sol"; - -abstract contract NiftyswapTestHelper is TestHelperBase { +library NiftyswapTestHelper { // // Niftyswap data encodings // diff --git a/tests_foundry/utils/TestHelperBase.test.sol b/tests_foundry/utils/TestHelperBase.test.sol index 2c83a23b..d33b59a3 100644 --- a/tests_foundry/utils/TestHelperBase.test.sol +++ b/tests_foundry/utils/TestHelperBase.test.sol @@ -4,10 +4,9 @@ pragma solidity ^0.8.4; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; -import {Constants} from "./Constants.test.sol"; import {Test} from "forge-std/Test.sol"; -abstract contract TestHelperBase is Test, Constants { +abstract contract TestHelperBase is Test { /** * Get token balances. */ From 2586b1be34fd2edbab5c7396b65eef78e2738fe4 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 29 Mar 2023 10:12:59 +1300 Subject: [PATCH 06/24] Gas optimisation for ERC721 Wrapper --- src/contracts/utils/ERC721FloorWrapper.sol | 16 ++++++++++---- tests_foundry/ERC721FloorWrapper.test.sol | 19 +++++++++++----- tests_foundry/utils/GasHelper.test.sol | 24 +++++++++++++++++++++ tests_foundry/utils/TestHelperBase.test.sol | 3 ++- 4 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 tests_foundry/utils/GasHelper.test.sol diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/utils/ERC721FloorWrapper.sol index 979ad298..768d0fad 100644 --- a/src/contracts/utils/ERC721FloorWrapper.sol +++ b/src/contracts/utils/ERC721FloorWrapper.sol @@ -38,10 +38,14 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 * @notice Users must first approve this contract address on the ERC-721 contract. */ function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { - for (uint256 i; i < tokenIds.length; i++) { - //FIXME Gas optimisation + uint256 length = tokenIds.length; + for (uint256 i; i < length;) { // Intentionally unsafe transfer IERC721(tokenAddr).transferFrom(msg.sender, address(this), tokenIds[i]); + unchecked { + // Can never overflow + ++i; + } } emit TokensDeposited(tokenAddr, tokenIds); _mint(recipient, convertAddressToUint256(tokenAddr), tokenIds.length, data); @@ -57,9 +61,13 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { _burn(msg.sender, convertAddressToUint256(tokenAddr), tokenIds.length); emit TokensWithdrawn(tokenAddr, tokenIds); - for (uint256 i; i < tokenIds.length; i++) { - //FIXME Gas optimisation + uint256 length = tokenIds.length; + for (uint256 i; i < length;) { IERC721(tokenAddr).safeTransferFrom(address(this), recipient, tokenIds[i], data); + unchecked { + // Can never overflow + ++i; + } } } diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 15f012a6..14c695d6 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -61,7 +61,9 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc721Addr, tokenIds); + startMeasuringGas("Deposit 1"); wrapper.deposit(erc721Addr, tokenIds, USER, ""); + stopMeasuringGas(); assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); @@ -91,23 +93,28 @@ contract ERC721FloorWrapperTest is TestHelperBase { test_deposit_happyPath(); } - function test_deposit_two() external { + function test_deposit_five() external { uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); - uint256[] memory tokenIds = new uint256[](2); + uint256[] memory tokenIds = new uint256[](5); tokenIds[0] = 0; tokenIds[1] = 1; + tokenIds[2] = 2; + tokenIds[3] = 3; + tokenIds[4] = 4; vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(erc721Addr, tokenIds); + startMeasuringGas("Deposit 5"); wrapper.deposit(erc721Addr, tokenIds, USER, ""); + stopMeasuringGas(); - assertEq(beforeERC1155UserBal + 2, wrapper.balanceOf(USER, erc721Uint256)); - assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); - assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); + assertEq(beforeERC1155UserBal + 5, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC721WrapperBal + 5, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 5, erc721.balanceOf(USER)); } function test_deposit_duplicateFails() external { @@ -232,7 +239,9 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(erc721Addr, tokenIds); + startMeasuringGas("Withdraw 1"); wrapper.withdraw(erc721Addr, tokenIds, USER, ""); + stopMeasuringGas(); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); diff --git a/tests_foundry/utils/GasHelper.test.sol b/tests_foundry/utils/GasHelper.test.sol new file mode 100644 index 00000000..7e800c84 --- /dev/null +++ b/tests_foundry/utils/GasHelper.test.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {console} from "forge-std/Test.sol"; + +abstract contract GasHelper { + string private checkpointLabel; + uint256 private checkpointGasLeft = 1; // Start the slot warm. + + function startMeasuringGas(string memory label) internal virtual { + checkpointLabel = label; + checkpointGasLeft = gasleft(); + } + + function stopMeasuringGas() internal virtual { + uint256 checkpointGasLeft2 = gasleft(); + + // Subtract 100 to account for the warm SLOAD in startMeasuringGas. + uint256 gasDelta = checkpointGasLeft - checkpointGasLeft2 - 100; + + // solhint-disable-next-line no-console + console.log(checkpointLabel, "= Gas", gasDelta); + } +} diff --git a/tests_foundry/utils/TestHelperBase.test.sol b/tests_foundry/utils/TestHelperBase.test.sol index d33b59a3..5a3a2780 100644 --- a/tests_foundry/utils/TestHelperBase.test.sol +++ b/tests_foundry/utils/TestHelperBase.test.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.4; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {GasHelper} from "./GasHelper.test.sol"; import {Test} from "forge-std/Test.sol"; -abstract contract TestHelperBase is Test { +abstract contract TestHelperBase is Test, GasHelper { /** * Get token balances. */ From 466521b87f38150178192b121a86ecab8ea480a1 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 29 Mar 2023 13:44:02 +1300 Subject: [PATCH 07/24] Move wrappers to subfolder --- src/contracts/{utils => wrappers}/ERC1155FloorWrapper.sol | 4 ++-- src/contracts/{utils => wrappers}/ERC721FloorWrapper.sol | 4 ++-- src/contracts/{utils => wrappers}/WrapAndNiftyswap.sol | 0 tests_foundry/ERC1155FloorWrapper.test.sol | 2 +- tests_foundry/ERC721FloorWrapper.test.sol | 2 +- tests_foundry/WrapAndNiftySwap.test.sol | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/contracts/{utils => wrappers}/ERC1155FloorWrapper.sol (97%) rename src/contracts/{utils => wrappers}/ERC721FloorWrapper.sol (96%) rename src/contracts/{utils => wrappers}/WrapAndNiftyswap.sol (100%) diff --git a/src/contracts/utils/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol similarity index 97% rename from src/contracts/utils/ERC1155FloorWrapper.sol rename to src/contracts/wrappers/ERC1155FloorWrapper.sol index a9f93e7e..88f95f28 100644 --- a/src/contracts/utils/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -7,8 +7,8 @@ import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/I import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; import {IERC1155FloorWrapper} from "../interfaces/IERC1155FloorWrapper.sol"; -import {AddressConverter} from "./AddressConverter.sol"; -import {ERC1155MetadataPrefix} from "./ERC1155MetadataPrefix.sol"; +import {AddressConverter} from "../utils/AddressConverter.sol"; +import {ERC1155MetadataPrefix} from "../utils/ERC1155MetadataPrefix.sol"; // Errors error UnsupportedMethod(); diff --git a/src/contracts/utils/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol similarity index 96% rename from src/contracts/utils/ERC721FloorWrapper.sol rename to src/contracts/wrappers/ERC721FloorWrapper.sol index 768d0fad..d47c9c86 100644 --- a/src/contracts/utils/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -7,8 +7,8 @@ import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; import {IERC721} from "../interfaces/IERC721.sol"; import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; -import {AddressConverter} from "./AddressConverter.sol"; -import {ERC1155MetadataPrefix} from "./ERC1155MetadataPrefix.sol"; +import {AddressConverter} from "../utils/AddressConverter.sol"; +import {ERC1155MetadataPrefix} from "../utils/ERC1155MetadataPrefix.sol"; // Errors error UnsupportedMethod(); diff --git a/src/contracts/utils/WrapAndNiftyswap.sol b/src/contracts/wrappers/WrapAndNiftyswap.sol similarity index 100% rename from src/contracts/utils/WrapAndNiftyswap.sol rename to src/contracts/wrappers/WrapAndNiftyswap.sol diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 4af9f23c..03b41c37 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -import {ERC1155FloorWrapper, InvalidERC1155Received} from "src/contracts/utils/ERC1155FloorWrapper.sol"; +import {ERC1155FloorWrapper, InvalidERC1155Received} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 14c695d6..f16250b6 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -import {ERC721FloorWrapper} from "src/contracts/utils/ERC721FloorWrapper.sol"; +import {ERC721FloorWrapper} from "src/contracts/wrappers/ERC721FloorWrapper.sol"; import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; diff --git a/tests_foundry/WrapAndNiftySwap.test.sol b/tests_foundry/WrapAndNiftySwap.test.sol index 35936f85..a93ff377 100644 --- a/tests_foundry/WrapAndNiftySwap.test.sol +++ b/tests_foundry/WrapAndNiftySwap.test.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; -import {WrapAndNiftyswap} from "src/contracts/utils/WrapAndNiftyswap.sol"; +import {WrapAndNiftyswap} from "src/contracts/wrappers/WrapAndNiftyswap.sol"; import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; import {ERC20WrapperMock} from "src/contracts/mocks/ERC20WrapperMock.sol"; From c9a4068f3a1d54d59122bdf2707b10aa5a97645b Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 30 Mar 2023 09:23:37 +1300 Subject: [PATCH 08/24] ERC721 wrapper single token support with Factory --- .../interfaces/IERC721FloorFactory.sol | 21 ++++ .../interfaces/IERC721FloorWrapper.sol | 10 +- src/contracts/utils/WrapperErrors.sol | 13 ++ src/contracts/wrappers/ERC721FloorFactory.sol | 57 +++++++++ src/contracts/wrappers/ERC721FloorWrapper.sol | 67 ++++++---- tests_foundry/AddressConverter.test.sol | 40 ------ tests_foundry/ERC721FloorFactory.test.sol | 88 +++++++++++++ tests_foundry/ERC721FloorWrapper.test.sol | 117 +++++++----------- 8 files changed, 275 insertions(+), 138 deletions(-) create mode 100644 src/contracts/interfaces/IERC721FloorFactory.sol create mode 100644 src/contracts/utils/WrapperErrors.sol create mode 100644 src/contracts/wrappers/ERC721FloorFactory.sol delete mode 100644 tests_foundry/AddressConverter.test.sol create mode 100644 tests_foundry/ERC721FloorFactory.test.sol diff --git a/src/contracts/interfaces/IERC721FloorFactory.sol b/src/contracts/interfaces/IERC721FloorFactory.sol new file mode 100644 index 00000000..59b6adae --- /dev/null +++ b/src/contracts/interfaces/IERC721FloorFactory.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +interface IERC721FloorFactory { + event NewERC721FloorWrapper(address indexed token); + event MetadataContractChanged(address indexed metadataContract); + + /** + * Creates an ERC-721 Floor Wrapper for given token contract + * @param tokenAddr The address of the ERC-721 token contract + * @return The address of the ERC-721 Floor Wrapper + */ + function createWrapper(address tokenAddr) external returns (address); + + /** + * Return address of the ERC-721 Floor Wrapper for a given token contract + * @param tokenAddr The address of the ERC-721 token contract + * @return The address of the ERC-721 Floor Wrapper + */ + function tokenToWrapper(address tokenAddr) external view returns (address); +} diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index a7b1b8e0..2ab78774 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -4,27 +4,25 @@ pragma solidity ^0.8.4; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; interface IERC721FloorWrapper is IERC1155 { - event TokensDeposited(address indexed tokenAddr, uint256[] tokenIds); + event TokensDeposited(uint256[] tokenIds); - event TokensWithdrawn(address indexed tokenAddr, uint256[] tokenIds); + event TokensWithdrawn(uint256[] tokenIds); /** * Deposit and wrap ERC-721 tokens. - * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to deposit. * @param recipient The recipient of the wrapped tokens. * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external; + function deposit(uint256[] memory tokenIds, address recipient, bytes calldata data) external; /** * Unwrap and withdraw ERC-721 tokens. - * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to withdraw. * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external; + function withdraw(uint256[] memory tokenIds, address recipient, bytes calldata data) external; } diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol new file mode 100644 index 00000000..2c1093c9 --- /dev/null +++ b/src/contracts/utils/WrapperErrors.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +/** + * Errors for the ERC-1155 and ERC-721 Wrapper and Factory contracts. + */ +abstract contract WrapperErrors { + // Factories + error WrapperAlreadyCreated(address tokenAddr, address wrapperAddr); + + // General + error UnsupportedMethod(); +} diff --git a/src/contracts/wrappers/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol new file mode 100644 index 00000000..7890ef16 --- /dev/null +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; +import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.sol"; +import {WrapperErrors} from "../utils/WrapperErrors.sol"; +import {Ownable} from "../utils/Ownable.sol"; +import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; + +contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperErrors { + mapping(address => address) public override tokenToWrapper; + + IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract + + constructor(address _admin) Ownable(_admin) {} // solhint-disable-line no-empty-blocks + + /** + * Creates an ERC-721 Floor Wrapper for given token contract + * @param tokenAddr The address of the ERC-721 token contract + * @return The address of the ERC-721 Floor Wrapper + */ + function createWrapper(address tokenAddr) external returns (address) { + if (tokenToWrapper[tokenAddr] != address(0)) { + revert WrapperAlreadyCreated(tokenAddr, tokenToWrapper[tokenAddr]); + } + + // Create new wrapper + ERC721FloorWrapper wrapper = new ERC721FloorWrapper(tokenAddr); + address wrapperAddr = address(wrapper); + + tokenToWrapper[tokenAddr] = wrapperAddr; + + emit NewERC721FloorWrapper(tokenAddr); + return wrapperAddr; + } + + // + // Metadata + // + + /** + * Changes the implementation of the ERC-1155 Metadata contract + * @dev This function changes the implementation for all child wrapper of this factory + * @param _contract The address of the ERC-1155 Metadata contract + */ + function setMetadataContract(IERC1155Metadata _contract) external onlyOwner { + emit MetadataContractChanged(address(_contract)); + metadataContract = _contract; + } + + /** + * @notice Returns the address of the ERC-1155 Metadata contract + */ + function metadataProvider() external view override returns (IERC1155Metadata) { + return metadataContract; + } +} diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index d47c9c86..dc8f4812 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -2,25 +2,31 @@ pragma solidity ^0.8.4; import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; + import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155Metadata} from "../interfaces/IERC1155Metadata.sol"; +import {IDelegatedERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; +import {WrapperErrors} from "../utils/WrapperErrors.sol"; + import {IERC721} from "../interfaces/IERC721.sol"; import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; -import {AddressConverter} from "../utils/AddressConverter.sol"; -import {ERC1155MetadataPrefix} from "../utils/ERC1155MetadataPrefix.sol"; - -// Errors -error UnsupportedMethod(); /** - * @notice Allows users to wrap any amount of any ERC-721 token with a 1:1 ratio - * of corresponding ERC-1155 tokens with native metaTransaction methods. - * Each ERC-721 within a collection is treated as if fungible. + * @notice Allows users to wrap any amount of a ERC-721 token with a 1:1 ratio. + * Therefore each ERC-721 within a collection can be treated as if fungible. */ -contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC1155MintBurn, AddressConverter { - // solhint-disable-next-line no-empty-blocks - constructor(string memory _prefix, address _admin) ERC1155MetadataPrefix(_prefix, false, _admin) {} +contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Metadata, WrapperErrors { + IERC721 public immutable token; + address internal immutable factory; + // This contract only supports a single token id + uint256 public constant TOKEN_ID = 0; + + constructor(address tokenAddr) { + token = IERC721(tokenAddr); + factory = msg.sender; + } /** * Prevent invalid method calls. @@ -29,41 +35,43 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 revert UnsupportedMethod(); } + // + // Tokens + // + /** * Deposit and wrap ERC-721 tokens. - * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to deposit. * @param recipient The recipient of the wrapped tokens. * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. */ - function deposit(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { + function deposit(uint256[] memory tokenIds, address recipient, bytes calldata data) external { uint256 length = tokenIds.length; for (uint256 i; i < length;) { // Intentionally unsafe transfer - IERC721(tokenAddr).transferFrom(msg.sender, address(this), tokenIds[i]); + token.transferFrom(msg.sender, address(this), tokenIds[i]); unchecked { // Can never overflow ++i; } } - emit TokensDeposited(tokenAddr, tokenIds); - _mint(recipient, convertAddressToUint256(tokenAddr), tokenIds.length, data); + emit TokensDeposited(tokenIds); + _mint(recipient, TOKEN_ID, tokenIds.length, data); } /** * Unwrap and withdraw ERC-721 tokens. - * @param tokenAddr The address of the ERC-721 tokens. * @param tokenIds The ERC-721 token ids to withdraw. * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(address tokenAddr, uint256[] memory tokenIds, address recipient, bytes calldata data) external { - _burn(msg.sender, convertAddressToUint256(tokenAddr), tokenIds.length); - emit TokensWithdrawn(tokenAddr, tokenIds); + function withdraw(uint256[] memory tokenIds, address recipient, bytes calldata data) external { + _burn(msg.sender, TOKEN_ID, tokenIds.length); + emit TokensWithdrawn(tokenIds); uint256 length = tokenIds.length; for (uint256 i; i < length;) { - IERC721(tokenAddr).safeTransferFrom(address(this), recipient, tokenIds[i], data); + token.safeTransferFrom(address(this), recipient, tokenIds[i], data); unchecked { // Can never overflow ++i; @@ -71,13 +79,28 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MetadataPrefix, ERC11 } } + // + // Metadata + // + + /** + * A distinct Uniform Resource Identifier (URI) for a given token. + * @param _id The token id. + * @dev URIs are defined in RFC 3986. + * The URI MUST point to a JSON file that conforms to the "ERC-1155 Metadata URI JSON Schema". + * @return URI string + */ + function uri(uint256 _id) external view override returns (string memory) { + return IDelegatedERC1155Metadata(factory).metadataProvider().uri(_id); + } + /** * Query if a contract supports an interface. * @param interfaceId The interfaceId to test. * @return supported Whether the interfaceId is supported. */ function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { - return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId + return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/tests_foundry/AddressConverter.test.sol b/tests_foundry/AddressConverter.test.sol deleted file mode 100644 index bea580d3..00000000 --- a/tests_foundry/AddressConverter.test.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.4; - -import {AddressConverter} from "src/contracts/utils/AddressConverter.sol"; - -import {Test, console} from "forge-std/Test.sol"; -import {stdError} from "forge-std/StdError.sol"; - -contract AddressConverterTest is Test { - AddressConverter private converter; - - function setUp() external { - converter = new AddressConverter(); - } - - function test_convertParity() external { - assertEq(address(0), addrUint256Addr(address(0))); - assertEq(address(1), addrUint256Addr(address(1))); - assertEq(address(2), addrUint256Addr(address(2))); - } - - // - // Helpers - // - function addrUint256Addr(address input) public view returns (address output) { - return converter.convertUint256ToAddress(converter.convertAddressToUint256(input)); - } - - /** - * Skip a test. - */ - modifier skipTest() { - // solhint-disable-next-line no-console - console.log("Test skipped"); - if (false) { - // Required for compiler - _; - } - } -} diff --git a/tests_foundry/ERC721FloorFactory.test.sol b/tests_foundry/ERC721FloorFactory.test.sol new file mode 100644 index 00000000..ed497970 --- /dev/null +++ b/tests_foundry/ERC721FloorFactory.test.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC721FloorFactory} from "src/contracts/interfaces/IERC721FloorFactory.sol"; +import {ERC721FloorFactory} from "src/contracts/wrappers/ERC721FloorFactory.sol"; +import {ERC721FloorWrapper} from "src/contracts/wrappers/ERC721FloorWrapper.sol"; +import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; +import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; +import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; +import {WrapperErrors} from "src/contracts/utils/WrapperErrors.sol"; +import {ERC1155MetadataPrefix} from "src/contracts/utils/ERC1155MetadataPrefix.sol"; + +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; + +import {console} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +// Note: Implements IERC721FloorFactory to access events +contract ERC721FloorFactoryTest is TestHelperBase, IERC721FloorFactory, WrapperErrors { + ERC721FloorFactory private factory; + + function setUp() external { + factory = new ERC721FloorFactory(address(this)); + } + + // + // Create Wrapper + // + function test_createWrapper_happyPath(address tokenAddr) public { + vm.expectEmit(true, true, true, true, address(factory)); + emit NewERC721FloorWrapper(tokenAddr); + startMeasuringGas("Create Wrapper"); // Logs only show when not fuzzing + address wrapper = factory.createWrapper(tokenAddr); + stopMeasuringGas(); + + assertEq(wrapper, factory.tokenToWrapper(tokenAddr)); + } + + function test_createWrapper_duplicateFails() external { + address tokenAddr = address(1); + test_createWrapper_happyPath(tokenAddr); + + vm.expectRevert( + abi.encodeWithSelector(WrapperAlreadyCreated.selector, tokenAddr, factory.tokenToWrapper(tokenAddr)) + ); + address wrapper = factory.createWrapper(tokenAddr); + } + + // + // Metadata + // + function test_metadataProvider_happyPath() external { + ERC1155MetadataPrefix metadata = new ERC1155MetadataPrefix("ipfs://", true, address(this)); + address metadataAddr = address(metadata); + vm.expectEmit(true, true, true, true, address(factory)); + emit MetadataContractChanged(metadataAddr); + factory.setMetadataContract(metadata); + + assertEq(address(factory.metadataProvider()), metadataAddr); + } + + // + // Helpers + // + + /** + * Skip a test. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } + + // + // Interface overrides + // + function createWrapper(address tokenAddr) external returns (address) { + revert UnsupportedMethod(); + } + + function tokenToWrapper(address tokenAddr) external view returns (address) { + revert UnsupportedMethod(); + } +} diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index f16250b6..80119ab1 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -20,21 +20,19 @@ import {stdError} from "forge-std/StdError.sol"; contract ERC721FloorWrapperTest is TestHelperBase { // Redeclare events - event TokensDeposited(address indexed tokenAddr, uint256[] tokenIds); - event TokensWithdrawn(address indexed tokenAddr, uint256[] tokenIds); + event TokensDeposited(uint256[] tokenIds); + event TokensWithdrawn(uint256[] tokenIds); ERC721FloorWrapper private wrapper; address private wrapperAddr; ERC721Mock private erc721; - address private erc721Addr; - uint256 private erc721Uint256; // Uint256 value of erc721Addr + uint256 private wrapperTokenId; function setUp() external { - wrapper = new ERC721FloorWrapper("", address(this)); - wrapperAddr = address(wrapper); erc721 = new ERC721Mock(); - erc721Addr = address(erc721); - erc721Uint256 = wrapper.convertAddressToUint256(erc721Addr); + wrapper = new ERC721FloorWrapper(address(erc721)); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); // Give tokens erc721.mintMock(USER, 5); @@ -51,7 +49,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { // Deposit // function test_deposit_happyPath() public { - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); @@ -60,18 +58,18 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc721Addr, tokenIds); + emit TokensDeposited(tokenIds); startMeasuringGas("Deposit 1"); - wrapper.deposit(erc721Addr, tokenIds, USER, ""); + wrapper.deposit(tokenIds, USER, ""); stopMeasuringGas(); - assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); } function test_deposit_toRecipient() public { - uint256 beforeERC1155OperatorBal = wrapper.balanceOf(OPERATOR, erc721Uint256); + uint256 beforeERC1155OperatorBal = wrapper.balanceOf(OPERATOR, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); @@ -80,10 +78,10 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc721Addr, tokenIds); - wrapper.deposit(erc721Addr, tokenIds, OPERATOR, ""); + emit TokensDeposited(tokenIds); + wrapper.deposit(tokenIds, OPERATOR, ""); - assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, erc721Uint256)); + assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); } @@ -94,7 +92,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { } function test_deposit_five() external { - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); @@ -107,12 +105,12 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc721Addr, tokenIds); + emit TokensDeposited(tokenIds); startMeasuringGas("Deposit 5"); - wrapper.deposit(erc721Addr, tokenIds, USER, ""); + wrapper.deposit(tokenIds, USER, ""); stopMeasuringGas(); - assertEq(beforeERC1155UserBal + 5, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal + 5, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal + 5, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal - 5, erc721.balanceOf(USER)); } @@ -124,7 +122,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.deposit(erc721Addr, tokenIds, USER, ""); + wrapper.deposit(tokenIds, USER, ""); } function test_deposit_wrongOwner() external { @@ -134,16 +132,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.deposit(erc721Addr, tokenIds, USER, ""); - } - - function test_deposit_invalidTokenAddr() external { - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - - vm.prank(USER); - vm.expectRevert(); - wrapper.deposit(address(1), tokenIds, USER, ""); + wrapper.deposit(tokenIds, USER, ""); } function test_deposit_andSell() public withDeposit { @@ -155,7 +144,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { factory.createExchange(wrapperAddr, currencyAddr, 0); address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0); uint256[] memory types = new uint256[](1); - types[0] = erc721Uint256; + types[0] = wrapperTokenId; withERC1155Liquidity(currency, exchangeAddr, types); // Sell data uint256[] memory sellAmounts = new uint256[](1); @@ -163,7 +152,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); // Before bals - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); uint256 beforeCurrencyUserBal = currency.balanceOf(USER, 0); @@ -173,12 +162,10 @@ contract ERC721FloorWrapperTest is TestHelperBase { tokenIds[0] = 2; tokenIds[1] = 3; vm.prank(USER); - wrapper.deposit( - erc721Addr, tokenIds, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) - ); + wrapper.deposit(tokenIds, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp)); // After bals - assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER, 0)); @@ -193,7 +180,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { factory.createExchange(wrapperAddr, currencyAddr, 0, 0); address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0, 0); uint256[] memory types = new uint256[](1); - types[0] = erc721Uint256; + types[0] = wrapperTokenId; withERC20Liquidity(currency, exchangeAddr, types); // Sell data uint256[] memory sellAmounts = new uint256[](1); @@ -201,7 +188,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); // Before bals - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); uint256 beforeCurrencyUserBal = currency.balanceOf(USER); @@ -212,14 +199,13 @@ contract ERC721FloorWrapperTest is TestHelperBase { tokenIds[1] = 3; vm.prank(USER); wrapper.deposit( - erc721Addr, tokenIds, exchangeAddr, Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], new address[](0), new uint256[](0), block.timestamp) ); // After bals - assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal + 2, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal - 2, erc721.balanceOf(USER)); assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); @@ -229,7 +215,7 @@ contract ERC721FloorWrapperTest is TestHelperBase { // Withdraw // function test_withdraw_happyPath() public withDeposit { - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); @@ -238,18 +224,18 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc721Addr, tokenIds); + emit TokensWithdrawn(tokenIds); startMeasuringGas("Withdraw 1"); - wrapper.withdraw(erc721Addr, tokenIds, USER, ""); + wrapper.withdraw(tokenIds, USER, ""); stopMeasuringGas(); - assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal + 1, erc721.balanceOf(USER)); } function test_withdraw_toRecipient() external withDeposit { - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721OperatorBal = erc721.balanceOf(OPERATOR); @@ -258,10 +244,10 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, OPERATOR, ""); + emit TokensWithdrawn(tokenIds); + wrapper.withdraw(tokenIds, OPERATOR, ""); - assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721OperatorBal + 1, erc721.balanceOf(OPERATOR)); } @@ -272,13 +258,13 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.withdraw(erc721Addr, tokenIds, USER, ""); + wrapper.withdraw(tokenIds, USER, ""); } function test_withdraw_twice() external { test_withdraw_happyPath(); - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721OperatorBal = erc721.balanceOf(OPERATOR); @@ -287,16 +273,16 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, OPERATOR, ""); + emit TokensWithdrawn(tokenIds); + wrapper.withdraw(tokenIds, OPERATOR, ""); - assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721OperatorBal + 1, erc721.balanceOf(OPERATOR)); } function test_withdraw_two() external withDeposit { - uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, erc721Uint256); + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); uint256 beforeERC721UserBal = erc721.balanceOf(USER); @@ -306,30 +292,21 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc721Addr, tokenIds); - wrapper.withdraw(erc721Addr, tokenIds, USER, ""); + emit TokensWithdrawn(tokenIds); + wrapper.withdraw(tokenIds, USER, ""); - assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, erc721Uint256)); + assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 2, erc721.balanceOf(wrapperAddr)); assertEq(beforeERC721UserBal + 2, erc721.balanceOf(USER)); } - function test_withdraw_invalidTokenAddr() external { - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - - vm.prank(USER); - vm.expectRevert(); - wrapper.withdraw(address(1), tokenIds, USER, ""); - } - function test_withdraw_insufficientBalance() external { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 0; vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc721Addr, tokenIds, USER, ""); + wrapper.withdraw(tokenIds, USER, ""); } // @@ -341,12 +318,12 @@ contract ERC721FloorWrapperTest is TestHelperBase { tokenIds[0] = 0; tokenIds[1] = 1; vm.prank(USER); - wrapper.deposit(erc721Addr, tokenIds, USER, ""); + wrapper.deposit(tokenIds, USER, ""); tokenIds[0] = 5; tokenIds[1] = 6; vm.prank(OPERATOR); - wrapper.deposit(erc721Addr, tokenIds, OPERATOR, ""); + wrapper.deposit(tokenIds, OPERATOR, ""); _; } From 9db626cb4151cfa8469e11e1676b0b3b3adb441e Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 30 Mar 2023 11:16:25 +1300 Subject: [PATCH 09/24] Use a proxy for wrapper deployments --- src/contracts/utils/Proxy.sol | 35 +++++++++++++++++++ src/contracts/utils/WrapperErrors.sol | 2 ++ src/contracts/wrappers/ERC721FloorFactory.sol | 26 ++++++++++---- src/contracts/wrappers/ERC721FloorWrapper.sol | 15 ++++++-- tests_foundry/ERC721FloorFactory.test.sol | 6 ++-- tests_foundry/ERC721FloorWrapper.test.sol | 31 +++++++++++++++- 6 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/contracts/utils/Proxy.sol diff --git a/src/contracts/utils/Proxy.sol b/src/contracts/utils/Proxy.sol new file mode 100644 index 00000000..3f92d05d --- /dev/null +++ b/src/contracts/utils/Proxy.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +contract Proxy { + address public implementation; + + constructor(address _implementation) { + implementation = _implementation; + } + + receive() external payable { + proxy(); + } + + fallback() external payable { + proxy(); + } + + function proxy() private { + address target; + assembly { + target := sload(implementation.slot) + } + assembly { + let ptr := mload(0x40) + calldatacopy(ptr, 0, calldatasize()) + let result := delegatecall(gas(), target, ptr, calldatasize(), 0, 0) + let size := returndatasize() + returndatacopy(ptr, 0, size) + switch result + case 0 { revert(ptr, size) } + default { return(ptr, size) } + } + } +} diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index 2c1093c9..d94fd373 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -7,7 +7,9 @@ pragma solidity ^0.8.4; abstract contract WrapperErrors { // Factories error WrapperAlreadyCreated(address tokenAddr, address wrapperAddr); + error WrapperCreationFailed(address tokenAddr); // General error UnsupportedMethod(); + error InvalidInitialization(); } diff --git a/src/contracts/wrappers/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol index 7890ef16..b9e8525a 100644 --- a/src/contracts/wrappers/ERC721FloorFactory.sol +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -5,28 +5,42 @@ import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.sol"; import {WrapperErrors} from "../utils/WrapperErrors.sol"; import {Ownable} from "../utils/Ownable.sol"; +import {Proxy} from "../utils/Proxy.sol"; import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperErrors { mapping(address => address) public override tokenToWrapper; + ERC721FloorWrapper private wrapperImpl; IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract - constructor(address _admin) Ownable(_admin) {} // solhint-disable-line no-empty-blocks + constructor(address _admin) Ownable(_admin) { + wrapperImpl = new ERC721FloorWrapper(); + wrapperImpl.initialize(address(0)); + } /** * Creates an ERC-721 Floor Wrapper for given token contract * @param tokenAddr The address of the ERC-721 token contract - * @return The address of the ERC-721 Floor Wrapper + * @return wrapperAddr The address of the ERC-721 Floor Wrapper */ - function createWrapper(address tokenAddr) external returns (address) { + function createWrapper(address tokenAddr) external returns (address wrapperAddr) { if (tokenToWrapper[tokenAddr] != address(0)) { revert WrapperAlreadyCreated(tokenAddr, tokenToWrapper[tokenAddr]); } - // Create new wrapper - ERC721FloorWrapper wrapper = new ERC721FloorWrapper(tokenAddr); - address wrapperAddr = address(wrapper); + // Compute the address of the proxy contract using create2 + bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(wrapperImpl)))); + bytes32 salt = keccak256(abi.encodePacked(tokenAddr)); + + // Deploy it + assembly { + wrapperAddr := create2(0, add(code, 32), mload(code), salt) + } + if (wrapperAddr == address(0)) { + revert WrapperCreationFailed(tokenAddr); + } + ERC721FloorWrapper(wrapperAddr).initialize(tokenAddr); tokenToWrapper[tokenAddr] = wrapperAddr; diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index dc8f4812..b44c3328 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -18,16 +18,25 @@ import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; * Therefore each ERC-721 within a collection can be treated as if fungible. */ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Metadata, WrapperErrors { - IERC721 public immutable token; + bool private initialized; + + IERC721 public token; address internal immutable factory; // This contract only supports a single token id uint256 public constant TOKEN_ID = 0; - constructor(address tokenAddr) { - token = IERC721(tokenAddr); + constructor() { factory = msg.sender; } + function initialize(address tokenAddr) external { + if (initialized || msg.sender != factory) { + revert InvalidInitialization(); + } + token = IERC721(tokenAddr); + initialized = true; + } + /** * Prevent invalid method calls. */ diff --git a/tests_foundry/ERC721FloorFactory.test.sol b/tests_foundry/ERC721FloorFactory.test.sol index ed497970..64259f7b 100644 --- a/tests_foundry/ERC721FloorFactory.test.sol +++ b/tests_foundry/ERC721FloorFactory.test.sol @@ -43,7 +43,7 @@ contract ERC721FloorFactoryTest is TestHelperBase, IERC721FloorFactory, WrapperE vm.expectRevert( abi.encodeWithSelector(WrapperAlreadyCreated.selector, tokenAddr, factory.tokenToWrapper(tokenAddr)) ); - address wrapper = factory.createWrapper(tokenAddr); + factory.createWrapper(tokenAddr); } // @@ -78,11 +78,11 @@ contract ERC721FloorFactoryTest is TestHelperBase, IERC721FloorFactory, WrapperE // // Interface overrides // - function createWrapper(address tokenAddr) external returns (address) { + function createWrapper(address) external pure returns (address) { revert UnsupportedMethod(); } - function tokenToWrapper(address tokenAddr) external view returns (address) { + function tokenToWrapper(address) external pure returns (address) { revert UnsupportedMethod(); } } diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 80119ab1..471429fd 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.4; import {ERC721FloorWrapper} from "src/contracts/wrappers/ERC721FloorWrapper.sol"; +import {ERC721FloorFactory} from "src/contracts/wrappers/ERC721FloorFactory.sol"; import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; @@ -30,7 +31,9 @@ contract ERC721FloorWrapperTest is TestHelperBase { function setUp() external { erc721 = new ERC721Mock(); - wrapper = new ERC721FloorWrapper(address(erc721)); + + wrapper = new ERC721FloorWrapper(); + wrapper.initialize(address(erc721)); wrapperAddr = address(wrapper); wrapperTokenId = wrapper.TOKEN_ID(); @@ -68,6 +71,10 @@ contract ERC721FloorWrapperTest is TestHelperBase { assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); } + function test_deposit_happyPathWithFactory() public withFactoryCreatedWrapper { + test_deposit_happyPath(); + } + function test_deposit_toRecipient() public { uint256 beforeERC1155OperatorBal = wrapper.balanceOf(OPERATOR, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); @@ -234,6 +241,10 @@ contract ERC721FloorWrapperTest is TestHelperBase { assertEq(beforeERC721UserBal + 1, erc721.balanceOf(USER)); } + function test_withdraw_happyPathWithFactory() public withFactoryCreatedWrapper { + test_withdraw_happyPath(); + } + function test_withdraw_toRecipient() external withDeposit { uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); @@ -293,7 +304,9 @@ contract ERC721FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds); + startMeasuringGas("Withdraw 2"); wrapper.withdraw(tokenIds, USER, ""); + stopMeasuringGas(); assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 2, erc721.balanceOf(wrapperAddr)); @@ -312,6 +325,22 @@ contract ERC721FloorWrapperTest is TestHelperBase { // // Helpers // + modifier withFactoryCreatedWrapper() { + // Recreate wrapper through factory + ERC721FloorFactory factory = new ERC721FloorFactory(address(this)); + wrapper = ERC721FloorWrapper(factory.createWrapper(address(erc721))); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); + + // Approvals + vm.prank(USER); + erc721.setApprovalForAll(wrapperAddr, true); + vm.prank(OPERATOR); + erc721.setApprovalForAll(wrapperAddr, true); + + _; + } + modifier withDeposit() { uint256[] memory tokenIds = new uint256[](2); From 26bac19735e70d6b9af3f763ede0325d82f3e8f5 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 30 Mar 2023 11:50:22 +1300 Subject: [PATCH 10/24] Use Factory for ERC1155 Wrapper --- .../interfaces/IERC1155FloorFactory.sol | 21 +++ .../interfaces/IERC1155FloorWrapper.sol | 24 +-- src/contracts/utils/AddressConverter.sol | 23 --- src/contracts/utils/WrapperErrors.sol | 3 + .../wrappers/ERC1155FloorFactory.sol | 71 ++++++++ .../wrappers/ERC1155FloorWrapper.sol | 83 +++++---- src/contracts/wrappers/ERC721FloorWrapper.sol | 4 +- tests_foundry/ERC1155FloorFactory.test.sol | 88 ++++++++++ tests_foundry/ERC1155FloorWrapper.test.sol | 158 ++++++++++-------- 9 files changed, 324 insertions(+), 151 deletions(-) create mode 100644 src/contracts/interfaces/IERC1155FloorFactory.sol delete mode 100644 src/contracts/utils/AddressConverter.sol create mode 100644 src/contracts/wrappers/ERC1155FloorFactory.sol create mode 100644 tests_foundry/ERC1155FloorFactory.test.sol diff --git a/src/contracts/interfaces/IERC1155FloorFactory.sol b/src/contracts/interfaces/IERC1155FloorFactory.sol new file mode 100644 index 00000000..388a9353 --- /dev/null +++ b/src/contracts/interfaces/IERC1155FloorFactory.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +interface IERC1155FloorFactory { + event NewERC1155FloorWrapper(address indexed token); + event MetadataContractChanged(address indexed metadataContract); + + /** + * Creates an ERC-1155 Floor Wrapper for given token contract + * @param tokenAddr The address of the ERC-1155 token contract + * @return The address of the ERC-1155 Floor Wrapper + */ + function createWrapper(address tokenAddr) external returns (address); + + /** + * Return address of the ERC-1155 Floor Wrapper for a given token contract + * @param tokenAddr The address of the ERC-1155 token contract + * @return The address of the ERC-1155 Floor Wrapper + */ + function tokenToWrapper(address tokenAddr) external view returns (address); +} diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol index 6303cafc..4d6fd34b 100644 --- a/src/contracts/interfaces/IERC1155FloorWrapper.sol +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -4,40 +4,28 @@ pragma solidity ^0.8.4; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; interface IERC1155FloorWrapper is IERC1155 { - event TokensDeposited(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + event TokensDeposited(uint256[] tokenIds, uint256[] tokenAmounts); - event TokensWithdrawn(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + event TokensWithdrawn(uint256[] tokenIds, uint256[] tokenAmounts); /** * Deposit and wrap ERC-1155 tokens. - * @param tokenAddr The address of the ERC-1155 tokens. * @param tokenIds The ERC-1155 token ids to deposit. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the wrapped tokens. * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit( - address tokenAddr, - uint256[] memory tokenIds, - uint256[] memory tokenAmounts, - address recipient, - bytes calldata data - ) external; + function deposit(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) + external; /** * Unwrap and withdraw ERC-1155 tokens. - * @param tokenAddr The address of the ERC-1155 tokens. * @param tokenIds The ERC-1155 token ids to withdraw. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw( - address tokenAddr, - uint256[] memory tokenIds, - uint256[] memory tokenAmounts, - address recipient, - bytes calldata data - ) external; + function withdraw(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) + external; } diff --git a/src/contracts/utils/AddressConverter.sol b/src/contracts/utils/AddressConverter.sol deleted file mode 100644 index 3002c7e9..00000000 --- a/src/contracts/utils/AddressConverter.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.4; - -contract AddressConverter { - /** - * Convert an address into a uint256. - * @param input The address to convert. - * @return output The resulting uint256. - */ - function convertAddressToUint256(address input) public pure returns (uint256 output) { - return uint256(uint160(input)); - } - - /** - * Convert a uint256 into an address. - * @param input The uint256 to convert. - * @return output The resulting address. - * @dev As uint256 is larger than address, this may result in collisions. - */ - function convertUint256ToAddress(uint256 input) public pure returns (address output) { - return address(uint160(input)); - } -} diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index d94fd373..97beb3ba 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -9,6 +9,9 @@ abstract contract WrapperErrors { error WrapperAlreadyCreated(address tokenAddr, address wrapperAddr); error WrapperCreationFailed(address tokenAddr); + // ERC1155 + error InvalidERC1155Received(); + // General error UnsupportedMethod(); error InvalidInitialization(); diff --git a/src/contracts/wrappers/ERC1155FloorFactory.sol b/src/contracts/wrappers/ERC1155FloorFactory.sol new file mode 100644 index 00000000..88e3fc49 --- /dev/null +++ b/src/contracts/wrappers/ERC1155FloorFactory.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC1155FloorFactory} from "../interfaces/IERC1155FloorFactory.sol"; +import {ERC1155FloorWrapper} from "../wrappers/ERC1155FloorWrapper.sol"; +import {WrapperErrors} from "../utils/WrapperErrors.sol"; +import {Ownable} from "../utils/Ownable.sol"; +import {Proxy} from "../utils/Proxy.sol"; +import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; + +contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperErrors { + mapping(address => address) public override tokenToWrapper; + ERC1155FloorWrapper private wrapperImpl; + + IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract + + constructor(address _admin) Ownable(_admin) { + wrapperImpl = new ERC1155FloorWrapper(); + wrapperImpl.initialize(address(0)); + } + + /** + * Creates an ERC-1155 Floor Wrapper for given token contract + * @param tokenAddr The address of the ERC-1155 token contract + * @return wrapperAddr The address of the ERC-1155 Floor Wrapper + */ + function createWrapper(address tokenAddr) external returns (address wrapperAddr) { + if (tokenToWrapper[tokenAddr] != address(0)) { + revert WrapperAlreadyCreated(tokenAddr, tokenToWrapper[tokenAddr]); + } + + // Compute the address of the proxy contract using create2 + bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(wrapperImpl)))); + bytes32 salt = keccak256(abi.encodePacked(tokenAddr)); + + // Deploy it + assembly { + wrapperAddr := create2(0, add(code, 32), mload(code), salt) + } + if (wrapperAddr == address(0)) { + revert WrapperCreationFailed(tokenAddr); + } + ERC1155FloorWrapper(wrapperAddr).initialize(tokenAddr); + + tokenToWrapper[tokenAddr] = wrapperAddr; + + emit NewERC1155FloorWrapper(tokenAddr); + return wrapperAddr; + } + + // + // Metadata + // + + /** + * Changes the implementation of the ERC-1155 Metadata contract + * @dev This function changes the implementation for all child wrapper of this factory + * @param _contract The address of the ERC-1155 Metadata contract + */ + function setMetadataContract(IERC1155Metadata _contract) external onlyOwner { + emit MetadataContractChanged(address(_contract)); + metadataContract = _contract; + } + + /** + * @notice Returns the address of the ERC-1155 Metadata contract + */ + function metadataProvider() external view override returns (IERC1155Metadata) { + return metadataContract; + } +} diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 88f95f28..4d7e8364 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -7,28 +7,40 @@ import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/I import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; import {IERC1155FloorWrapper} from "../interfaces/IERC1155FloorWrapper.sol"; -import {AddressConverter} from "../utils/AddressConverter.sol"; -import {ERC1155MetadataPrefix} from "../utils/ERC1155MetadataPrefix.sol"; - -// Errors -error UnsupportedMethod(); -error InvalidERC1155Received(); +import {IERC1155Metadata} from "../interfaces/IERC1155Metadata.sol"; +import {IDelegatedERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; +import {WrapperErrors} from "../utils/WrapperErrors.sol"; /** - * Allows an all token ids within an ERC-1155 contract to be wrapped and - * treated as a single ERC-1155 token id. + * @notice Allows users to wrap any amount of a ERC-1155 token with a 1:1 ratio. + * Therefore each ERC-1155 within a collection can be treated as if fungible. */ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, - ERC1155MetadataPrefix, + IERC1155Metadata, IERC1155TokenReceiver, - AddressConverter + WrapperErrors { + bool private initialized; + address internal immutable factory; + IERC1155 public token; + // This contract only supports a single token id + uint256 public constant TOKEN_ID = 0; + bool private isDepositing; - // solhint-disable-next-line no-empty-blocks - constructor(string memory _prefix, address _admin) ERC1155MetadataPrefix(_prefix, false, _admin) {} + constructor() { + factory = msg.sender; + } + + function initialize(address tokenAddr) external { + if (initialized || msg.sender != factory) { + revert InvalidInitialization(); + } + token = IERC1155(tokenAddr); + initialized = true; + } modifier onlyDepositing() { if (!isDepositing) { @@ -47,57 +59,47 @@ contract ERC1155FloorWrapper is /** * Deposit and wrap ERC-1155 tokens. - * @param tokenAddr The address of the ERC-1155 tokens. * @param tokenIds The ERC-1155 token ids to deposit. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the wrapped tokens. * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit( - address tokenAddr, - uint256[] memory tokenIds, - uint256[] memory tokenAmounts, - address recipient, - bytes calldata data - ) external { + function deposit(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) + external + { isDepositing = true; - IERC1155(tokenAddr).safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); + token.safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); delete isDepositing; - emit TokensDeposited(tokenAddr, tokenIds, tokenAmounts); + emit TokensDeposited(tokenIds, tokenAmounts); uint256 total; for (uint256 i = 0; i < tokenIds.length; i++) { total += tokenAmounts[i]; } - _mint(recipient, convertAddressToUint256(tokenAddr), total, data); + _mint(recipient, TOKEN_ID, total, data); } /** * Unwrap and withdraw ERC-1155 tokens. - * @param tokenAddr The address of the ERC-1155 tokens. * @param tokenIds The ERC-1155 token ids to withdraw. * @param tokenAmounts The amount of each token to deposit. * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw( - address tokenAddr, - uint256[] memory tokenIds, - uint256[] memory tokenAmounts, - address recipient, - bytes calldata data - ) external { + function withdraw(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) + external + { uint256 total; for (uint256 i = 0; i < tokenIds.length; i++) { total += tokenAmounts[i]; } - _burn(msg.sender, convertAddressToUint256(tokenAddr), total); + _burn(msg.sender, TOKEN_ID, total); - emit TokensWithdrawn(tokenAddr, tokenIds, tokenAmounts); + emit TokensWithdrawn(tokenIds, tokenAmounts); - IERC1155(tokenAddr).safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, data); + token.safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, data); } /** @@ -124,6 +126,17 @@ contract ERC1155FloorWrapper is return IERC1155TokenReceiver.onERC1155BatchReceived.selector; } + /** + * A distinct Uniform Resource Identifier (URI) for a given token. + * @param _id The token id. + * @dev URIs are defined in RFC 3986. + * The URI MUST point to a JSON file that conforms to the "ERC-1155 Metadata URI JSON Schema". + * @return URI string + */ + function uri(uint256 _id) external view override returns (string memory) { + return IDelegatedERC1155Metadata(factory).metadataProvider().uri(_id); + } + /** * Query if a contract supports an interface. * @param interfaceId The interfaceId to test. @@ -131,6 +144,6 @@ contract ERC1155FloorWrapper is */ function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId - || super.supportsInterface(interfaceId); + || interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index b44c3328..9f1dc720 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -109,7 +109,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met * @return supported Whether the interfaceId is supported. */ function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { - return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155Metadata).interfaceId - || super.supportsInterface(interfaceId); + return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId + || interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/tests_foundry/ERC1155FloorFactory.test.sol b/tests_foundry/ERC1155FloorFactory.test.sol new file mode 100644 index 00000000..500d0e07 --- /dev/null +++ b/tests_foundry/ERC1155FloorFactory.test.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC1155FloorFactory} from "src/contracts/interfaces/IERC1155FloorFactory.sol"; +import {ERC1155FloorFactory} from "src/contracts/wrappers/ERC1155FloorFactory.sol"; +import {ERC1155FloorWrapper} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; +import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; +import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; +import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; +import {WrapperErrors} from "src/contracts/utils/WrapperErrors.sol"; +import {ERC1155MetadataPrefix} from "src/contracts/utils/ERC1155MetadataPrefix.sol"; + +import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; + +import {console} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +// Note: Implements IERC1155FloorFactory to access events +contract ERC1155FloorFactoryTest is TestHelperBase, IERC1155FloorFactory, WrapperErrors { + ERC1155FloorFactory private factory; + + function setUp() external { + factory = new ERC1155FloorFactory(address(this)); + } + + // + // Create Wrapper + // + function test_createWrapper_happyPath(address tokenAddr) public { + vm.expectEmit(true, true, true, true, address(factory)); + emit NewERC1155FloorWrapper(tokenAddr); + startMeasuringGas("Create Wrapper"); // Logs only show when not fuzzing + address wrapper = factory.createWrapper(tokenAddr); + stopMeasuringGas(); + + assertEq(wrapper, factory.tokenToWrapper(tokenAddr)); + } + + function test_createWrapper_duplicateFails() external { + address tokenAddr = address(1); + test_createWrapper_happyPath(tokenAddr); + + vm.expectRevert( + abi.encodeWithSelector(WrapperAlreadyCreated.selector, tokenAddr, factory.tokenToWrapper(tokenAddr)) + ); + factory.createWrapper(tokenAddr); + } + + // + // Metadata + // + function test_metadataProvider_happyPath() external { + ERC1155MetadataPrefix metadata = new ERC1155MetadataPrefix("ipfs://", true, address(this)); + address metadataAddr = address(metadata); + vm.expectEmit(true, true, true, true, address(factory)); + emit MetadataContractChanged(metadataAddr); + factory.setMetadataContract(metadata); + + assertEq(address(factory.metadataProvider()), metadataAddr); + } + + // + // Helpers + // + + /** + * Skip a test. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } + + // + // Interface overrides + // + function createWrapper(address) external pure returns (address) { + revert UnsupportedMethod(); + } + + function tokenToWrapper(address) external pure returns (address) { + revert UnsupportedMethod(); + } +} diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 03b41c37..3fe0b95a 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -import {ERC1155FloorWrapper, InvalidERC1155Received} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; +import {ERC1155FloorWrapper} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; +import {ERC1155FloorFactory} from "src/contracts/wrappers/ERC1155FloorFactory.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.sol"; import {INiftyswapExchange20} from "src/contracts/interfaces/INiftyswapExchange20.sol"; import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; +import {WrapperErrors} from "src/contracts/utils/WrapperErrors.sol"; import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; import {Niftyswap20TestHelper} from "./utils/Niftyswap20TestHelper.test.sol"; @@ -17,23 +19,25 @@ import {USER, OPERATOR} from "./utils/Constants.test.sol"; import {console} from "forge-std/Test.sol"; import {stdError} from "forge-std/StdError.sol"; -contract ERC1155FloorWrapperTest is TestHelperBase { +contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { // Redeclare events - event TokensDeposited(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); - event TokensWithdrawn(address tokenAddr, uint256[] tokenIds, uint256[] tokenAmounts); + event TokensDeposited(uint256[] tokenIds, uint256[] tokenAmounts); + event TokensWithdrawn(uint256[] tokenIds, uint256[] tokenAmounts); ERC1155FloorWrapper private wrapper; address private wrapperAddr; ERC1155Mock private erc1155; address private erc1155Addr; - uint256 private erc1155Uint256; // The Uint256 value of erc1155Addr + uint256 private wrapperTokenId; function setUp() external { - wrapper = new ERC1155FloorWrapper("", address(this)); - wrapperAddr = address(wrapper); erc1155 = new ERC1155Mock(); erc1155Addr = address(erc1155); - erc1155Uint256 = wrapper.convertAddressToUint256(erc1155Addr); + + wrapper = new ERC1155FloorWrapper(); + wrapper.initialize(erc1155Addr); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); // Give tokens uint256[] memory tokenIds = new uint256[](3); @@ -58,7 +62,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { // Deposit // function test_deposit_happyPath() public { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); @@ -69,16 +73,20 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensDeposited(tokenIds, tokenAmounts); + wrapper.deposit(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal + 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal + 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal - 1, erc1155.balanceOf(USER, 0)); } + function test_deposit_happyPathWithFactory() public withFactoryCreatedWrapper { + test_deposit_happyPath(); + } + function test_deposit_toRecipient() public { - uint256 beforeWrapperOperatorBal = wrapper.balanceOf(OPERATOR, erc1155Uint256); + uint256 beforeWrapperOperatorBal = wrapper.balanceOf(OPERATOR, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); @@ -89,10 +97,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); + emit TokensDeposited(tokenIds, tokenAmounts); + wrapper.deposit(tokenIds, tokenAmounts, OPERATOR, ""); - assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, erc1155Uint256)); + assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal - 1, erc1155.balanceOf(USER, 0)); } @@ -103,7 +111,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { } function test_deposit_two() external { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); @@ -114,16 +122,16 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensDeposited(tokenIds, tokenAmounts); + wrapper.deposit(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); } function test_deposit_twoDiffTokens() external { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal0 = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal0 = erc1155.balanceOf(USER, 0); uint256 beforeERC1155WrapperBal1 = erc1155.balanceOf(wrapperAddr, 1); @@ -138,10 +146,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensDeposited(erc1155Addr, tokenIds, tokenAmounts); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensDeposited(tokenIds, tokenAmounts); + wrapper.deposit(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal0 + 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal0 - 1, erc1155.balanceOf(USER, 0)); assertEq(beforeERC1155WrapperBal1 + 1, erc1155.balanceOf(wrapperAddr, 1)); @@ -158,18 +166,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); - } - - function test_deposit_invalidTokenAddr() external { - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - uint256[] memory tokenAmounts = new uint256[](1); - tokenAmounts[0] = 1; - - vm.prank(USER); - vm.expectRevert(); - wrapper.deposit(address(1), tokenIds, tokenAmounts, USER, ""); + wrapper.deposit(tokenIds, tokenAmounts, USER, ""); } function test_deposit_andSell() public withDeposit { @@ -181,7 +178,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { factory.createExchange(wrapperAddr, currencyAddr, 0); address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0); uint256[] memory types = new uint256[](1); - types[0] = erc1155Uint256; + types[0] = wrapperTokenId; withERC1155Liquidity(currency, exchangeAddr, types); // Sell data uint256[] memory sellAmounts = new uint256[](1); @@ -189,7 +186,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); // Before bals - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); uint256 beforeCurrencyUserBal = currency.balanceOf(USER, 0); @@ -199,15 +196,11 @@ contract ERC1155FloorWrapperTest is TestHelperBase { tokenIds[0] = 0; vm.prank(USER); wrapper.deposit( - erc1155Addr, - tokenIds, - sellAmounts, - exchangeAddr, - NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) + tokenIds, sellAmounts, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) ); // After bals - assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER, 0)); @@ -222,7 +215,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { factory.createExchange(wrapperAddr, currencyAddr, 0, 0); address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0, 0); uint256[] memory types = new uint256[](1); - types[0] = erc1155Uint256; + types[0] = wrapperTokenId; withERC20Liquidity(currency, exchangeAddr, types); // Sell data uint256[] memory sellAmounts = new uint256[](1); @@ -230,7 +223,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); // Before bals - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); uint256 beforeCurrencyUserBal = currency.balanceOf(USER); @@ -240,7 +233,6 @@ contract ERC1155FloorWrapperTest is TestHelperBase { tokenIds[0] = 0; vm.prank(USER); wrapper.deposit( - erc1155Addr, tokenIds, sellAmounts, exchangeAddr, @@ -248,7 +240,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { ); // After bals - assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal - 2, erc1155.balanceOf(USER, 0)); assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); @@ -258,7 +250,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { // Withdraw // function test_withdraw_happyPath() public withDeposit { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); @@ -269,16 +261,20 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensWithdrawn(tokenIds, tokenAmounts); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal + 1, erc1155.balanceOf(USER, 0)); } + function test_withdraw_happyPathWithFactory() public withFactoryCreatedWrapper { + test_withdraw_happyPath(); + } + function test_withdraw_toRecipient() external withDeposit { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155OperatorBal = erc1155.balanceOf(OPERATOR, 0); @@ -289,10 +285,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); + emit TokensWithdrawn(tokenIds, tokenAmounts); + wrapper.withdraw(tokenIds, tokenAmounts, OPERATOR, ""); - assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155OperatorBal + 1, erc1155.balanceOf(OPERATOR, 0)); } @@ -305,13 +301,13 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); } function test_withdraw_twice() external { test_withdraw_happyPath(); - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155OperatorBal = erc1155.balanceOf(OPERATOR, 0); @@ -322,16 +318,16 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); + emit TokensWithdrawn(tokenIds, tokenAmounts); + wrapper.withdraw(tokenIds, tokenAmounts, OPERATOR, ""); - assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155OperatorBal + 1, erc1155.balanceOf(OPERATOR, 0)); } function test_withdraw_two() external withDeposit { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155UserBal = erc1155.balanceOf(USER, 0); @@ -342,16 +338,16 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensWithdrawn(tokenIds, tokenAmounts); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 2, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155UserBal + 2, erc1155.balanceOf(USER, 0)); } function test_withdraw_twoDiffTokens() external withDeposit { - uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, erc1155Uint256); + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); uint256 beforeERC1155WrapperBal0 = erc1155.balanceOf(wrapperAddr, 0); uint256 beforeERC1155WrapperBal1 = erc1155.balanceOf(wrapperAddr, 1); uint256 beforeERC1155UserBal0 = erc1155.balanceOf(USER, 0); @@ -366,10 +362,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); - emit TokensWithdrawn(erc1155Addr, tokenIds, tokenAmounts); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + emit TokensWithdrawn(tokenIds, tokenAmounts); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); - assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, erc1155Uint256)); + assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal0 - 1, erc1155.balanceOf(wrapperAddr, 0)); assertEq(beforeERC1155WrapperBal1 - 1, erc1155.balanceOf(wrapperAddr, 1)); assertEq(beforeERC1155UserBal0 + 1, erc1155.balanceOf(USER, 0)); @@ -384,7 +380,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(); - wrapper.withdraw(address(1), tokenIds, tokenAmounts, USER, ""); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); } function test_withdraw_insufficientBalance() external { @@ -395,7 +391,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); } // @@ -410,6 +406,22 @@ contract ERC1155FloorWrapperTest is TestHelperBase { // // Helpers // + modifier withFactoryCreatedWrapper() { + // Recreate wrapper through factory + ERC1155FloorFactory factory = new ERC1155FloorFactory(address(this)); + wrapper = ERC1155FloorWrapper(factory.createWrapper(address(erc1155))); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); + + // Approvals + vm.prank(USER); + erc1155.setApprovalForAll(wrapperAddr, true); + vm.prank(OPERATOR); + erc1155.setApprovalForAll(wrapperAddr, true); + + _; + } + modifier withDeposit() { uint256[] memory tokenIds = new uint256[](2); tokenIds[0] = 0; @@ -419,9 +431,9 @@ contract ERC1155FloorWrapperTest is TestHelperBase { tokenAmounts[1] = 2; vm.prank(USER); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, USER, ""); + wrapper.deposit(tokenIds, tokenAmounts, USER, ""); vm.prank(OPERATOR); - wrapper.deposit(erc1155Addr, tokenIds, tokenAmounts, OPERATOR, ""); + wrapper.deposit(tokenIds, tokenAmounts, OPERATOR, ""); _; } From f9da9469e095dd400e2a60013dcc2c7018685196 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Mar 2023 10:57:13 +1300 Subject: [PATCH 11/24] Predict addresses of wrappers --- src/contracts/utils/WrapperProxyDeployer.sol | 66 +++++++++++++++++++ .../wrappers/ERC1155FloorFactory.sol | 42 ++++++------ src/contracts/wrappers/ERC721FloorFactory.sol | 44 ++++++------- 3 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/contracts/utils/WrapperProxyDeployer.sol diff --git a/src/contracts/utils/WrapperProxyDeployer.sol b/src/contracts/utils/WrapperProxyDeployer.sol new file mode 100644 index 00000000..6052964b --- /dev/null +++ b/src/contracts/utils/WrapperProxyDeployer.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; +import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.sol"; +import {WrapperErrors} from "../utils/WrapperErrors.sol"; +import {Ownable} from "../utils/Ownable.sol"; +import {Proxy} from "../utils/Proxy.sol"; +import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; + +abstract contract WrapperProxyDeployer is WrapperErrors { + /** + * Creates a proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @param tokenAddr The address of the token contract + * @return proxyAddr The address of the deployed proxy + */ + function deployProxy(address implAddr, address tokenAddr) internal returns (address proxyAddr) { + bytes memory code = getProxyCode(implAddr); + implAddr = predictWrapperAddress(code, tokenAddr); + if (isContract(implAddr)) { + revert WrapperAlreadyCreated(tokenAddr, implAddr); + } + + // Compute the address of the proxy contract using create2 + bytes32 salt = getProxySalt(tokenAddr); + + // Deploy it + assembly { + implAddr := create2(0, add(code, 32), mload(code), salt) + } + if (implAddr == address(0)) { + revert WrapperCreationFailed(tokenAddr); + } + return implAddr; + } + + function predictWrapperAddress(address implAddr, address tokenAddr) internal view returns (address) { + bytes memory code = getProxyCode(implAddr); + return predictWrapperAddress(code, tokenAddr); + } + + function predictWrapperAddress(bytes memory code, address tokenAddr) private view returns (address) { + bytes32 salt = getProxySalt(tokenAddr); + address deployer = address(this); + bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, keccak256(code))); + return address(uint160(uint256(_data))); + } + + function getProxyCode(address implAddr) private pure returns (bytes memory) { + return abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(implAddr)))); + } + + function getProxySalt(address tokenAddr) private pure returns (bytes32) { + return keccak256(abi.encodePacked(tokenAddr)); + } + + function isContract(address addr) internal view returns (bool) { + uint256 csize; + // solhint-disable-next-line no-inline-assembly + assembly { + csize := extcodesize(addr) + } + return csize != 0; + } +} diff --git a/src/contracts/wrappers/ERC1155FloorFactory.sol b/src/contracts/wrappers/ERC1155FloorFactory.sol index 88e3fc49..5e9cb296 100644 --- a/src/contracts/wrappers/ERC1155FloorFactory.sol +++ b/src/contracts/wrappers/ERC1155FloorFactory.sol @@ -3,20 +3,19 @@ pragma solidity ^0.8.4; import {IERC1155FloorFactory} from "../interfaces/IERC1155FloorFactory.sol"; import {ERC1155FloorWrapper} from "../wrappers/ERC1155FloorWrapper.sol"; -import {WrapperErrors} from "../utils/WrapperErrors.sol"; import {Ownable} from "../utils/Ownable.sol"; import {Proxy} from "../utils/Proxy.sol"; +import {WrapperProxyDeployer} from "../utils/WrapperProxyDeployer.sol"; import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; -contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperErrors { - mapping(address => address) public override tokenToWrapper; - ERC1155FloorWrapper private wrapperImpl; - +contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperProxyDeployer { + address private immutable implAddr; // Address of the wrapper implementation IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract constructor(address _admin) Ownable(_admin) { - wrapperImpl = new ERC1155FloorWrapper(); + ERC1155FloorWrapper wrapperImpl = new ERC1155FloorWrapper(); wrapperImpl.initialize(address(0)); + implAddr = address(wrapperImpl); } /** @@ -25,26 +24,23 @@ contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155 * @return wrapperAddr The address of the ERC-1155 Floor Wrapper */ function createWrapper(address tokenAddr) external returns (address wrapperAddr) { - if (tokenToWrapper[tokenAddr] != address(0)) { - revert WrapperAlreadyCreated(tokenAddr, tokenToWrapper[tokenAddr]); - } - - // Compute the address of the proxy contract using create2 - bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(wrapperImpl)))); - bytes32 salt = keccak256(abi.encodePacked(tokenAddr)); - - // Deploy it - assembly { - wrapperAddr := create2(0, add(code, 32), mload(code), salt) - } - if (wrapperAddr == address(0)) { - revert WrapperCreationFailed(tokenAddr); - } + wrapperAddr = deployProxy(implAddr, tokenAddr); ERC1155FloorWrapper(wrapperAddr).initialize(tokenAddr); + emit NewERC1155FloorWrapper(tokenAddr); + return wrapperAddr; + } - tokenToWrapper[tokenAddr] = wrapperAddr; + /** + * Return address of the ERC-1155 Floor Wrapper for a given token contract + * @param tokenAddr The address of the ERC-1155 token contract + * @return wrapperAddr The address of the ERC-1155 Floor Wrapper + */ + function tokenToWrapper(address tokenAddr) public view returns (address wrapperAddr) { + wrapperAddr = predictWrapperAddress(implAddr, tokenAddr); + if (!isContract(wrapperAddr)) { + return address(0); + } - emit NewERC1155FloorWrapper(tokenAddr); return wrapperAddr; } diff --git a/src/contracts/wrappers/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol index b9e8525a..aca032f1 100644 --- a/src/contracts/wrappers/ERC721FloorFactory.sol +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -3,20 +3,19 @@ pragma solidity ^0.8.4; import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.sol"; -import {WrapperErrors} from "../utils/WrapperErrors.sol"; import {Ownable} from "../utils/Ownable.sol"; import {Proxy} from "../utils/Proxy.sol"; +import {WrapperProxyDeployer} from "../utils/WrapperProxyDeployer.sol"; import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; -contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperErrors { - mapping(address => address) public override tokenToWrapper; - ERC721FloorWrapper private wrapperImpl; - - IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract +contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Metadata, WrapperProxyDeployer { + address private immutable implAddr; // Address of the wrapper implementation + IERC1155Metadata internal metadataContract; // Address of the ERC-1155 Metadata contract constructor(address _admin) Ownable(_admin) { - wrapperImpl = new ERC721FloorWrapper(); + ERC721FloorWrapper wrapperImpl = new ERC721FloorWrapper(); wrapperImpl.initialize(address(0)); + implAddr = address(wrapperImpl); } /** @@ -25,26 +24,23 @@ contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Me * @return wrapperAddr The address of the ERC-721 Floor Wrapper */ function createWrapper(address tokenAddr) external returns (address wrapperAddr) { - if (tokenToWrapper[tokenAddr] != address(0)) { - revert WrapperAlreadyCreated(tokenAddr, tokenToWrapper[tokenAddr]); - } - - // Compute the address of the proxy contract using create2 - bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(wrapperImpl)))); - bytes32 salt = keccak256(abi.encodePacked(tokenAddr)); - - // Deploy it - assembly { - wrapperAddr := create2(0, add(code, 32), mload(code), salt) - } - if (wrapperAddr == address(0)) { - revert WrapperCreationFailed(tokenAddr); - } + wrapperAddr = deployProxy(implAddr, tokenAddr); ERC721FloorWrapper(wrapperAddr).initialize(tokenAddr); + emit NewERC721FloorWrapper(tokenAddr); + return wrapperAddr; + } - tokenToWrapper[tokenAddr] = wrapperAddr; + /** + * Return address of the ERC-721 Floor Wrapper for a given token contract + * @param tokenAddr The address of the ERC-721 token contract + * @return wrapperAddr The address of the ERC-721 Floor Wrapper + */ + function tokenToWrapper(address tokenAddr) public view returns (address wrapperAddr) { + wrapperAddr = predictWrapperAddress(implAddr, tokenAddr); + if (!isContract(wrapperAddr)) { + return address(0); + } - emit NewERC721FloorWrapper(tokenAddr); return wrapperAddr; } From e8ae1012e6646e80d3654b296f9bbb11aa462d55 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Mar 2023 11:04:47 +1300 Subject: [PATCH 12/24] Optimise Wrapper initiation --- src/contracts/wrappers/ERC1155FloorFactory.sol | 1 - src/contracts/wrappers/ERC1155FloorWrapper.sol | 4 +--- src/contracts/wrappers/ERC721FloorFactory.sol | 1 - src/contracts/wrappers/ERC721FloorWrapper.sol | 5 +---- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/contracts/wrappers/ERC1155FloorFactory.sol b/src/contracts/wrappers/ERC1155FloorFactory.sol index 5e9cb296..c542b830 100644 --- a/src/contracts/wrappers/ERC1155FloorFactory.sol +++ b/src/contracts/wrappers/ERC1155FloorFactory.sol @@ -14,7 +14,6 @@ contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155 constructor(address _admin) Ownable(_admin) { ERC1155FloorWrapper wrapperImpl = new ERC1155FloorWrapper(); - wrapperImpl.initialize(address(0)); implAddr = address(wrapperImpl); } diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 4d7e8364..1e382b2e 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -22,7 +22,6 @@ contract ERC1155FloorWrapper is IERC1155TokenReceiver, WrapperErrors { - bool private initialized; address internal immutable factory; IERC1155 public token; // This contract only supports a single token id @@ -35,11 +34,10 @@ contract ERC1155FloorWrapper is } function initialize(address tokenAddr) external { - if (initialized || msg.sender != factory) { + if (msg.sender != factory) { revert InvalidInitialization(); } token = IERC1155(tokenAddr); - initialized = true; } modifier onlyDepositing() { diff --git a/src/contracts/wrappers/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol index aca032f1..65ff9f0d 100644 --- a/src/contracts/wrappers/ERC721FloorFactory.sol +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -14,7 +14,6 @@ contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Me constructor(address _admin) Ownable(_admin) { ERC721FloorWrapper wrapperImpl = new ERC721FloorWrapper(); - wrapperImpl.initialize(address(0)); implAddr = address(wrapperImpl); } diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index 9f1dc720..1dad7d63 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -18,8 +18,6 @@ import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; * Therefore each ERC-721 within a collection can be treated as if fungible. */ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Metadata, WrapperErrors { - bool private initialized; - IERC721 public token; address internal immutable factory; // This contract only supports a single token id @@ -30,11 +28,10 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met } function initialize(address tokenAddr) external { - if (initialized || msg.sender != factory) { + if (msg.sender != factory) { revert InvalidInitialization(); } token = IERC721(tokenAddr); - initialized = true; } /** From 36fd7a680d0a7e6d9137af3f140d52a0f54e629c Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 31 Mar 2023 11:42:58 +1300 Subject: [PATCH 13/24] Use calldata in Wrapper funcs --- .../interfaces/IERC1155FloorWrapper.sol | 16 ++++++++++++---- .../interfaces/IERC721FloorWrapper.sol | 4 ++-- src/contracts/wrappers/ERC1155FloorWrapper.sol | 18 ++++++++++++------ src/contracts/wrappers/ERC721FloorWrapper.sol | 4 ++-- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol index 4d6fd34b..cfcd152c 100644 --- a/src/contracts/interfaces/IERC1155FloorWrapper.sol +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -16,8 +16,12 @@ interface IERC1155FloorWrapper is IERC1155 { * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) - external; + function deposit( + uint256[] calldata tokenIds, + uint256[] calldata tokenAmounts, + address recipient, + bytes calldata data + ) external; /** * Unwrap and withdraw ERC-1155 tokens. @@ -26,6 +30,10 @@ interface IERC1155FloorWrapper is IERC1155 { * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) - external; + function withdraw( + uint256[] calldata tokenIds, + uint256[] calldata tokenAmounts, + address recipient, + bytes calldata data + ) external; } diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index 2ab78774..330aa7bf 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -16,7 +16,7 @@ interface IERC721FloorWrapper is IERC1155 { * @notice Users must first approve this contract address on the ERC-721 contract. * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. */ - function deposit(uint256[] memory tokenIds, address recipient, bytes calldata data) external; + function deposit(uint256[] calldata tokenIds, address recipient, bytes calldata data) external; /** * Unwrap and withdraw ERC-721 tokens. @@ -24,5 +24,5 @@ interface IERC721FloorWrapper is IERC1155 { * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(uint256[] memory tokenIds, address recipient, bytes calldata data) external; + function withdraw(uint256[] calldata tokenIds, address recipient, bytes calldata data) external; } diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 1e382b2e..e6baf021 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -63,9 +63,12 @@ contract ERC1155FloorWrapper is * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-1155 contract. */ - function deposit(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) - external - { + function deposit( + uint256[] calldata tokenIds, + uint256[] calldata tokenAmounts, + address recipient, + bytes calldata data + ) external { isDepositing = true; token.safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); delete isDepositing; @@ -86,9 +89,12 @@ contract ERC1155FloorWrapper is * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(uint256[] memory tokenIds, uint256[] memory tokenAmounts, address recipient, bytes calldata data) - external - { + function withdraw( + uint256[] calldata tokenIds, + uint256[] calldata tokenAmounts, + address recipient, + bytes calldata data + ) external { uint256 total; for (uint256 i = 0; i < tokenIds.length; i++) { total += tokenAmounts[i]; diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index 1dad7d63..c9eca361 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -52,7 +52,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met * @param data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. */ - function deposit(uint256[] memory tokenIds, address recipient, bytes calldata data) external { + function deposit(uint256[] calldata tokenIds, address recipient, bytes calldata data) external { uint256 length = tokenIds.length; for (uint256 i; i < length;) { // Intentionally unsafe transfer @@ -72,7 +72,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met * @param recipient The recipient of the unwrapped tokens. * @param data Data to pass to ERC-1155 receiver. */ - function withdraw(uint256[] memory tokenIds, address recipient, bytes calldata data) external { + function withdraw(uint256[] calldata tokenIds, address recipient, bytes calldata data) external { _burn(msg.sender, TOKEN_ID, tokenIds.length); emit TokensWithdrawn(tokenIds); uint256 length = tokenIds.length; From f29aa9010a18106559c7651d839260ad291adfa8 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 3 Apr 2023 07:32:25 +1200 Subject: [PATCH 14/24] Add token set check to init --- .../wrappers/ERC1155FloorWrapper.sol | 2 +- src/contracts/wrappers/ERC721FloorWrapper.sol | 2 +- tests_foundry/ERC1155FloorWrapper.test.sol | 19 ++++++++++++++++ tests_foundry/ERC721FloorWrapper.test.sol | 22 ++++++++++++++++++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index e6baf021..b554cf92 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -34,7 +34,7 @@ contract ERC1155FloorWrapper is } function initialize(address tokenAddr) external { - if (msg.sender != factory) { + if (msg.sender != factory || address(token) != address(0)) { revert InvalidInitialization(); } token = IERC1155(tokenAddr); diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index c9eca361..e5431bc2 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -28,7 +28,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met } function initialize(address tokenAddr) external { - if (msg.sender != factory) { + if (msg.sender != factory || address(token) != address(0)) { revert InvalidInitialization(); } token = IERC721(tokenAddr); diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 3fe0b95a..c2b9711f 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -58,6 +58,25 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { erc1155.setApprovalForAll(wrapperAddr, true); } + // + // Initialization + // + function test_initialize_invalid() public { + wrapper = new ERC1155FloorWrapper(); + + // Invalid caller + vm.expectRevert(InvalidInitialization.selector); + vm.prank(USER); + wrapper.initialize(address(erc1155)); + + // Correct + wrapper.initialize(address(erc1155)); + + // Already init + vm.expectRevert(InvalidInitialization.selector); + wrapper.initialize(address(erc1155)); + } + // // Deposit // diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index 471429fd..f65a9700 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -10,6 +10,7 @@ import {INiftyswapExchange} from "src/contracts/interfaces/INiftyswapExchange.so import {INiftyswapExchange20} from "src/contracts/interfaces/INiftyswapExchange20.sol"; import {NiftyswapFactory} from "src/contracts/exchange/NiftyswapFactory.sol"; import {NiftyswapFactory20} from "src/contracts/exchange/NiftyswapFactory20.sol"; +import {WrapperErrors} from "src/contracts/utils/WrapperErrors.sol"; import {USER, OPERATOR} from "./utils/Constants.test.sol"; import {NiftyswapTestHelper} from "./utils/NiftyswapTestHelper.test.sol"; @@ -19,7 +20,7 @@ import {TestHelperBase} from "./utils/TestHelperBase.test.sol"; import {console} from "forge-std/Test.sol"; import {stdError} from "forge-std/StdError.sol"; -contract ERC721FloorWrapperTest is TestHelperBase { +contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { // Redeclare events event TokensDeposited(uint256[] tokenIds); event TokensWithdrawn(uint256[] tokenIds); @@ -48,6 +49,25 @@ contract ERC721FloorWrapperTest is TestHelperBase { erc721.setApprovalForAll(wrapperAddr, true); } + // + // Initialization + // + function test_initialize_invalid() public { + wrapper = new ERC721FloorWrapper(); + + // Invalid caller + vm.expectRevert(InvalidInitialization.selector); + vm.prank(USER); + wrapper.initialize(address(erc721)); + + // Correct + wrapper.initialize(address(erc721)); + + // Already init + vm.expectRevert(InvalidInitialization.selector); + wrapper.initialize(address(erc721)); + } + // // Deposit // From 65c76d3ab64cd77dc94ca65d6892b25fcc903406 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 3 Apr 2023 09:26:09 +1200 Subject: [PATCH 15/24] Optimise loops --- src/contracts/wrappers/ERC1155FloorWrapper.sol | 13 +++++++++++-- src/contracts/wrappers/ERC721FloorWrapper.sol | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index b554cf92..3749ed6b 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -76,8 +76,13 @@ contract ERC1155FloorWrapper is emit TokensDeposited(tokenIds, tokenAmounts); uint256 total; - for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 length = tokenIds.length; + for (uint256 i; i < length;) { total += tokenAmounts[i]; + unchecked { + // Can never overflow + i++; + } } _mint(recipient, TOKEN_ID, total, data); } @@ -96,8 +101,12 @@ contract ERC1155FloorWrapper is bytes calldata data ) external { uint256 total; - for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 length = tokenIds.length; + for (uint256 i; i < length;) { total += tokenAmounts[i]; + unchecked { + i++; + } } _burn(msg.sender, TOKEN_ID, total); diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index e5431bc2..17ee0d49 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -59,7 +59,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met token.transferFrom(msg.sender, address(this), tokenIds[i]); unchecked { // Can never overflow - ++i; + i++; } } emit TokensDeposited(tokenIds); @@ -80,7 +80,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met token.safeTransferFrom(address(this), recipient, tokenIds[i], data); unchecked { // Can never overflow - ++i; + i++; } } } From f7847e55c3fcf4030fc19bfa51bfcb689039ed9c Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 3 Apr 2023 11:45:14 +1200 Subject: [PATCH 16/24] Fix doc typo --- src/contracts/interfaces/IWrapAndNiftyswap.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/interfaces/IWrapAndNiftyswap.sol b/src/contracts/interfaces/IWrapAndNiftyswap.sol index e3da9f6e..fd6b35fc 100644 --- a/src/contracts/interfaces/IWrapAndNiftyswap.sol +++ b/src/contracts/interfaces/IWrapAndNiftyswap.sol @@ -13,7 +13,7 @@ interface IWrapAndNiftyswap { /** * @notice Accepts only tokenWrapper tokens - * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256[],uint256[],bytes)"))` */ function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes calldata _data) external From e7c5d517393ea29b8c0a44c7ecc222b302272ad7 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 3 Apr 2023 11:50:20 +1200 Subject: [PATCH 17/24] Use receiver hook for ERC1155FloorWrapper --- .../interfaces/IERC1155FloorWrapper.sol | 62 +++--- src/contracts/utils/WrapperErrors.sol | 2 + .../wrappers/ERC1155FloorWrapper.sol | 184 ++++++++++-------- tests_foundry/ERC1155FloorWrapper.test.sol | 150 +++++++++----- 4 files changed, 248 insertions(+), 150 deletions(-) diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol index cfcd152c..693bf06e 100644 --- a/src/contracts/interfaces/IERC1155FloorWrapper.sol +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -2,38 +2,54 @@ pragma solidity ^0.8.4; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol"; -interface IERC1155FloorWrapper is IERC1155 { +interface IERC1155FloorWrapper is IERC1155, IERC1155TokenReceiver { event TokensDeposited(uint256[] tokenIds, uint256[] tokenAmounts); event TokensWithdrawn(uint256[] tokenIds, uint256[] tokenAmounts); + struct DepositRequestObj { + address recipient; + bytes data; + } + + struct WithdrawRequestObj { + uint256[] tokenIds; + uint256[] tokenAmounts; + address recipient; + bytes data; + } + /** - * Deposit and wrap ERC-1155 tokens. - * @param tokenIds The ERC-1155 token ids to deposit. - * @param tokenAmounts The amount of each token to deposit. - * @param recipient The recipient of the wrapped tokens. - * @param data Data to pass to ERC-1155 receiver. - * @notice Users must first approve this contract address on the ERC-1155 contract. + * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. + * @notice Unwrapped ERC-1155 tokens are treated as deposits. Wrapped ERC-1155 tokens are treated as withdrawals. + * @param operator The address which called `safeTransferFrom` function. + * @param from The address which previously owned the token. + * @param id The ID of the token being transferred. + * @param amount The amount of tokens being transferred. + * @param data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` */ - function deposit( - uint256[] calldata tokenIds, - uint256[] calldata tokenAmounts, - address recipient, - bytes calldata data - ) external; + function onERC1155Received(address operator, address from, uint256 id, uint256 amount, bytes calldata data) + external + returns (bytes4); /** - * Unwrap and withdraw ERC-1155 tokens. - * @param tokenIds The ERC-1155 token ids to withdraw. - * @param tokenAmounts The amount of each token to deposit. - * @param recipient The recipient of the unwrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. + * @notice Unwrapped ERC-1155 tokens are treated as deposits. Wrapped ERC-1155 tokens are treated as withdrawals. + * @param operator The address which called `safeTransferFrom` function. + * @param from The address which previously owned the token. + * @param ids The IDs of the tokens being transferred. + * @param amounts The amounts of tokens being transferred. + * @param data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` */ - function withdraw( - uint256[] calldata tokenIds, - uint256[] calldata tokenAmounts, - address recipient, + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata amounts, bytes calldata data - ) external; + ) external returns (bytes4); } diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index 97beb3ba..a15e5090 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -11,6 +11,8 @@ abstract contract WrapperErrors { // ERC1155 error InvalidERC1155Received(); + error InvalidDepositRequest(); + error InvalidWithdrawRequest(); // General error UnsupportedMethod(); diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 3749ed6b..38949f77 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -3,10 +3,9 @@ pragma solidity ^0.8.4; import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; -import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol"; import {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol"; -import {IERC1155FloorWrapper} from "../interfaces/IERC1155FloorWrapper.sol"; +import {IERC1155FloorWrapper, IERC1155TokenReceiver} from "../interfaces/IERC1155FloorWrapper.sol"; import {IERC1155Metadata} from "../interfaces/IERC1155Metadata.sol"; import {IDelegatedERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; import {WrapperErrors} from "../utils/WrapperErrors.sol"; @@ -15,37 +14,21 @@ import {WrapperErrors} from "../utils/WrapperErrors.sol"; * @notice Allows users to wrap any amount of a ERC-1155 token with a 1:1 ratio. * Therefore each ERC-1155 within a collection can be treated as if fungible. */ -contract ERC1155FloorWrapper is - IERC1155FloorWrapper, - ERC1155MintBurn, - IERC1155Metadata, - IERC1155TokenReceiver, - WrapperErrors -{ +contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155Metadata, WrapperErrors { address internal immutable factory; - IERC1155 public token; + address public tokenAddr; // This contract only supports a single token id uint256 public constant TOKEN_ID = 0; - bool private isDepositing; - constructor() { factory = msg.sender; } - function initialize(address tokenAddr) external { - if (msg.sender != factory || address(token) != address(0)) { + function initialize(address _tokenAddr) external { + if (msg.sender != factory || tokenAddr != address(0)) { revert InvalidInitialization(); } - token = IERC1155(tokenAddr); - } - - modifier onlyDepositing() { - if (!isDepositing) { - revert InvalidERC1155Received(); - } - delete isDepositing; - _; + tokenAddr = _tokenAddr; } /** @@ -56,23 +39,75 @@ contract ERC1155FloorWrapper is } /** - * Deposit and wrap ERC-1155 tokens. - * @param tokenIds The ERC-1155 token ids to deposit. - * @param tokenAmounts The amount of each token to deposit. - * @param recipient The recipient of the wrapped tokens. - * @param data Data to pass to ERC-1155 receiver. - * @notice Users must first approve this contract address on the ERC-1155 contract. + * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. + * @param id The ID of the token being transferred. + * @param amount The amount of tokens being transferred. + * @param data Additional data formatted as DepositRequestObj or WithdrawRequestObj. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256[],uint256[],bytes)"))` */ - function deposit( - uint256[] calldata tokenIds, - uint256[] calldata tokenAmounts, - address recipient, + function onERC1155Received(address, address, uint256 id, uint256 amount, bytes calldata data) + external + returns (bytes4) + { + if (msg.sender == tokenAddr) { + // Deposit + uint256[] memory ids = new uint256[](1); + ids[0] = id; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + _deposit(ids, amounts, data); + } else if (msg.sender == address(this)) { + // Withdraw + _withdraw(amount, data); + } else { + revert InvalidERC1155Received(); + } + + return IERC1155TokenReceiver.onERC1155Received.selector; + } + + /** + * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. + * @param ids The IDs of the tokens being transferred. + * @param amounts The amounts of tokens being transferred. + * @param data Additional data formatted as DepositRequestObj or WithdrawRequestObj. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address, + address, + uint256[] calldata ids, + uint256[] calldata amounts, bytes calldata data - ) external { - isDepositing = true; - token.safeBatchTransferFrom(msg.sender, address(this), tokenIds, tokenAmounts, ""); - delete isDepositing; + ) public returns (bytes4) { + if (msg.sender == tokenAddr) { + // Deposit + _deposit(ids, amounts, data); + } else if (msg.sender == address(this)) { + // Withdraw + if (ids.length != 1) { + revert InvalidERC1155Received(); + } + // Either ids[0] == TOKEN_ID or amounts[0] == 0 + _withdraw(amounts[0], data); + } else { + revert InvalidERC1155Received(); + } + + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + /** + * Wrap deposited ERC-1155 tokens. + * @param tokenIds The ERC-1155 token ids deposited. + * @param tokenAmounts The amount of each token deposited. + * @param data Data received during deposit. + */ + function _deposit( + uint256[] memory tokenIds, //FIXME Use calldata with _depositBatch + uint256[] memory tokenAmounts, //FIXME Use calldata with _depositBatch + bytes calldata data + ) private { emit TokensDeposited(tokenIds, tokenAmounts); uint256 total; @@ -84,59 +119,49 @@ contract ERC1155FloorWrapper is i++; } } - _mint(recipient, TOKEN_ID, total, data); + + DepositRequestObj memory obj; + (obj) = abi.decode(data, (DepositRequestObj)); + if (obj.recipient == address(0)) { + // Don't allow deposits to the zero address + revert InvalidDepositRequest(); + } + + _mint(obj.recipient, TOKEN_ID, total, obj.data); } /** - * Unwrap and withdraw ERC-1155 tokens. - * @param tokenIds The ERC-1155 token ids to withdraw. - * @param tokenAmounts The amount of each token to deposit. - * @param recipient The recipient of the unwrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * Unwrap withdrawn ERC-1155 tokens. + * @param amount The amount of wrapped tokens received for withdraw. + * @param data Data received during unwrap ERC-1155 receiver request. */ - function withdraw( - uint256[] calldata tokenIds, - uint256[] calldata tokenAmounts, - address recipient, - bytes calldata data - ) external { + function _withdraw(uint256 amount, bytes calldata data) private { + _burn(address(this), TOKEN_ID, amount); + + WithdrawRequestObj memory obj; + (obj) = abi.decode(data, (WithdrawRequestObj)); + if (obj.recipient == address(0)) { + // Don't allow withdraws to the zero address + revert InvalidWithdrawRequest(); + } + uint256 total; - uint256 length = tokenIds.length; + uint256 length = obj.tokenAmounts.length; for (uint256 i; i < length;) { - total += tokenAmounts[i]; + total += obj.tokenAmounts[i]; unchecked { i++; } } - _burn(msg.sender, TOKEN_ID, total); - - emit TokensWithdrawn(tokenIds, tokenAmounts); - - token.safeBatchTransferFrom(address(this), recipient, tokenIds, tokenAmounts, data); - } - /** - * Handle the receipt of a single ERC-1155 token type. - * @dev This function can only be called when deposits are in progress. - */ - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - onlyDepositing - returns (bytes4) - { - return IERC1155TokenReceiver.onERC1155BatchReceived.selector; - } + if (total != amount) { + revert InvalidWithdrawRequest(); + } - /** - * Handle the receipt of multiple ERC-1155 token types. - * @dev This function can only be called when deposits are in progress. - */ - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - onlyDepositing - returns (bytes4) - { - return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + IERC1155(tokenAddr).safeBatchTransferFrom( + address(this), obj.recipient, obj.tokenIds, obj.tokenAmounts, obj.data + ); + emit TokensWithdrawn(obj.tokenIds, obj.tokenAmounts); } /** @@ -157,6 +182,7 @@ contract ERC1155FloorWrapper is */ function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId - || interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(interfaceId); + || interfaceId == type(IERC1155Metadata).interfaceId || interfaceId == type(IERC1155FloorWrapper).interfaceId + || super.supportsInterface(interfaceId); } } diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index c2b9711f..126b21e7 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -import {ERC1155FloorWrapper} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; +import {IERC1155FloorWrapper, ERC1155FloorWrapper} from "src/contracts/wrappers/ERC1155FloorWrapper.sol"; import {ERC1155FloorFactory} from "src/contracts/wrappers/ERC1155FloorFactory.sol"; import {ERC1155Mock} from "src/contracts/mocks/ERC1155Mock.sol"; import {ERC20TokenMock} from "src/contracts/mocks/ERC20TokenMock.sol"; @@ -50,12 +50,6 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { tokenAmounts[2] = 5; erc1155.batchMintMock(USER, tokenIds, tokenAmounts, ""); erc1155.batchMintMock(OPERATOR, tokenIds, tokenAmounts, ""); - - // Approvals - vm.prank(USER); - erc1155.setApprovalForAll(wrapperAddr, true); - vm.prank(OPERATOR); - erc1155.setApprovalForAll(wrapperAddr, true); } // @@ -93,7 +87,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(tokenIds, tokenAmounts); - wrapper.deposit(tokenIds, tokenAmounts, USER, ""); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); assertEq(beforeWrapperUserBal + 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -117,7 +111,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(tokenIds, tokenAmounts); - wrapper.deposit(tokenIds, tokenAmounts, OPERATOR, ""); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(OPERATOR, "")); assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -142,7 +136,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(tokenIds, tokenAmounts); - wrapper.deposit(tokenIds, tokenAmounts, USER, ""); + erc1155.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeDepositRequest(USER, "")); assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal + 2, erc1155.balanceOf(wrapperAddr, 0)); @@ -166,7 +160,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensDeposited(tokenIds, tokenAmounts); - wrapper.deposit(tokenIds, tokenAmounts, USER, ""); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); assertEq(beforeWrapperUserBal + 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal0 + 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -175,19 +169,6 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { assertEq(beforeERC1155UserBal1 - 1, erc1155.balanceOf(USER, 1)); } - function test_deposit_wrongOwner() external { - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 0; - tokenIds[1] = 5; - uint256[] memory tokenAmounts = new uint256[](2); - tokenAmounts[0] = 1; - tokenAmounts[1] = 1; - - vm.prank(USER); - vm.expectRevert(stdError.arithmeticError); - wrapper.deposit(tokenIds, tokenAmounts, USER, ""); - } - function test_deposit_andSell() public withDeposit { // Niftyswap ERC1155Mock currency = new ERC1155Mock(); @@ -214,8 +195,12 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 0; vm.prank(USER); - wrapper.deposit( - tokenIds, sellAmounts, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp) + erc1155.safeBatchTransferFrom( + USER, + wrapperAddr, + tokenIds, + sellAmounts, + encodeDepositRequest(exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp)) ); // After bals @@ -251,11 +236,17 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 0; vm.prank(USER); - wrapper.deposit( + erc1155.safeBatchTransferFrom( + USER, + wrapperAddr, tokenIds, sellAmounts, - exchangeAddr, - Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], new address[](0), new uint256[](0), block.timestamp) + encodeDepositRequest( + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens( + USER, prices[0], new address[](0), new uint256[](0), block.timestamp + ) + ) ); // After bals @@ -265,6 +256,28 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); } + function test_deposit_invalidData() public { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, ""); + } + + function test_deposit_invalidRecipient() public { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(InvalidDepositRequest.selector); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(address(0), "")); + } + // // Withdraw // @@ -281,7 +294,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds, tokenAmounts); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -305,7 +318,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds, tokenAmounts); - wrapper.withdraw(tokenIds, tokenAmounts, OPERATOR, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, OPERATOR, "")); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -320,7 +333,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); } function test_withdraw_twice() external { @@ -338,7 +351,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds, tokenAmounts); - wrapper.withdraw(tokenIds, tokenAmounts, OPERATOR, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, OPERATOR, "")); assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -358,7 +371,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds, tokenAmounts); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal - 2, erc1155.balanceOf(wrapperAddr, 0)); @@ -382,7 +395,7 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds, tokenAmounts); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); assertEq(beforeWrapperUserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC1155WrapperBal0 - 1, erc1155.balanceOf(wrapperAddr, 0)); @@ -391,7 +404,35 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { assertEq(beforeERC1155UserBal1 + 1, erc1155.balanceOf(USER, 1)); } - function test_withdraw_invalidTokenAddr() external { + function test_withdraw_invalidAmount() external withDeposit { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 2; + + // Too few + vm.prank(USER); + vm.expectRevert(InvalidWithdrawRequest.selector); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + + // Too many + vm.prank(USER); + vm.expectRevert(InvalidWithdrawRequest.selector); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 3, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + } + + function test_withdraw_insufficientBalance() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory tokenAmounts = new uint256[](1); + tokenAmounts[0] = 6; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 6, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + } + + function test_withdraw_invalidData() public { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 0; uint256[] memory tokenAmounts = new uint256[](1); @@ -399,24 +440,30 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectRevert(); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + wrapper.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, ""); } - function test_withdraw_insufficientBalance() external { + function test_withdraw_invalidReceiver() public { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 0; uint256[] memory tokenAmounts = new uint256[](1); tokenAmounts[0] = 1; vm.prank(USER); - vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(tokenIds, tokenAmounts, USER, ""); + vm.expectRevert(); + wrapper.safeBatchTransferFrom( + USER, wrapperAddr, tokenIds, tokenAmounts, encodeWithdrawRequest(tokenIds, tokenAmounts, address(0), "") + ); } // // Transfers // function test_transfers_invalidTransfer() external { + // New token + erc1155 = new ERC1155Mock(); + erc1155.mintMock(USER, 0, 5, ""); + vm.prank(USER); vm.expectRevert(InvalidERC1155Received.selector); erc1155.safeTransferFrom(USER, wrapperAddr, 0, 1, ""); @@ -432,12 +479,6 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { wrapperAddr = address(wrapper); wrapperTokenId = wrapper.TOKEN_ID(); - // Approvals - vm.prank(USER); - erc1155.setApprovalForAll(wrapperAddr, true); - vm.prank(OPERATOR); - erc1155.setApprovalForAll(wrapperAddr, true); - _; } @@ -450,9 +491,9 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { tokenAmounts[1] = 2; vm.prank(USER); - wrapper.deposit(tokenIds, tokenAmounts, USER, ""); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); vm.prank(OPERATOR); - wrapper.deposit(tokenIds, tokenAmounts, OPERATOR, ""); + erc1155.safeBatchTransferFrom(OPERATOR, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(OPERATOR, "")); _; } @@ -510,6 +551,19 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { ); } + function encodeDepositRequest(address recipient, bytes memory data) private pure returns (bytes memory) { + return abi.encode(IERC1155FloorWrapper.DepositRequestObj(recipient, data)); + } + + function encodeWithdrawRequest( + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + address recipient, + bytes memory data + ) private pure returns (bytes memory) { + return abi.encode(IERC1155FloorWrapper.WithdrawRequestObj(tokenIds, tokenAmounts, recipient, data)); + } + /** * Skip a test. */ From c89bf42b9585f0018f81803ec09fd2f628b0c52d Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 6 Apr 2023 10:51:06 +1200 Subject: [PATCH 18/24] Bump version 6.0.2 --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index 7f227eb5..6a65e302 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "@0xsequence/niftyswap", - "version": "6.0.1", + "version": "6.0.2", "description": "Swap protocol for ERC-1155 tokens, inspired by Uniswap", "repository": "https://github.com/0xsequence/niftyswap", "homepage": "https://sequence.build", From 43689340897b55946a308f6a26ac1d699de83335 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 4 May 2023 06:48:22 +1200 Subject: [PATCH 19/24] Add audit for wrappers --- audits/2023_04_Wrappers_Ignacio_Mazzara.md | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 audits/2023_04_Wrappers_Ignacio_Mazzara.md diff --git a/audits/2023_04_Wrappers_Ignacio_Mazzara.md b/audits/2023_04_Wrappers_Ignacio_Mazzara.md new file mode 100644 index 00000000..03809e95 --- /dev/null +++ b/audits/2023_04_Wrappers_Ignacio_Mazzara.md @@ -0,0 +1,167 @@ +# Niftyswap - Security Review + +## Table of contest + +- [Introduction](#introduction) +- [General Notes](#general-notes) +- [Interfaces](#interfaces) + - [IERC1155FloorFactory.sol](#IERC1155FloorFactory.sol) + - [IERC1155FloorWrapper.sol](#IERC1155FloorWrapper.sol) + - [IERC721.sol](#IERC721.sol) + - [IERC721FloorFactory.sol](#IERC721FloorFactory.sol) + - [IERC721FloorWrapper.sol](#IERC721FloorWrapper.sol) + - [IWrapAndNiftyswap.sol](#IWrapAndNiftyswap.sol) +- [Utils](#utils) + - [Proxy.sol](#Proxy.sol) + - [WrapperErrors.sol](#WrapperErrors.sol) + - [WrapperProxyDeployer.sol](#WrapperProxyDeployer.sol) +- [Wrappers](#wrappers) + - [ERC1155FloorFactory.sol](#ERC1155FloorFactory.sol) + - [ERC1155FloorWrapper.sol](#ERC1155FloorWrapper.sol) + - [ERC721FloorFactory.sol](#ERC721FloorFactory.sol) + - [ERC721FloorWrapper.sol](#ERC721FloorWrapper.sol) + - [WrapAndNiftyswap.sol](#WrapAndNiftyswap.sol) -> No + +## Introduction + +0xsequence team requested the review of the contracts under the repository **[niftyswap](https://github.com/0xsequence/niftyswap)** referenced by the commit [dc937eb9ba17e1d4886826fd0579febbe3ecb3ad](https://github.com/0xsequence/niftyswap/pull/79/commits/c89bf42b9585f0018f81803ec09fd2f628b0c52d). Spot at the [PR #79](https://github.com/0xsequence/niftyswap/pull/79/files#diff-447dff6610565178e797d7be963e0fe871eccb314da6a80fe9f6629a0f28184f) the following contracts: _IERC1155FloorFactory.sol_, _IERC1155FloorWrapper.sol_, _IERC721.sol_, _IERC721FloorFactory.sol_, _IERC721FloorWrapper.sol_, _IWrapAndNiftyswap.sol_, _Proxy.sol_, _WrapperErrors.sol_, _WrapperProxyDeployer.sol_, _ERC1155FloorFactory.sol_, _ERC1155FloorWrapper.sol_, _ERC721FloorFactory.sol_, _ERC721FloorWrapper.sol_, _WrapAndNiftyswap.sol_. + +The rest of the contracts in the repositories are assumed to be audited. + +## General Notes + +The _ERC1155FloorWrapper_ wraps and unwraps tokens using the `onERC1155Received` and `onERC1155BatchReceived` functions, but the _ERC721FloorWrapper_ uses specific `deposit` and `withdraw` functions with no support to `onERC721Received`. Using different strategies for the same outcome makes the contract harder to follow and costly for the users in the case of the _ERC721FloorWrapper_. They need an extra transaction to `approve` each token or an `approvalForAll` before depositing. Consider using the same strategy for both wrappers. + +Another thing to have in mind is that the users can claim unwrapped tokens if someone, by mistake, doesn't use the wrappers as expected. This has been addressed before the audit started[here by the team](https://github.com/0xsequence/niftyswap/pull/79#pullrequestreview-1362003788) + +## Interfaces + +### IERC1155FloorFactory.sol + +Nothing found. + +### IERC1155FloorWrapper.sol + +#### Notes + +- N1 - line 32 - Wrong return value in the dev notation. It says `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` and it should be bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) + +### IERC721.sol + +Nothing found. + +### IERC721FloorFactory.sol + +Nothing found. + +### IERC721FloorWrapper.sol + +Nothing found. + +### IWrapAndNiftyswap + +#### Notes + +- N1 - lines 12, 18, and 27 - Parameter names start with `_` while other interfaces use another convention. Consider removing the `_` or adding it to other parameter names interfaces. + +## Utils + +### Proxy.sol + +### Low + +- L1 - line 8 - There is no check whether the implementation is a contract. Consider adding the `isContract` check to prevent human errors. + +#### Notes + +- N1 - line 5 - Consider using the `immutable` keyword for the `implementation` variable since it is not intended to be changed during the contract's lifecycle. This change will reduce gas consumption at deployment and execution. + +### WrapperErrors.sol + +Nothing found. + +### WrapperProxyDeployer.sol + +#### Notes + +- N1 - lines 4, 5, 7, 9 - Unused imports. Consider removing them. + +- N2 - line 18 - Possibility to create a proxy for the zero address. Consider checking that `tokenAddr` is a contract or at least different from the zero address. + +- N3 - lines 20, 21, 22, 23 - Consider removing some operations if there is no need to return specific errors. Checking whether the contract was already created can be done off-chain before and after sending the tx. The `WrapperCreationFailed` may be enough. + +- N4 - line 25 - Wrong comment. `getProxysalt` returns the salt needed for `create2`, not the resultant address. + +- N5 - lines 38, 43, 50, 54, and 58 - Missing dev notation. + + +## Wrappers + +### ERC1155FloorFactory.sol + +#### Low + + +- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. + +#### Notes + +- N1 - 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. + +- N2 - line 15 - Missing dev notation. + + +### ERC1155FloorWrapper.sol + +#### Low + +- L1 - line 30 - There is no check if the contract being set supports the `IERC1155` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. + +- L2 - line 161 - Possible griefing attack when withdrawing specific tokenIds. If the amount of one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. + +#### Notes + +- N1 - lines 23 and 27 - Missing dev notation. + +- N2 - 27 and 174 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. + +- N3 - lines 107 and 108 - Consider removing `FIXME` comments. + +- N4 - line 111 - Gas optimization. The `_deposit` function doesn't care about the `tokenIds` array. The `TokensDeposited` event can be emitted directly in `onERC1155Received` and `onERC1155BatchReceived`. Also, consider moving the recipient check at the beginning of the `_deposit` function to prevent consuming more gas in the case of failure. + + +### ERC721FloorFactory.sol + +#### Low + + +- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. + +#### Notes + +- N1 - lines 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. + +- N2 - line 15 - Missing dev notation. + + +### ERC721FloorWrapper + +### Low + +- L1 - line 30 - There is no check if the contract being set supports the `IERC721` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. + +- L2 - line 55 - Consider checking if the recipient address is not the zero address. + +- L3 - line 80 - Possible griefing attack when withdrawing specific tokenIds. If one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. + +#### Notes + +- N1 - lines 66, 76 and 78 - Consider re-using the `length` variable to reduce gas consumption. + +- N2 - line 73 - Change ERC-1155 to ERC-721. + +- N3 - line 99 - `_id` is the only parameter that starts with `_`. Consider removing the `_` or adding it to other parameter names. + + + +Ignacio Mazzara - April 2023. \ No newline at end of file From e7b6453fbeade20e6ddac0e5a3ee78b29628aeea Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 4 May 2023 08:16:18 +1200 Subject: [PATCH 20/24] Addressed audit comments --- audits/2023_04_Wrappers_Ignacio_Mazzara.md | 58 +++++------ .../interfaces/IERC1155FloorFactory.sol | 8 +- .../interfaces/IERC1155FloorWrapper.sol | 34 +++---- src/contracts/interfaces/IERC721.sol | 18 ++-- .../interfaces/IERC721FloorFactory.sol | 8 +- .../interfaces/IERC721FloorWrapper.sol | 16 ++-- src/contracts/utils/Proxy.sol | 12 +++ src/contracts/utils/WrapperErrors.sol | 1 - src/contracts/utils/WrapperProxyDeployer.sol | 53 ++++++---- .../wrappers/ERC1155FloorFactory.sol | 14 ++- .../wrappers/ERC1155FloorWrapper.sol | 96 ++++++++++--------- src/contracts/wrappers/ERC721FloorFactory.sol | 20 ++-- src/contracts/wrappers/ERC721FloorWrapper.sol | 61 +++++++----- tests_foundry/ERC1155FloorFactory.test.sol | 4 +- tests_foundry/ERC721FloorFactory.test.sol | 4 +- 15 files changed, 231 insertions(+), 176 deletions(-) diff --git a/audits/2023_04_Wrappers_Ignacio_Mazzara.md b/audits/2023_04_Wrappers_Ignacio_Mazzara.md index 03809e95..525c09b7 100644 --- a/audits/2023_04_Wrappers_Ignacio_Mazzara.md +++ b/audits/2023_04_Wrappers_Ignacio_Mazzara.md @@ -1,5 +1,7 @@ # Niftyswap - Security Review +**Responses to comments follow in bold.** + ## Table of contest - [Introduction](#introduction) @@ -44,7 +46,7 @@ Nothing found. #### Notes -- N1 - line 32 - Wrong return value in the dev notation. It says `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` and it should be bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) +- N1 - line 32 - Wrong return value in the dev notation. It says `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` and it should be bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) **ADDRESSED: Fixed docstring.** ### IERC721.sol @@ -62,7 +64,7 @@ Nothing found. #### Notes -- N1 - lines 12, 18, and 27 - Parameter names start with `_` while other interfaces use another convention. Consider removing the `_` or adding it to other parameter names interfaces. +- N1 - lines 12, 18, and 27 - Parameter names start with `_` while other interfaces use another convention. Consider removing the `_` or adding it to other parameter names interfaces. **ADDRESSED: Added _ to new interface function parameters.** ## Utils @@ -70,11 +72,11 @@ Nothing found. ### Low -- L1 - line 8 - There is no check whether the implementation is a contract. Consider adding the `isContract` check to prevent human errors. +- L1 - line 8 - There is no check whether the implementation is a contract. Consider adding the `isContract` check to prevent human errors. **IGNORED: With CREATE2 it's possible that the proxy may be deployed pointing to a known address before the implementation has been deployed.** #### Notes -- N1 - line 5 - Consider using the `immutable` keyword for the `implementation` variable since it is not intended to be changed during the contract's lifecycle. This change will reduce gas consumption at deployment and execution. +- N1 - line 5 - Consider using the `immutable` keyword for the `implementation` variable since it is not intended to be changed during the contract's lifecycle. This change will reduce gas consumption at deployment and execution. **IGNORED: Assembly access to immutable variables is not supported in solidity 0.8.4.** ### WrapperErrors.sol @@ -84,15 +86,15 @@ Nothing found. #### Notes -- N1 - lines 4, 5, 7, 9 - Unused imports. Consider removing them. +- N1 - lines 4, 5, 7, 9 - Unused imports. Consider removing them. **ADDRESSED: Removed unused imports.** -- N2 - line 18 - Possibility to create a proxy for the zero address. Consider checking that `tokenAddr` is a contract or at least different from the zero address. +- N2 - line 18 - Possibility to create a proxy for the zero address. Consider checking that `tokenAddr` is a contract or at least different from the zero address. **IGNORED: With CREATE2 it's possible that the proxy may be deployed pointing to a known address before the implementation has been deployed.** -- N3 - lines 20, 21, 22, 23 - Consider removing some operations if there is no need to return specific errors. Checking whether the contract was already created can be done off-chain before and after sending the tx. The `WrapperCreationFailed` may be enough. +- N3 - lines 20, 21, 22, 23 - Consider removing some operations if there is no need to return specific errors. Checking whether the contract was already created can be done off-chain before and after sending the tx. The `WrapperCreationFailed` may be enough. **ADDRESSED: Removed additional checks.** -- N4 - line 25 - Wrong comment. `getProxysalt` returns the salt needed for `create2`, not the resultant address. +- N4 - line 25 - Wrong comment. `getProxysalt` returns the salt needed for `create2`, not the resultant address. **ADDRESSED: Comment removed.** -- N5 - lines 38, 43, 50, 54, and 58 - Missing dev notation. +- N5 - lines 38, 43, 50, 54, and 58 - Missing dev notation. **ADDRESSED: Documentation added.** ## Wrappers @@ -102,32 +104,32 @@ Nothing found. #### Low -- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. +- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. **IGNORED: This can be performed off-chain. In the event of user error the function can be called again to fix the issue. This is consistent with the existing NiftyswapFactory20.sol** #### Notes -- N1 - 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. +- N1 - 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. **ADDRESSED: Removed _ and updated variable name.** -- N2 - line 15 - Missing dev notation. +- N2 - line 15 - Missing dev notation. **ADDRESSED: Added documentation.** ### ERC1155FloorWrapper.sol #### Low -- L1 - line 30 - There is no check if the contract being set supports the `IERC1155` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. +- L1 - line 30 - There is no check if the contract being set supports the `IERC1155` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. **IGNORED: With CREATE2 it's possible that the wrapper may be deployed pointing to a known address before the token has been deployed.** -- L2 - line 161 - Possible griefing attack when withdrawing specific tokenIds. If the amount of one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. +- L2 - line 161 - Possible griefing attack when withdrawing specific tokenIds. If the amount of one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. **IGNORED: This is desired behavour and mirrors existing Niftyswap Exchanges.** #### Notes -- N1 - lines 23 and 27 - Missing dev notation. +- N1 - lines 23 and 27 - Missing dev notation. **ADDRESSED: Added documentation.** -- N2 - 27 and 174 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. +- N2 - 27 and 174 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. **ADDRESSED: Added _ to all function parameters.** -- N3 - lines 107 and 108 - Consider removing `FIXME` comments. +- N3 - lines 107 and 108 - Consider removing `FIXME` comments. **ADDRESSED: Removed comments.** -- N4 - line 111 - Gas optimization. The `_deposit` function doesn't care about the `tokenIds` array. The `TokensDeposited` event can be emitted directly in `onERC1155Received` and `onERC1155BatchReceived`. Also, consider moving the recipient check at the beginning of the `_deposit` function to prevent consuming more gas in the case of failure. +- N4 - line 111 - Gas optimization. The `_deposit` function doesn't care about the `tokenIds` array. The `TokensDeposited` event can be emitted directly in `onERC1155Received` and `onERC1155BatchReceived`. Also, consider moving the recipient check at the beginning of the `_deposit` function to prevent consuming more gas in the case of failure. **ADDRESSED: Followed advice.** ### ERC721FloorFactory.sol @@ -135,33 +137,33 @@ Nothing found. #### Low -- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. +- L1 - line 55 - There is no check if the contract being set supports the `IERC1155Metadata` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. **IGNORED: This can be performed off-chain. In the event of user error the function can be called again to fix the issue. This is consistent with the existing NiftyswapFactory20.sol** #### Notes -- N1 - lines 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. +- N1 - lines 15 and 55 - Function parameter names start with `_` while others do not. Consider removing the `_` or adding it to the rest. **ADDRESSED: Added _ to all function parameters.** -- N2 - line 15 - Missing dev notation. +- N2 - line 15 - Missing dev notation. **ADDRESSED: Added documentation.** ### ERC721FloorWrapper ### Low -- L1 - line 30 - There is no check if the contract being set supports the `IERC721` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. +- L1 - line 30 - There is no check if the contract being set supports the `IERC721` interface and/or if it is at least a contract. Consider adding those checks to prevent human errors. **IGNORED: With CREATE2 it's possible that the wrapper may be deployed pointing to a known address before the token has been deployed.** -- L2 - line 55 - Consider checking if the recipient address is not the zero address. +- L2 - line 55 - Consider checking if the recipient address is not the zero address. **Addressed: Added zero address check.** -- L3 - line 80 - Possible griefing attack when withdrawing specific tokenIds. If one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. +- L3 - line 80 - Possible griefing attack when withdrawing specific tokenIds. If one of the tokenIds desired is not available, the whole transaction will fail. Consider removing the usage of tokenIds. **IGNORED: This is desired behavour and mirrors existing Niftyswap Exchanges.** #### Notes -- N1 - lines 66, 76 and 78 - Consider re-using the `length` variable to reduce gas consumption. +- N1 - lines 66, 76 and 78 - Consider re-using the `length` variable to reduce gas consumption. **ADDRESSED: Reused length variable.** -- N2 - line 73 - Change ERC-1155 to ERC-721. +- N2 - line 73 - Change ERC-1155 to ERC-721. **ADDRESSED: Fixed documentation.** -- N3 - line 99 - `_id` is the only parameter that starts with `_`. Consider removing the `_` or adding it to other parameter names. +- N3 - line 99 - `_id` is the only parameter that starts with `_`. Consider removing the `_` or adding it to other parameter names. **ADDRESSED: Added _ to all function parameters.** -Ignacio Mazzara - April 2023. \ No newline at end of file +Ignacio Mazzara - April 2023. diff --git a/src/contracts/interfaces/IERC1155FloorFactory.sol b/src/contracts/interfaces/IERC1155FloorFactory.sol index 388a9353..1ffa8b67 100644 --- a/src/contracts/interfaces/IERC1155FloorFactory.sol +++ b/src/contracts/interfaces/IERC1155FloorFactory.sol @@ -7,15 +7,15 @@ interface IERC1155FloorFactory { /** * Creates an ERC-1155 Floor Wrapper for given token contract - * @param tokenAddr The address of the ERC-1155 token contract + * @param _tokenAddr The address of the ERC-1155 token contract * @return The address of the ERC-1155 Floor Wrapper */ - function createWrapper(address tokenAddr) external returns (address); + function createWrapper(address _tokenAddr) external returns (address); /** * Return address of the ERC-1155 Floor Wrapper for a given token contract - * @param tokenAddr The address of the ERC-1155 token contract + * @param _tokenAddr The address of the ERC-1155 token contract * @return The address of the ERC-1155 Floor Wrapper */ - function tokenToWrapper(address tokenAddr) external view returns (address); + function tokenToWrapper(address _tokenAddr) external view returns (address); } diff --git a/src/contracts/interfaces/IERC1155FloorWrapper.sol b/src/contracts/interfaces/IERC1155FloorWrapper.sol index 693bf06e..cd11d0ba 100644 --- a/src/contracts/interfaces/IERC1155FloorWrapper.sol +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -24,32 +24,32 @@ interface IERC1155FloorWrapper is IERC1155, IERC1155TokenReceiver { /** * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. * @notice Unwrapped ERC-1155 tokens are treated as deposits. Wrapped ERC-1155 tokens are treated as withdrawals. - * @param operator The address which called `safeTransferFrom` function. - * @param from The address which previously owned the token. - * @param id The ID of the token being transferred. - * @param amount The amount of tokens being transferred. - * @param data Additional data with no specified format. - * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * @param _operator The address which called `safeTransferFrom` function. + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _amount The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)` */ - function onERC1155Received(address operator, address from, uint256 id, uint256 amount, bytes calldata data) + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes calldata _data) external returns (bytes4); /** * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. * @notice Unwrapped ERC-1155 tokens are treated as deposits. Wrapped ERC-1155 tokens are treated as withdrawals. - * @param operator The address which called `safeTransferFrom` function. - * @param from The address which previously owned the token. - * @param ids The IDs of the tokens being transferred. - * @param amounts The amounts of tokens being transferred. - * @param data Additional data with no specified format. + * @param _operator The address which called `safeTransferFrom` function. + * @param _from The address which previously owned the token. + * @param _ids The IDs of the tokens being transferred. + * @param _amounts The amounts of tokens being transferred. + * @param _data Additional data with no specified format. * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` */ function onERC1155BatchReceived( - address operator, - address from, - uint256[] calldata ids, - uint256[] calldata amounts, - bytes calldata data + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data ) external returns (bytes4); } diff --git a/src/contracts/interfaces/IERC721.sol b/src/contracts/interfaces/IERC721.sol index b8cb933d..75bd627d 100644 --- a/src/contracts/interfaces/IERC721.sol +++ b/src/contracts/interfaces/IERC721.sol @@ -6,13 +6,13 @@ interface IERC721 { event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); - function balanceOf(address owner) external view returns (uint256 balance); - function ownerOf(uint256 tokenId) external view returns (address owner); - function safeTransferFrom(address from, address to, uint256 tokenId) external; - function transferFrom(address from, address to, uint256 tokenId) external; - function approve(address to, uint256 tokenId) external; - function getApproved(uint256 tokenId) external view returns (address operator); - function setApprovalForAll(address operator, bool _approved) external; - function isApprovedForAll(address owner, address operator) external view returns (bool); - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function balanceOf(address _owner) external view returns (uint256 balance); + function ownerOf(uint256 _tokenId) external view returns (address owner); + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external; + function transferFrom(address _from, address _to, uint256 _tokenId) external; + function approve(address _to, uint256 _tokenId) external; + function getApproved(uint256 _tokenId) external view returns (address operator); + function setApprovalForAll(address _operator, bool _approved) external; + function isApprovedForAll(address _owner, address _operator) external view returns (bool); + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external; } diff --git a/src/contracts/interfaces/IERC721FloorFactory.sol b/src/contracts/interfaces/IERC721FloorFactory.sol index 59b6adae..7fc107dc 100644 --- a/src/contracts/interfaces/IERC721FloorFactory.sol +++ b/src/contracts/interfaces/IERC721FloorFactory.sol @@ -7,15 +7,15 @@ interface IERC721FloorFactory { /** * Creates an ERC-721 Floor Wrapper for given token contract - * @param tokenAddr The address of the ERC-721 token contract + * @param _tokenAddr The address of the ERC-721 token contract * @return The address of the ERC-721 Floor Wrapper */ - function createWrapper(address tokenAddr) external returns (address); + function createWrapper(address _tokenAddr) external returns (address); /** * Return address of the ERC-721 Floor Wrapper for a given token contract - * @param tokenAddr The address of the ERC-721 token contract + * @param _tokenAddr The address of the ERC-721 token contract * @return The address of the ERC-721 Floor Wrapper */ - function tokenToWrapper(address tokenAddr) external view returns (address); + function tokenToWrapper(address _tokenAddr) external view returns (address); } diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index 330aa7bf..570b355a 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -10,19 +10,19 @@ interface IERC721FloorWrapper is IERC1155 { /** * Deposit and wrap ERC-721 tokens. - * @param tokenIds The ERC-721 token ids to deposit. - * @param recipient The recipient of the wrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * @param _tokenIds The ERC-721 token ids to deposit. + * @param _recipient The recipient of the wrapped tokens. + * @param _data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. */ - function deposit(uint256[] calldata tokenIds, address recipient, bytes calldata data) external; + function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external; /** * Unwrap and withdraw ERC-721 tokens. - * @param tokenIds The ERC-721 token ids to withdraw. - * @param recipient The recipient of the unwrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * @param _tokenIds The ERC-721 token ids to withdraw. + * @param _recipient The recipient of the unwrapped tokens. + * @param _data Data to pass to ERC-1155 receiver. */ - function withdraw(uint256[] calldata tokenIds, address recipient, bytes calldata data) external; + function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external; } diff --git a/src/contracts/utils/Proxy.sol b/src/contracts/utils/Proxy.sol index 3f92d05d..f72053bd 100644 --- a/src/contracts/utils/Proxy.sol +++ b/src/contracts/utils/Proxy.sol @@ -4,18 +4,30 @@ pragma solidity ^0.8.4; contract Proxy { address public implementation; + /** + * Initializes the contract, setting proxy implementation address. + */ constructor(address _implementation) { implementation = _implementation; } + /** + * Forward calls to the proxy implementation contract. + */ receive() external payable { proxy(); } + /** + * Forward calls to the proxy implementation contract. + */ fallback() external payable { proxy(); } + /** + * Forward calls to the proxy implementation contract. + */ function proxy() private { address target; assembly { diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index a15e5090..008a03f7 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.4; */ abstract contract WrapperErrors { // Factories - error WrapperAlreadyCreated(address tokenAddr, address wrapperAddr); error WrapperCreationFailed(address tokenAddr); // ERC1155 diff --git a/src/contracts/utils/WrapperProxyDeployer.sol b/src/contracts/utils/WrapperProxyDeployer.sol index 6052964b..474b85c3 100644 --- a/src/contracts/utils/WrapperProxyDeployer.sol +++ b/src/contracts/utils/WrapperProxyDeployer.sol @@ -1,12 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; -import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.sol"; import {WrapperErrors} from "../utils/WrapperErrors.sol"; -import {Ownable} from "../utils/Ownable.sol"; import {Proxy} from "../utils/Proxy.sol"; -import {IDelegatedERC1155Metadata, IERC1155Metadata} from "../interfaces/IDelegatedERC1155Metadata.sol"; abstract contract WrapperProxyDeployer is WrapperErrors { /** @@ -17,45 +13,66 @@ abstract contract WrapperProxyDeployer is WrapperErrors { */ function deployProxy(address implAddr, address tokenAddr) internal returns (address proxyAddr) { bytes memory code = getProxyCode(implAddr); - implAddr = predictWrapperAddress(code, tokenAddr); - if (isContract(implAddr)) { - revert WrapperAlreadyCreated(tokenAddr, implAddr); - } - - // Compute the address of the proxy contract using create2 bytes32 salt = getProxySalt(tokenAddr); // Deploy it assembly { - implAddr := create2(0, add(code, 32), mload(code), salt) + proxyAddr := create2(0, add(code, 32), mload(code), salt) } - if (implAddr == address(0)) { + if (proxyAddr == address(0)) { revert WrapperCreationFailed(tokenAddr); } - return implAddr; + return proxyAddr; } - function predictWrapperAddress(address implAddr, address tokenAddr) internal view returns (address) { + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param implAddr The address of the proxy implementation + * @param tokenAddr The address of the token contract + * @return proxyAddr The address of the deployed wrapper + */ + function predictWrapperAddress(address implAddr, address tokenAddr) internal view returns (address proxyAddr) { bytes memory code = getProxyCode(implAddr); return predictWrapperAddress(code, tokenAddr); } - function predictWrapperAddress(bytes memory code, address tokenAddr) private view returns (address) { + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param code The code of the wrapper implementation + * @param tokenAddr The address of the token contract + * @return proxyAddr The address of the deployed wrapper + */ + function predictWrapperAddress(bytes memory code, address tokenAddr) private view returns (address proxyAddr) { bytes32 salt = getProxySalt(tokenAddr); address deployer = address(this); bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, keccak256(code))); return address(uint160(uint256(_data))); } - function getProxyCode(address implAddr) private pure returns (bytes memory) { + /** + * Returns the code of the proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @return code The code of the proxy contract + */ + function getProxyCode(address implAddr) private pure returns (bytes memory code) { return abi.encodePacked(type(Proxy).creationCode, uint256(uint160(address(implAddr)))); } - function getProxySalt(address tokenAddr) private pure returns (bytes32) { + /** + * Returns the salt for the proxy contract for a given token contract + * @param tokenAddr The address of the token contract + * @return salt The salt for the proxy contract + */ + function getProxySalt(address tokenAddr) private pure returns (bytes32 salt) { return keccak256(abi.encodePacked(tokenAddr)); } - function isContract(address addr) internal view returns (bool) { + /** + * Checks if an address is a contract. + * @param addr The address to check + * @return result True if the address is a contract + */ + function isContract(address addr) internal view returns (bool result) { uint256 csize; // solhint-disable-next-line no-inline-assembly assembly { diff --git a/src/contracts/wrappers/ERC1155FloorFactory.sol b/src/contracts/wrappers/ERC1155FloorFactory.sol index c542b830..9aa989e6 100644 --- a/src/contracts/wrappers/ERC1155FloorFactory.sol +++ b/src/contracts/wrappers/ERC1155FloorFactory.sol @@ -12,7 +12,11 @@ contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155 address private immutable implAddr; // Address of the wrapper implementation IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract - constructor(address _admin) Ownable(_admin) { + /** + * Creates an ERC-1155 Floor Factory. + * @param admin The address of the admin of the factory + */ + constructor(address admin) Ownable(admin) { ERC1155FloorWrapper wrapperImpl = new ERC1155FloorWrapper(); implAddr = address(wrapperImpl); } @@ -50,11 +54,11 @@ contract ERC1155FloorFactory is IERC1155FloorFactory, Ownable, IDelegatedERC1155 /** * Changes the implementation of the ERC-1155 Metadata contract * @dev This function changes the implementation for all child wrapper of this factory - * @param _contract The address of the ERC-1155 Metadata contract + * @param metadataAddr The address of the ERC-1155 Metadata contract */ - function setMetadataContract(IERC1155Metadata _contract) external onlyOwner { - emit MetadataContractChanged(address(_contract)); - metadataContract = _contract; + function setMetadataContract(IERC1155Metadata metadataAddr) external onlyOwner { + emit MetadataContractChanged(address(metadataAddr)); + metadataContract = metadataAddr; } /** diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 38949f77..d6945e3c 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -20,10 +20,19 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M // This contract only supports a single token id uint256 public constant TOKEN_ID = 0; + /** + * Creates an ERC-1155 Floor Factory. + * @dev This contract is expected to be deployed by the ERC-1155 Floor Factory. + */ constructor() { factory = msg.sender; } + /** + * Initializes the contract with the token address. + * @param _tokenAddr The address of the ERC-1155 token to wrap. + * @dev This is expected to be called immediately after contract creation. + */ function initialize(address _tokenAddr) external { if (msg.sender != factory || tokenAddr != address(0)) { revert InvalidInitialization(); @@ -40,25 +49,26 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M /** * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. - * @param id The ID of the token being transferred. - * @param amount The amount of tokens being transferred. - * @param data Additional data formatted as DepositRequestObj or WithdrawRequestObj. + * @param _id The ID of the token being transferred. + * @param _amount The amount of tokens being transferred. + * @param _data Additional data formatted as DepositRequestObj or WithdrawRequestObj. * @return `bytes4(keccak256("onERC1155Received(address,address,uint256[],uint256[],bytes)"))` */ - function onERC1155Received(address, address, uint256 id, uint256 amount, bytes calldata data) + function onERC1155Received(address, address, uint256 _id, uint256 _amount, bytes calldata _data) external returns (bytes4) { if (msg.sender == tokenAddr) { // Deposit uint256[] memory ids = new uint256[](1); - ids[0] = id; + ids[0] = _id; uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - _deposit(ids, amounts, data); + amounts[0] = _amount; + emit TokensDeposited(ids, amounts); + _deposit(amounts, _data); } else if (msg.sender == address(this)) { // Withdraw - _withdraw(amount, data); + _withdraw(_amount, _data); } else { revert InvalidERC1155Received(); } @@ -68,28 +78,29 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M /** * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. - * @param ids The IDs of the tokens being transferred. - * @param amounts The amounts of tokens being transferred. - * @param data Additional data formatted as DepositRequestObj or WithdrawRequestObj. + * @param _ids The IDs of the tokens being transferred. + * @param _amounts The amounts of tokens being transferred. + * @param _data Additional data formatted as DepositRequestObj or WithdrawRequestObj. * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` */ function onERC1155BatchReceived( address, address, - uint256[] calldata ids, - uint256[] calldata amounts, - bytes calldata data + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data ) public returns (bytes4) { if (msg.sender == tokenAddr) { // Deposit - _deposit(ids, amounts, data); + emit TokensDeposited(_ids, _amounts); + _deposit(_amounts, _data); } else if (msg.sender == address(this)) { // Withdraw - if (ids.length != 1) { + if (_ids.length != 1) { revert InvalidERC1155Received(); } // Either ids[0] == TOKEN_ID or amounts[0] == 0 - _withdraw(amounts[0], data); + _withdraw(_amounts[0], _data); } else { revert InvalidERC1155Received(); } @@ -99,47 +110,40 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M /** * Wrap deposited ERC-1155 tokens. - * @param tokenIds The ERC-1155 token ids deposited. - * @param tokenAmounts The amount of each token deposited. - * @param data Data received during deposit. + * @param _tokenAmounts The amount of each token deposited. + * @param _data Data received during deposit. */ - function _deposit( - uint256[] memory tokenIds, //FIXME Use calldata with _depositBatch - uint256[] memory tokenAmounts, //FIXME Use calldata with _depositBatch - bytes calldata data - ) private { - emit TokensDeposited(tokenIds, tokenAmounts); + function _deposit(uint256[] memory _tokenAmounts, bytes calldata _data) private { + DepositRequestObj memory obj; + (obj) = abi.decode(_data, (DepositRequestObj)); + if (obj.recipient == address(0)) { + // Don't allow deposits to the zero address + revert InvalidDepositRequest(); + } uint256 total; - uint256 length = tokenIds.length; + uint256 length = _tokenAmounts.length; for (uint256 i; i < length;) { - total += tokenAmounts[i]; + total += _tokenAmounts[i]; unchecked { // Can never overflow i++; } } - DepositRequestObj memory obj; - (obj) = abi.decode(data, (DepositRequestObj)); - if (obj.recipient == address(0)) { - // Don't allow deposits to the zero address - revert InvalidDepositRequest(); - } - _mint(obj.recipient, TOKEN_ID, total, obj.data); } /** * Unwrap withdrawn ERC-1155 tokens. - * @param amount The amount of wrapped tokens received for withdraw. - * @param data Data received during unwrap ERC-1155 receiver request. + * @param _amount The amount of wrapped tokens received for withdraw. + * @param _data Data received during unwrap ERC-1155 receiver request. */ - function _withdraw(uint256 amount, bytes calldata data) private { - _burn(address(this), TOKEN_ID, amount); + function _withdraw(uint256 _amount, bytes calldata _data) private { + _burn(address(this), TOKEN_ID, _amount); WithdrawRequestObj memory obj; - (obj) = abi.decode(data, (WithdrawRequestObj)); + (obj) = abi.decode(_data, (WithdrawRequestObj)); if (obj.recipient == address(0)) { // Don't allow withdraws to the zero address revert InvalidWithdrawRequest(); @@ -154,7 +158,7 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M } } - if (total != amount) { + if (total != _amount) { revert InvalidWithdrawRequest(); } @@ -177,12 +181,12 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M /** * Query if a contract supports an interface. - * @param interfaceId The interfaceId to test. + * @param _interfaceId The interfaceId to test. * @return supported Whether the interfaceId is supported. */ - function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { - return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId - || interfaceId == type(IERC1155Metadata).interfaceId || interfaceId == type(IERC1155FloorWrapper).interfaceId - || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(IERC1155).interfaceId + || _interfaceId == type(IERC1155Metadata).interfaceId || _interfaceId == type(IERC1155FloorWrapper).interfaceId + || super.supportsInterface(_interfaceId); } } diff --git a/src/contracts/wrappers/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol index 65ff9f0d..80c41c0b 100644 --- a/src/contracts/wrappers/ERC721FloorFactory.sol +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -12,6 +12,10 @@ contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Me address private immutable implAddr; // Address of the wrapper implementation IERC1155Metadata internal metadataContract; // Address of the ERC-1155 Metadata contract + /** + * Creates an ERC-721 Floor Factory. + * @param _admin The address of the admin of the factory + */ constructor(address _admin) Ownable(_admin) { ERC721FloorWrapper wrapperImpl = new ERC721FloorWrapper(); implAddr = address(wrapperImpl); @@ -19,23 +23,23 @@ contract ERC721FloorFactory is IERC721FloorFactory, Ownable, IDelegatedERC1155Me /** * Creates an ERC-721 Floor Wrapper for given token contract - * @param tokenAddr The address of the ERC-721 token contract + * @param _tokenAddr The address of the ERC-721 token contract * @return wrapperAddr The address of the ERC-721 Floor Wrapper */ - function createWrapper(address tokenAddr) external returns (address wrapperAddr) { - wrapperAddr = deployProxy(implAddr, tokenAddr); - ERC721FloorWrapper(wrapperAddr).initialize(tokenAddr); - emit NewERC721FloorWrapper(tokenAddr); + function createWrapper(address _tokenAddr) external returns (address wrapperAddr) { + wrapperAddr = deployProxy(implAddr, _tokenAddr); + ERC721FloorWrapper(wrapperAddr).initialize(_tokenAddr); + emit NewERC721FloorWrapper(_tokenAddr); return wrapperAddr; } /** * Return address of the ERC-721 Floor Wrapper for a given token contract - * @param tokenAddr The address of the ERC-721 token contract + * @param _tokenAddr The address of the ERC-721 token contract * @return wrapperAddr The address of the ERC-721 Floor Wrapper */ - function tokenToWrapper(address tokenAddr) public view returns (address wrapperAddr) { - wrapperAddr = predictWrapperAddress(implAddr, tokenAddr); + function tokenToWrapper(address _tokenAddr) public view returns (address wrapperAddr) { + wrapperAddr = predictWrapperAddress(implAddr, _tokenAddr); if (!isContract(wrapperAddr)) { return address(0); } diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index 17ee0d49..974c96f8 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -23,15 +23,24 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met // This contract only supports a single token id uint256 public constant TOKEN_ID = 0; + /** + * Creates an ERC-721 Floor Factory. + * @dev This contract is expected to be deployed by the ERC-721 Floor Factory. + */ constructor() { factory = msg.sender; } - function initialize(address tokenAddr) external { + /** + * Initializes the contract with the token address. + * @param _tokenAddr The address of the ERC-721 token to wrap. + * @dev This is expected to be called immediately after contract creation. + */ + function initialize(address _tokenAddr) external { if (msg.sender != factory || address(token) != address(0)) { revert InvalidInitialization(); } - token = IERC721(tokenAddr); + token = IERC721(_tokenAddr); } /** @@ -47,37 +56,45 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met /** * Deposit and wrap ERC-721 tokens. - * @param tokenIds The ERC-721 token ids to deposit. - * @param recipient The recipient of the wrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * @param _tokenIds The ERC-721 token ids to deposit. + * @param _recipient The recipient of the wrapped tokens. + * @param _data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. */ - function deposit(uint256[] calldata tokenIds, address recipient, bytes calldata data) external { - uint256 length = tokenIds.length; + function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external { + if (_recipient == address(0)) { + revert InvalidDepositRequest(); + } + + uint256 length = _tokenIds.length; for (uint256 i; i < length;) { // Intentionally unsafe transfer - token.transferFrom(msg.sender, address(this), tokenIds[i]); + token.transferFrom(msg.sender, address(this), _tokenIds[i]); unchecked { // Can never overflow i++; } } - emit TokensDeposited(tokenIds); - _mint(recipient, TOKEN_ID, tokenIds.length, data); + emit TokensDeposited(_tokenIds); + _mint(_recipient, TOKEN_ID, length, _data); } /** * Unwrap and withdraw ERC-721 tokens. - * @param tokenIds The ERC-721 token ids to withdraw. - * @param recipient The recipient of the unwrapped tokens. - * @param data Data to pass to ERC-1155 receiver. + * @param _tokenIds The ERC-721 token ids to withdraw. + * @param _recipient The recipient of the unwrapped tokens. + * @param _data Data to pass to ERC-721 receiver. */ - function withdraw(uint256[] calldata tokenIds, address recipient, bytes calldata data) external { - _burn(msg.sender, TOKEN_ID, tokenIds.length); - emit TokensWithdrawn(tokenIds); - uint256 length = tokenIds.length; + function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external { + if (_recipient == address(0)) { + revert InvalidWithdrawRequest(); + } + + uint256 length = _tokenIds.length; + _burn(msg.sender, TOKEN_ID, length); + emit TokensWithdrawn(_tokenIds); for (uint256 i; i < length;) { - token.safeTransferFrom(address(this), recipient, tokenIds[i], data); + token.safeTransferFrom(address(this), _recipient, _tokenIds[i], _data); unchecked { // Can never overflow i++; @@ -102,11 +119,11 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met /** * Query if a contract supports an interface. - * @param interfaceId The interfaceId to test. + * @param _interfaceId The interfaceId to test. * @return supported Whether the interfaceId is supported. */ - function supportsInterface(bytes4 interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { - return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155).interfaceId - || interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(IERC1155).interfaceId + || _interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(_interfaceId); } } diff --git a/tests_foundry/ERC1155FloorFactory.test.sol b/tests_foundry/ERC1155FloorFactory.test.sol index 500d0e07..b038cb7e 100644 --- a/tests_foundry/ERC1155FloorFactory.test.sol +++ b/tests_foundry/ERC1155FloorFactory.test.sol @@ -40,9 +40,7 @@ contract ERC1155FloorFactoryTest is TestHelperBase, IERC1155FloorFactory, Wrappe address tokenAddr = address(1); test_createWrapper_happyPath(tokenAddr); - vm.expectRevert( - abi.encodeWithSelector(WrapperAlreadyCreated.selector, tokenAddr, factory.tokenToWrapper(tokenAddr)) - ); + vm.expectRevert(abi.encodeWithSelector(WrapperCreationFailed.selector, tokenAddr)); factory.createWrapper(tokenAddr); } diff --git a/tests_foundry/ERC721FloorFactory.test.sol b/tests_foundry/ERC721FloorFactory.test.sol index 64259f7b..aa66838f 100644 --- a/tests_foundry/ERC721FloorFactory.test.sol +++ b/tests_foundry/ERC721FloorFactory.test.sol @@ -40,9 +40,7 @@ contract ERC721FloorFactoryTest is TestHelperBase, IERC721FloorFactory, WrapperE address tokenAddr = address(1); test_createWrapper_happyPath(tokenAddr); - vm.expectRevert( - abi.encodeWithSelector(WrapperAlreadyCreated.selector, tokenAddr, factory.tokenToWrapper(tokenAddr)) - ); + vm.expectRevert(abi.encodeWithSelector(WrapperCreationFailed.selector, tokenAddr)); factory.createWrapper(tokenAddr); } From eec33792a6e87ec621edc48bfea1e369527240cf Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 8 May 2023 07:04:13 +1200 Subject: [PATCH 21/24] Use ERC1967 Proxy --- src/contracts/utils/IERC1967.sol | 8 ++++ src/contracts/utils/Proxy.sol | 15 +++---- src/contracts/utils/StorageSlot.sol | 64 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 src/contracts/utils/IERC1967.sol create mode 100644 src/contracts/utils/StorageSlot.sol diff --git a/src/contracts/utils/IERC1967.sol b/src/contracts/utils/IERC1967.sol new file mode 100644 index 00000000..100a48ea --- /dev/null +++ b/src/contracts/utils/IERC1967.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +interface IERC1967 { + event Upgraded(address indexed implementation); + event AdminChanged(address previousAdmin, address newAdmin); + event BeaconUpgraded(address indexed beacon); +} diff --git a/src/contracts/utils/Proxy.sol b/src/contracts/utils/Proxy.sol index f72053bd..b4e1e5aa 100644 --- a/src/contracts/utils/Proxy.sol +++ b/src/contracts/utils/Proxy.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; -contract Proxy { - address public implementation; +import {IERC1967} from "./IERC1967.sol"; +import {StorageSlot} from "./StorageSlot.sol"; + +contract Proxy is IERC1967 { + bytes32 private constant _IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); /** * Initializes the contract, setting proxy implementation address. */ constructor(address _implementation) { - implementation = _implementation; + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = _implementation; + emit Upgraded(_implementation); } /** @@ -29,10 +33,7 @@ contract Proxy { * Forward calls to the proxy implementation contract. */ function proxy() private { - address target; - assembly { - target := sload(implementation.slot) - } + address target = StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) diff --git a/src/contracts/utils/StorageSlot.sol b/src/contracts/utils/StorageSlot.sol new file mode 100644 index 00000000..ae0dc561 --- /dev/null +++ b/src/contracts/utils/StorageSlot.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + assembly { + r.slot := slot + } + } +} From 8316aab1b82b7b642caaeb34de96b5ed05066116 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 8 May 2023 08:10:39 +1200 Subject: [PATCH 22/24] ERC721Wrapper uses Receiver --- audits/2023_04_Wrappers_Ignacio_Mazzara.md | 4 +- .../interfaces/IERC721FloorWrapper.sol | 66 ++++++- src/contracts/interfaces/IERC721Receiver.sol | 8 + src/contracts/utils/WrapperErrors.sol | 3 +- .../wrappers/ERC1155FloorWrapper.sol | 8 + src/contracts/wrappers/ERC721FloorWrapper.sol | 106 ++++++++++-- tests_foundry/ERC1155FloorWrapper.test.sol | 8 + tests_foundry/ERC721FloorWrapper.test.sol | 163 +++++++++++++++++- 8 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 src/contracts/interfaces/IERC721Receiver.sol diff --git a/audits/2023_04_Wrappers_Ignacio_Mazzara.md b/audits/2023_04_Wrappers_Ignacio_Mazzara.md index 525c09b7..a1c3efd6 100644 --- a/audits/2023_04_Wrappers_Ignacio_Mazzara.md +++ b/audits/2023_04_Wrappers_Ignacio_Mazzara.md @@ -32,9 +32,9 @@ The rest of the contracts in the repositories are assumed to be audited. ## General Notes -The _ERC1155FloorWrapper_ wraps and unwraps tokens using the `onERC1155Received` and `onERC1155BatchReceived` functions, but the _ERC721FloorWrapper_ uses specific `deposit` and `withdraw` functions with no support to `onERC721Received`. Using different strategies for the same outcome makes the contract harder to follow and costly for the users in the case of the _ERC721FloorWrapper_. They need an extra transaction to `approve` each token or an `approvalForAll` before depositing. Consider using the same strategy for both wrappers. +The _ERC1155FloorWrapper_ wraps and unwraps tokens using the `onERC1155Received` and `onERC1155BatchReceived` functions, but the _ERC721FloorWrapper_ uses specific `deposit` and `withdraw` functions with no support to `onERC721Received`. Using different strategies for the same outcome makes the contract harder to follow and costly for the users in the case of the _ERC721FloorWrapper_. They need an extra transaction to `approve` each token or an `approvalForAll` before depositing. Consider using the same strategy for both wrappers. **ADDRESSED: Updated ERC721FloorWrapper to use receiver functions. Have kept deposit to enable multiple deposits in a single transaction.** -Another thing to have in mind is that the users can claim unwrapped tokens if someone, by mistake, doesn't use the wrappers as expected. This has been addressed before the audit started[here by the team](https://github.com/0xsequence/niftyswap/pull/79#pullrequestreview-1362003788) +Another thing to have in mind is that the users can claim unwrapped tokens if someone, by mistake, doesn't use the wrappers as expected. This has been addressed before the audit started[here by the team](https://github.com/0xsequence/niftyswap/pull/79#pullrequestreview-1362003788). **IGNORED: Intentional, as stated.** ## Interfaces diff --git a/src/contracts/interfaces/IERC721FloorWrapper.sol b/src/contracts/interfaces/IERC721FloorWrapper.sol index 570b355a..374f2423 100644 --- a/src/contracts/interfaces/IERC721FloorWrapper.sol +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -2,27 +2,79 @@ pragma solidity ^0.8.4; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; +import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol"; +import {IERC721Receiver} from "./IERC721Receiver.sol"; -interface IERC721FloorWrapper is IERC1155 { +interface IERC721FloorWrapper is IERC1155, IERC1155TokenReceiver, IERC721Receiver { event TokensDeposited(uint256[] tokenIds); event TokensWithdrawn(uint256[] tokenIds); + struct DepositRequestObj { + address recipient; + bytes data; + } + + struct WithdrawRequestObj { + uint256[] tokenIds; + address recipient; + bytes data; + } + + /** + * Accepts ERC-721 tokens to wrap. + * @param _operator The address which called `safeTransferFrom` function. + * @param _from The address which previously owned the token. + * @param _tokenId The ID of the token being transferred. + * @param _data Additional data formatted as DepositRequestObj. + * @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + * @notice This is the preferred method for wrapping a single token. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); + /** * Deposit and wrap ERC-721 tokens. * @param _tokenIds The ERC-721 token ids to deposit. * @param _recipient The recipient of the wrapped tokens. * @param _data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. - * @dev This contract intentionally does not support IERC721Receiver for gas optimisations. + * @notice This function can wrap multiple ERC-721 tokens at once. */ function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external; /** - * Unwrap and withdraw ERC-721 tokens. - * @param _tokenIds The ERC-721 token ids to withdraw. - * @param _recipient The recipient of the unwrapped tokens. - * @param _data Data to pass to ERC-1155 receiver. + * Accepts wrapped ERC-1155 tokens to unwrap. + * @param _operator The address which called `safeTransferFrom` function. + * @param _from The address which previously owned the token. + * @param _tokenId The ID of the token being transferred. + * @param _amount The amount of tokens being transferred. + * @param _data Additional data formatted as WithdrawRequestObj. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)` + */ + function onERC1155Received( + address _operator, + address _from, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data + ) external returns (bytes4); + + /** + * Accepts wrapped ERC-1155 tokens to unwrap. + * @param _operator The address which called `safeTransferFrom` function. + * @param _from The address which previously owned the token. + * @param _tokenIds The IDs of the tokens being transferred. + * @param _amounts The amounts of tokens being transferred. + * @param _data Additional data formatted as WithdrawRequestObj. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` */ - function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external; + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _tokenIds, + uint256[] calldata _amounts, + bytes calldata _data + ) external returns (bytes4); } diff --git a/src/contracts/interfaces/IERC721Receiver.sol b/src/contracts/interfaces/IERC721Receiver.sol new file mode 100644 index 00000000..1926fb79 --- /dev/null +++ b/src/contracts/interfaces/IERC721Receiver.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +interface IERC721Receiver { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4); +} diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index 008a03f7..2faab34f 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -8,7 +8,8 @@ abstract contract WrapperErrors { // Factories error WrapperCreationFailed(address tokenAddr); - // ERC1155 + // Wrappers + error InvalidERC721Received(); error InvalidERC1155Received(); error InvalidDepositRequest(); error InvalidWithdrawRequest(); diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index d6945e3c..099fb710 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -47,6 +47,10 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M revert UnsupportedMethod(); } + // + // Tokens + // + /** * Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap. * @param _id The ID of the token being transferred. @@ -168,6 +172,10 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M emit TokensWithdrawn(obj.tokenIds, obj.tokenAmounts); } + // + // Views + // + /** * A distinct Uniform Resource Identifier (URI) for a given token. * @param _id The token id. diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index 974c96f8..cca4b0a8 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -11,7 +11,7 @@ import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC import {WrapperErrors} from "../utils/WrapperErrors.sol"; import {IERC721} from "../interfaces/IERC721.sol"; -import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol"; +import {IERC721FloorWrapper, IERC1155TokenReceiver, IERC721Receiver} from "../interfaces/IERC721FloorWrapper.sol"; /** * @notice Allows users to wrap any amount of a ERC-721 token with a 1:1 ratio. @@ -51,15 +51,43 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met } // - // Tokens + // Deposit // + /** + * Accepts ERC-721 tokens to wrap. + * @param _tokenId The ID of the token being transferred. + * @param _data Additional data formatted as DepositRequestObj. + * @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + * @notice This is the preferred method for wrapping a single token. + */ + function onERC721Received(address, address, uint256 _tokenId, bytes calldata _data) external returns (bytes4) { + if (msg.sender != address(token)) { + revert InvalidERC721Received(); + } + DepositRequestObj memory obj; + (obj) = abi.decode(_data, (DepositRequestObj)); + if (obj.recipient == address(0)) { + // Don't allow deposits to the zero address + revert InvalidWithdrawRequest(); + } + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = _tokenId; + emit TokensDeposited(tokenIds); + + _mint(obj.recipient, TOKEN_ID, 1, obj.data); + + return IERC721Receiver.onERC721Received.selector; + } + /** * Deposit and wrap ERC-721 tokens. * @param _tokenIds The ERC-721 token ids to deposit. * @param _recipient The recipient of the wrapped tokens. * @param _data Data to pass to ERC-1155 receiver. * @notice Users must first approve this contract address on the ERC-721 contract. + * @notice This function can wrap multiple ERC-721 tokens at once. */ function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external { if (_recipient == address(0)) { @@ -79,22 +107,78 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met _mint(_recipient, TOKEN_ID, length, _data); } + // + // Withdraw + // + + /** + * Accepts wrapped ERC-1155 tokens to unwrap. + * @param _amount The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)` + */ + function onERC1155Received(address, address, uint256, uint256 _amount, bytes calldata _data) + external + returns (bytes4) + { + if (msg.sender != address(this)) { + revert InvalidERC1155Received(); + } + _withdraw(_amount, _data); + + return IERC1155TokenReceiver.onERC1155Received.selector; + } + + /** + * Accepts wrapped ERC-1155 tokens to unwrap. + * @param _ids The IDs of the tokens being transferred. + * @param _amounts The amounts of tokens being transferred. + * @param _data Additional data formatted as WithdrawRequestObj. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address, + address, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) public returns (bytes4) { + if (msg.sender != address(this)) { + revert InvalidERC1155Received(); + } + + if (_ids.length != 1) { + revert InvalidERC1155Received(); + } + // Either ids[0] == TOKEN_ID or amounts[0] == 0 + _withdraw(_amounts[0], _data); + + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + /** * Unwrap and withdraw ERC-721 tokens. - * @param _tokenIds The ERC-721 token ids to withdraw. - * @param _recipient The recipient of the unwrapped tokens. - * @param _data Data to pass to ERC-721 receiver. + * @param _amount The amount of ERC-1155 tokens recieved. + * @param _data Additional data formatted as WithdrawRequestObj. */ - function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external { - if (_recipient == address(0)) { + function _withdraw(uint256 _amount, bytes calldata _data) private { + WithdrawRequestObj memory obj; + (obj) = abi.decode(_data, (WithdrawRequestObj)); + if (obj.recipient == address(0)) { + // Don't allow deposits to the zero address + revert InvalidWithdrawRequest(); + } + + uint256 length = obj.tokenIds.length; + if (_amount != length) { + // The amount of tokens received must match the amount of tokens being withdrawn revert InvalidWithdrawRequest(); } - uint256 length = _tokenIds.length; _burn(msg.sender, TOKEN_ID, length); - emit TokensWithdrawn(_tokenIds); + emit TokensWithdrawn(obj.tokenIds); for (uint256 i; i < length;) { - token.safeTransferFrom(address(this), _recipient, _tokenIds[i], _data); + token.safeTransferFrom(address(this), obj.recipient, obj.tokenIds[i], obj.data); unchecked { // Can never overflow i++; @@ -103,7 +187,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met } // - // Metadata + // Views // /** diff --git a/tests_foundry/ERC1155FloorWrapper.test.sol b/tests_foundry/ERC1155FloorWrapper.test.sol index 126b21e7..ec0ea2c8 100644 --- a/tests_foundry/ERC1155FloorWrapper.test.sol +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -278,6 +278,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(address(0), "")); } + // + // Deposit with Batch Receiver + // + // // Withdraw // @@ -456,6 +460,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { ); } + // + // Withdraw with Batch Receiver + // + // // Transfers // diff --git a/tests_foundry/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol index f65a9700..37a61f7e 100644 --- a/tests_foundry/ERC721FloorWrapper.test.sol +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; +import {IERC721FloorWrapper} from "src/contracts/interfaces/IERC721FloorWrapper.sol"; import {ERC721FloorWrapper} from "src/contracts/wrappers/ERC721FloorWrapper.sol"; import {ERC721FloorFactory} from "src/contracts/wrappers/ERC721FloorFactory.sol"; import {ERC721Mock} from "src/contracts/mocks/ERC721Mock.sol"; @@ -238,6 +239,144 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); } + // + // Deposit through Receiver + // + + function test_depositReceiver_happyPath() public { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = erc721.ownerOf(0) == USER ? 0 : 1; // Use 0 or 1 + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(tokenIds); + startMeasuringGas("Deposit 1"); + erc721.safeTransferFrom(USER, wrapperAddr, tokenIds[0], encodeDepositRequest(USER, "")); + stopMeasuringGas(); + + assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + } + + function test_depositReceiver_happyPathWithFactory() public withFactoryCreatedWrapper { + test_depositReceiver_happyPath(); + } + + function test_depositReceiver_toRecipient() public { + uint256 beforeERC1155OperatorBal = wrapper.balanceOf(OPERATOR, wrapperTokenId); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectEmit(true, true, true, true, wrapperAddr); + emit TokensDeposited(tokenIds); + erc721.safeTransferFrom(USER, wrapperAddr, tokenIds[0], encodeDepositRequest(OPERATOR, "")); + + assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + } + + function test_depositReceiver_twice() external { + test_deposit_happyPath(); + test_deposit_happyPath(); + } + + function test_depositReceiver_wrongOwner() external { + vm.prank(USER); + vm.expectRevert("ERC721: caller is not token owner or approved"); + erc721.safeTransferFrom(USER, wrapperAddr, 5, encodeDepositRequest(USER, "")); + } + + function test_depositReceiver_andSell() public withDeposit { + // Niftyswap + ERC1155Mock currency = new ERC1155Mock(); + address currencyAddr = address(currency); + NiftyswapFactory factory = new NiftyswapFactory(); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0); + uint256[] memory types = new uint256[](1); + types[0] = wrapperTokenId; + withERC1155Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 1; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER, 0); + + // Deposit and sell + vm.prank(USER); + erc721.safeTransferFrom( + USER, + wrapperAddr, + 2, + encodeDepositRequest(exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp)) + ); + + // After bals + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER, 0)); + } + + function test_depositReceiver_andSell20() public withDeposit { + // Niftyswap + ERC20TokenMock currency = new ERC20TokenMock(); + address currencyAddr = address(currency); + NiftyswapFactory20 factory = new NiftyswapFactory20(address(this)); + // Wrapped tokens are never currency + factory.createExchange(wrapperAddr, currencyAddr, 0, 0); + address exchangeAddr = factory.tokensToExchange(wrapperAddr, currencyAddr, 0, 0); + uint256[] memory types = new uint256[](1); + types[0] = wrapperTokenId; + withERC20Liquidity(currency, exchangeAddr, types); + // Sell data + uint256[] memory sellAmounts = new uint256[](1); + sellAmounts[0] = 1; + uint256[] memory prices = INiftyswapExchange(exchangeAddr).getPrice_tokenToCurrency(types, sellAmounts); + + // Before bals + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + uint256 beforeCurrencyUserBal = currency.balanceOf(USER); + + // Deposit and sell + vm.prank(USER); + erc721.safeTransferFrom( + USER, + wrapperAddr, + 2, + encodeDepositRequest( + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens( + USER, prices[0], new address[](0), new uint256[](0), block.timestamp + ) + ) + ); + + // After bals + assertEq(beforeERC1155UserBal, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 1, erc721.balanceOf(USER)); + assertEq(beforeCurrencyUserBal + prices[0], currency.balanceOf(USER)); + } + // // Withdraw // @@ -253,7 +392,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds); startMeasuringGas("Withdraw 1"); - wrapper.withdraw(tokenIds, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); stopMeasuringGas(); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); @@ -276,7 +415,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds); - wrapper.withdraw(tokenIds, OPERATOR, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, OPERATOR, "")); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); @@ -289,7 +428,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectRevert("ERC721: transfer from incorrect owner"); - wrapper.withdraw(tokenIds, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); } function test_withdraw_twice() external { @@ -305,7 +444,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds); - wrapper.withdraw(tokenIds, OPERATOR, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, OPERATOR, "")); assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); @@ -325,7 +464,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.expectEmit(true, true, true, true, wrapperAddr); emit TokensWithdrawn(tokenIds); startMeasuringGas("Withdraw 2"); - wrapper.withdraw(tokenIds, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, USER, "")); stopMeasuringGas(); assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); @@ -339,7 +478,7 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { vm.prank(USER); vm.expectRevert(stdError.arithmeticError); - wrapper.withdraw(tokenIds, USER, ""); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); } // @@ -429,6 +568,18 @@ contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { ); } + function encodeDepositRequest(address recipient, bytes memory data) private pure returns (bytes memory) { + return abi.encode(IERC721FloorWrapper.DepositRequestObj(recipient, data)); + } + + function encodeWithdrawRequest(uint256[] memory tokenIds, address recipient, bytes memory data) + private + pure + returns (bytes memory) + { + return abi.encode(IERC721FloorWrapper.WithdrawRequestObj(tokenIds, recipient, data)); + } + /** * Skip a test. */ From 5250b81246eeeacbb93a1efb5f8d794c725a0734 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 6 Jun 2023 13:41:08 +1200 Subject: [PATCH 23/24] Check values before transfers --- src/contracts/mocks/ERC1155RoyaltyMock.sol | 2 +- src/contracts/utils/WrapperErrors.sol | 1 + .../wrappers/ERC1155FloorWrapper.sol | 54 ++++++++++++++++-- src/contracts/wrappers/ERC721FloorWrapper.sol | 55 +++++++++++++++++-- src/package.json | 2 +- yarn.lock | 9 +++ 6 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/contracts/mocks/ERC1155RoyaltyMock.sol b/src/contracts/mocks/ERC1155RoyaltyMock.sol index 476b1774..c17496bf 100644 --- a/src/contracts/mocks/ERC1155RoyaltyMock.sol +++ b/src/contracts/mocks/ERC1155RoyaltyMock.sol @@ -59,7 +59,7 @@ contract ERC1155RoyaltyMock is ERC1155MintBurnMock { * @param _interfaceID The interface identifier, as specified in ERC-165 * @return `true` if the contract implements `_interfaceID` and */ - function supportsInterface(bytes4 _interfaceID) public pure virtual override(ERC1155MintBurnMock) returns (bool) { + function supportsInterface(bytes4 _interfaceID) public view virtual override(ERC1155MintBurnMock) returns (bool) { // Should be 0x2a55205a if (_interfaceID == _INTERFACE_ID_ERC2981) { return true; diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol index 2faab34f..b83f6927 100644 --- a/src/contracts/utils/WrapperErrors.sol +++ b/src/contracts/utils/WrapperErrors.sol @@ -13,6 +13,7 @@ abstract contract WrapperErrors { error InvalidERC1155Received(); error InvalidDepositRequest(); error InvalidWithdrawRequest(); + error InvalidTransferRequest(); // General error UnsupportedMethod(); diff --git a/src/contracts/wrappers/ERC1155FloorWrapper.sol b/src/contracts/wrappers/ERC1155FloorWrapper.sol index 099fb710..7a2cf42c 100644 --- a/src/contracts/wrappers/ERC1155FloorWrapper.sol +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -100,10 +100,7 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M _deposit(_amounts, _data); } else if (msg.sender == address(this)) { // Withdraw - if (_ids.length != 1) { - revert InvalidERC1155Received(); - } - // Either ids[0] == TOKEN_ID or amounts[0] == 0 + assert(_ids.length == 1); // Always true, see transfer override _withdraw(_amounts[0], _data); } else { revert InvalidERC1155Received(); @@ -172,6 +169,53 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M emit TokensWithdrawn(obj.tokenIds, obj.tokenAmounts); } + // + // Transfer overrides + // + + /** + * Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) + public + override(ERC1155, IERC1155) + { + if (_amount == 0) { + revert InvalidTransferRequest(); + } + + super.safeTransferFrom(_from, _to, _id, _amount, _data); + } + + /** + * Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + * @dev As this contract only supports a single token id, this function requires a single transfer. + * @dev Prefer using `safeTransferFrom` over this function. + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public override(ERC1155, IERC1155) { + if (_ids.length != 1 || _ids[0] != TOKEN_ID || _amounts.length != 1 || _amounts[0] == 0) { + revert InvalidTransferRequest(); + } + + super.safeBatchTransferFrom(_from, _to, _ids, _amounts, _data); + } + // // Views // @@ -192,7 +236,7 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M * @param _interfaceId The interfaceId to test. * @return supported Whether the interfaceId is supported. */ - function supportsInterface(bytes4 _interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + function supportsInterface(bytes4 _interfaceId) public view override(IERC165, ERC1155) returns (bool supported) { return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(IERC1155).interfaceId || _interfaceId == type(IERC1155Metadata).interfaceId || _interfaceId == type(IERC1155FloorWrapper).interfaceId || super.supportsInterface(_interfaceId); diff --git a/src/contracts/wrappers/ERC721FloorWrapper.sol b/src/contracts/wrappers/ERC721FloorWrapper.sol index cca4b0a8..e95d1a07 100644 --- a/src/contracts/wrappers/ERC721FloorWrapper.sol +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -144,13 +144,11 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met bytes calldata _data ) public returns (bytes4) { if (msg.sender != address(this)) { + // Only accept tokens from this contract revert InvalidERC1155Received(); } - if (_ids.length != 1) { - revert InvalidERC1155Received(); - } - // Either ids[0] == TOKEN_ID or amounts[0] == 0 + assert(_ids.length == 1); // Always true, see transfer override _withdraw(_amounts[0], _data); return IERC1155TokenReceiver.onERC1155BatchReceived.selector; @@ -186,6 +184,53 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met } } + // + // Transfer overrides + // + + /** + * Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) + public + override(ERC1155, IERC1155) + { + if (_amount == 0) { + revert InvalidTransferRequest(); + } + + super.safeTransferFrom(_from, _to, _id, _amount, _data); + } + + /** + * Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + * @dev As this contract only supports a single token id, this function requires a single transfer. + * @dev Prefer using `safeTransferFrom` over this function. + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public override(ERC1155, IERC1155) { + if (_ids.length != 1 || _ids[0] != TOKEN_ID || _amounts.length != 1 || _amounts[0] == 0) { + revert InvalidTransferRequest(); + } + + super.safeBatchTransferFrom(_from, _to, _ids, _amounts, _data); + } + // // Views // @@ -206,7 +251,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met * @param _interfaceId The interfaceId to test. * @return supported Whether the interfaceId is supported. */ - function supportsInterface(bytes4 _interfaceId) public pure override(IERC165, ERC1155) returns (bool supported) { + function supportsInterface(bytes4 _interfaceId) public view override(IERC165, ERC1155) returns (bool supported) { return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(IERC1155).interfaceId || _interfaceId == type(IERC1155Metadata).interfaceId || super.supportsInterface(_interfaceId); } diff --git a/src/package.json b/src/package.json index 6a65e302..eb681ba6 100644 --- a/src/package.json +++ b/src/package.json @@ -23,7 +23,7 @@ "ethers": "^5.0.32" }, "dependencies": { - "@0xsequence/erc-1155": "^4.0.1", + "@0xsequence/erc-1155": "^4.0.3", "@0xsequence/erc20-meta-token": "^4.0.1", "@uniswap/lib": "^4.0.1-alpha" }, diff --git a/yarn.lock b/yarn.lock index 6cc024da..489a5fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,6 +11,15 @@ "@ethersproject/providers" "^5.7.2" ethers "^5.7.2" +"@0xsequence/erc-1155@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@0xsequence/erc-1155/-/erc-1155-4.0.3.tgz#b9b890c1ced04a84f36be22dd20424b43b9e1da2" + integrity sha512-vDh4OEuq0bR3iIqhsxpuRpczH5GxwH/mjBWm9uP6VYKnKM36ZBLApSNUlIOCXYRCS0DYFxsdvYjAemb3w1l4ow== + optionalDependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/providers" "^5.7.2" + ethers "^5.7.2" + "@0xsequence/erc20-meta-token@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@0xsequence/erc20-meta-token/-/erc20-meta-token-4.0.1.tgz#44fe77822af0ff3b1111466615c9958e92aca782" From fdd647f0593cf5a9c4b47b72794885719f3ed95d Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 7 Jun 2023 10:39:57 +1200 Subject: [PATCH 24/24] Fix Immutable variables cannot be initialized inside a try/catch statement --- src/contracts/exchange/NiftyswapExchange20.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contracts/exchange/NiftyswapExchange20.sol b/src/contracts/exchange/NiftyswapExchange20.sol index 305a4399..1f1bf1b5 100644 --- a/src/contracts/exchange/NiftyswapExchange20.sol +++ b/src/contracts/exchange/NiftyswapExchange20.sol @@ -85,9 +85,11 @@ contract NiftyswapExchange20 is FEE_MULTIPLIER = 1000 - _lpFee; // If global royalty, lets check for ERC-2981 support + bool supportsERC2981 = false; try IERC1155(_tokenAddr).supportsInterface(type(IERC2981).interfaceId) returns (bool supported) { - IS_ERC2981 = supported; + supportsERC2981 = supported; } catch {} // solhint-disable-line no-empty-blocks + IS_ERC2981 = supportsERC2981; } //