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 diff --git a/audits/2023_04_Wrappers_Ignacio_Mazzara.md b/audits/2023_04_Wrappers_Ignacio_Mazzara.md new file mode 100644 index 00000000..a1c3efd6 --- /dev/null +++ b/audits/2023_04_Wrappers_Ignacio_Mazzara.md @@ -0,0 +1,169 @@ +# Niftyswap - Security Review + +**Responses to comments follow in bold.** + +## 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. **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). **IGNORED: Intentional, as stated.** + +## 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)")) **ADDRESSED: Fixed docstring.** + +### 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. **ADDRESSED: Added _ to new interface function parameters.** + +## 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. **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. **IGNORED: Assembly access to immutable variables is not supported in solidity 0.8.4.** + +### WrapperErrors.sol + +Nothing found. + +### WrapperProxyDeployer.sol + +#### Notes + +- 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. **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. **ADDRESSED: Removed additional checks.** + +- 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. **ADDRESSED: Documentation added.** + + +## 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. **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. **ADDRESSED: Removed _ and updated variable name.** + +- 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. **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. **IGNORED: This is desired behavour and mirrors existing Niftyswap Exchanges.** + +#### Notes + +- 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. **ADDRESSED: Added _ to all function parameters.** + +- 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. **ADDRESSED: Followed advice.** + + +### 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. **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. **ADDRESSED: Added _ to all function parameters.** + +- 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. **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. **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. **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. **ADDRESSED: Reused length variable.** + +- 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. **ADDRESSED: Added _ to all function parameters.** + + + +Ignacio Mazzara - April 2023. 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; } // diff --git a/src/contracts/interfaces/IERC1155FloorFactory.sol b/src/contracts/interfaces/IERC1155FloorFactory.sol new file mode 100644 index 00000000..1ffa8b67 --- /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 new file mode 100644 index 00000000..cd11d0ba --- /dev/null +++ b/src/contracts/interfaces/IERC1155FloorWrapper.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +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, 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; + } + + /** + * 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("onERC1155Received(address,address,uint256,uint256,bytes)` + */ + 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. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + 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 new file mode 100644 index 00000000..75bd627d --- /dev/null +++ b/src/contracts/interfaces/IERC721.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +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/IERC721FloorFactory.sol b/src/contracts/interfaces/IERC721FloorFactory.sol new file mode 100644 index 00000000..7fc107dc --- /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 new file mode 100644 index 00000000..374f2423 --- /dev/null +++ b/src/contracts/interfaces/IERC721FloorWrapper.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +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, 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. + * @notice This function can wrap multiple ERC-721 tokens at once. + */ + function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external; + + /** + * 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 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/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 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/mocks/ERC721Mock.sol b/src/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000..ff97a07d --- /dev/null +++ b/src/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +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/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 new file mode 100644 index 00000000..b4e1e5aa --- /dev/null +++ b/src/contracts/utils/Proxy.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +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) { + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = _implementation; + emit Upgraded(_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 = StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + 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/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 + } + } +} diff --git a/src/contracts/utils/WrapperErrors.sol b/src/contracts/utils/WrapperErrors.sol new file mode 100644 index 00000000..b83f6927 --- /dev/null +++ b/src/contracts/utils/WrapperErrors.sol @@ -0,0 +1,21 @@ +// 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 WrapperCreationFailed(address tokenAddr); + + // Wrappers + error InvalidERC721Received(); + error InvalidERC1155Received(); + error InvalidDepositRequest(); + error InvalidWithdrawRequest(); + error InvalidTransferRequest(); + + // General + error UnsupportedMethod(); + error InvalidInitialization(); +} diff --git a/src/contracts/utils/WrapperProxyDeployer.sol b/src/contracts/utils/WrapperProxyDeployer.sol new file mode 100644 index 00000000..474b85c3 --- /dev/null +++ b/src/contracts/utils/WrapperProxyDeployer.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {WrapperErrors} from "../utils/WrapperErrors.sol"; +import {Proxy} from "../utils/Proxy.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); + bytes32 salt = getProxySalt(tokenAddr); + + // Deploy it + assembly { + proxyAddr := create2(0, add(code, 32), mload(code), salt) + } + if (proxyAddr == address(0)) { + revert WrapperCreationFailed(tokenAddr); + } + return proxyAddr; + } + + /** + * 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); + } + + /** + * 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))); + } + + /** + * 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)))); + } + + /** + * 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)); + } + + /** + * 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 { + csize := extcodesize(addr) + } + return csize != 0; + } +} diff --git a/src/contracts/wrappers/ERC1155FloorFactory.sol b/src/contracts/wrappers/ERC1155FloorFactory.sol new file mode 100644 index 00000000..9aa989e6 --- /dev/null +++ b/src/contracts/wrappers/ERC1155FloorFactory.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC1155FloorFactory} from "../interfaces/IERC1155FloorFactory.sol"; +import {ERC1155FloorWrapper} from "../wrappers/ERC1155FloorWrapper.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, WrapperProxyDeployer { + address private immutable implAddr; // Address of the wrapper implementation + IERC1155Metadata internal metadataContract; // address of the ERC-1155 Metadata contract + + /** + * 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); + } + + /** + * 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) { + wrapperAddr = deployProxy(implAddr, tokenAddr); + ERC1155FloorWrapper(wrapperAddr).initialize(tokenAddr); + emit NewERC1155FloorWrapper(tokenAddr); + return 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); + } + + 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 metadataAddr The address of the ERC-1155 Metadata contract + */ + function setMetadataContract(IERC1155Metadata metadataAddr) external onlyOwner { + emit MetadataContractChanged(address(metadataAddr)); + metadataContract = metadataAddr; + } + + /** + * @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 new file mode 100644 index 00000000..7a2cf42c --- /dev/null +++ b/src/contracts/wrappers/ERC1155FloorWrapper.sol @@ -0,0 +1,244 @@ +// 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 {ERC1155} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155.sol"; +import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.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"; + +/** + * @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, WrapperErrors { + address internal immutable factory; + address public tokenAddr; + // 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(); + } + tokenAddr = _tokenAddr; + } + + /** + * Prevent invalid method calls. + */ + fallback() external { + 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. + * @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) + 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; + emit TokensDeposited(ids, amounts); + _deposit(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 + ) public returns (bytes4) { + if (msg.sender == tokenAddr) { + // Deposit + emit TokensDeposited(_ids, _amounts); + _deposit(_amounts, _data); + } else if (msg.sender == address(this)) { + // Withdraw + assert(_ids.length == 1); // Always true, see transfer override + _withdraw(_amounts[0], _data); + } else { + revert InvalidERC1155Received(); + } + + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + /** + * Wrap deposited ERC-1155 tokens. + * @param _tokenAmounts The amount of each token deposited. + * @param _data Data received during deposit. + */ + 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 = _tokenAmounts.length; + for (uint256 i; i < length;) { + total += _tokenAmounts[i]; + unchecked { + // Can never overflow + i++; + } + } + + _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. + */ + 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 = obj.tokenAmounts.length; + for (uint256 i; i < length;) { + total += obj.tokenAmounts[i]; + unchecked { + i++; + } + } + + if (total != _amount) { + revert InvalidWithdrawRequest(); + } + + IERC1155(tokenAddr).safeBatchTransferFrom( + address(this), obj.recipient, obj.tokenIds, obj.tokenAmounts, obj.data + ); + 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 + // + + /** + * 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 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/ERC721FloorFactory.sol b/src/contracts/wrappers/ERC721FloorFactory.sol new file mode 100644 index 00000000..80c41c0b --- /dev/null +++ b/src/contracts/wrappers/ERC721FloorFactory.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {IERC721FloorFactory} from "../interfaces/IERC721FloorFactory.sol"; +import {ERC721FloorWrapper} from "../wrappers/ERC721FloorWrapper.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, WrapperProxyDeployer { + 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); + } + + /** + * Creates an ERC-721 Floor Wrapper for given 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); + 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 + * @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); + } + + 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 new file mode 100644 index 00000000..e95d1a07 --- /dev/null +++ b/src/contracts/wrappers/ERC721FloorWrapper.sol @@ -0,0 +1,258 @@ +// 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 {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, IERC1155TokenReceiver, IERC721Receiver} from "../interfaces/IERC721FloorWrapper.sol"; + +/** + * @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, ERC1155MintBurn, IERC1155Metadata, WrapperErrors { + IERC721 public token; + address internal immutable factory; + // 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; + } + + /** + * 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); + } + + /** + * Prevent invalid method calls. + */ + fallback() external { + revert UnsupportedMethod(); + } + + // + // 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)) { + revert InvalidDepositRequest(); + } + + uint256 length = _tokenIds.length; + for (uint256 i; i < length;) { + // Intentionally unsafe transfer + token.transferFrom(msg.sender, address(this), _tokenIds[i]); + unchecked { + // Can never overflow + i++; + } + } + emit TokensDeposited(_tokenIds); + _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)) { + // Only accept tokens from this contract + revert InvalidERC1155Received(); + } + + assert(_ids.length == 1); // Always true, see transfer override + _withdraw(_amounts[0], _data); + + return IERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + /** + * Unwrap and withdraw ERC-721 tokens. + * @param _amount The amount of ERC-1155 tokens recieved. + * @param _data Additional data formatted as WithdrawRequestObj. + */ + 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(); + } + + _burn(msg.sender, TOKEN_ID, length); + emit TokensWithdrawn(obj.tokenIds); + for (uint256 i; i < length;) { + token.safeTransferFrom(address(this), obj.recipient, obj.tokenIds[i], obj.data); + unchecked { + // Can never overflow + i++; + } + } + } + + // + // 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 + // + + /** + * 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 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/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/src/package.json b/src/package.json index dca00f47..eb681ba6 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", @@ -23,10 +23,11 @@ "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" }, "devDependencies": { + "@openzeppelin/contracts": "^4.8.2" } } diff --git a/tests_foundry/ERC1155FloorFactory.test.sol b/tests_foundry/ERC1155FloorFactory.test.sol new file mode 100644 index 00000000..b038cb7e --- /dev/null +++ b/tests_foundry/ERC1155FloorFactory.test.sol @@ -0,0 +1,86 @@ +// 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(WrapperCreationFailed.selector, 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 new file mode 100644 index 00000000..ec0ea2c8 --- /dev/null +++ b/tests_foundry/ERC1155FloorWrapper.test.sol @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +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"; +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"; +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"; + +contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors { + // Redeclare events + 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 wrapperTokenId; + + function setUp() external { + erc1155 = new ERC1155Mock(); + erc1155Addr = address(erc1155); + + wrapper = new ERC1155FloorWrapper(); + wrapper.initialize(erc1155Addr); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); + + // 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, ""); + } + + // + // 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 + // + function test_deposit_happyPath() public { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); + 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(tokenIds, tokenAmounts); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); + + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(OPERATOR, "")); + + assertEq(beforeWrapperOperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + erc1155.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeDepositRequest(USER, "")); + + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); + + 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)); + assertEq(beforeERC1155UserBal1 - 1, erc1155.balanceOf(USER, 1)); + } + + 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] = wrapperTokenId; + 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, wrapperTokenId); + 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); + erc1155.safeBatchTransferFrom( + USER, + wrapperAddr, + tokenIds, + sellAmounts, + encodeDepositRequest(exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp)) + ); + + // After bals + 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)); + } + + 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] = wrapperTokenId; + 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, wrapperTokenId); + 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); + erc1155.safeBatchTransferFrom( + USER, + wrapperAddr, + tokenIds, + sellAmounts, + encodeDepositRequest( + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens( + USER, prices[0], new address[](0), new uint256[](0), block.timestamp + ) + ) + ); + + // After bals + 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)); + } + + 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), "")); + } + + // + // Deposit with Batch Receiver + // + + // + // Withdraw + // + function test_withdraw_happyPath() public withDeposit { + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); + 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(tokenIds, tokenAmounts); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, OPERATOR, "")); + + assertEq(beforeWrapperUserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); + 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.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + } + + function test_withdraw_twice() external { + test_withdraw_happyPath(); + + uint256 beforeWrapperUserBal = wrapper.balanceOf(USER, wrapperTokenId); + 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(tokenIds, tokenAmounts); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, tokenAmounts, OPERATOR, "")); + + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + + 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, wrapperTokenId); + 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(tokenIds, tokenAmounts); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, tokenAmounts, USER, "")); + + 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)); + assertEq(beforeERC1155UserBal1 + 1, erc1155.balanceOf(USER, 1)); + } + + 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); + tokenAmounts[0] = 1; + + vm.prank(USER); + vm.expectRevert(); + wrapper.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, ""); + } + + 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(); + wrapper.safeBatchTransferFrom( + USER, wrapperAddr, tokenIds, tokenAmounts, encodeWithdrawRequest(tokenIds, tokenAmounts, address(0), "") + ); + } + + // + // Withdraw with Batch Receiver + // + + // + // 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, ""); + } + + // + // 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(); + + _; + } + + 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); + erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(USER, "")); + vm.prank(OPERATOR); + erc1155.safeBatchTransferFrom(OPERATOR, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(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) + ); + } + + 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. + */ + 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..aa66838f --- /dev/null +++ b/tests_foundry/ERC721FloorFactory.test.sol @@ -0,0 +1,86 @@ +// 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(WrapperCreationFailed.selector, 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/ERC721FloorWrapper.test.sol b/tests_foundry/ERC721FloorWrapper.test.sol new file mode 100644 index 00000000..37a61f7e --- /dev/null +++ b/tests_foundry/ERC721FloorWrapper.test.sol @@ -0,0 +1,594 @@ +// 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"; +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 {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"; +import {stdError} from "forge-std/StdError.sol"; + +contract ERC721FloorWrapperTest is TestHelperBase, WrapperErrors { + // Redeclare events + event TokensDeposited(uint256[] tokenIds); + event TokensWithdrawn(uint256[] tokenIds); + + ERC721FloorWrapper private wrapper; + address private wrapperAddr; + ERC721Mock private erc721; + uint256 private wrapperTokenId; + + function setUp() external { + erc721 = new ERC721Mock(); + + wrapper = new ERC721FloorWrapper(); + wrapper.initialize(address(erc721)); + wrapperAddr = address(wrapper); + wrapperTokenId = wrapper.TOKEN_ID(); + + // 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); + } + + // + // 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 + // + function test_deposit_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"); + wrapper.deposit(tokenIds, USER, ""); + stopMeasuringGas(); + + assertEq(beforeERC1155UserBal + 1, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 1, erc721.balanceOf(wrapperAddr)); + 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); + 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); + wrapper.deposit(tokenIds, OPERATOR, ""); + + assertEq(beforeERC1155OperatorBal + 1, wrapper.balanceOf(OPERATOR, wrapperTokenId)); + 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_five() external { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); + uint256 beforeERC721WrapperBal = erc721.balanceOf(wrapperAddr); + uint256 beforeERC721UserBal = erc721.balanceOf(USER); + + 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(tokenIds); + startMeasuringGas("Deposit 5"); + wrapper.deposit(tokenIds, USER, ""); + stopMeasuringGas(); + + assertEq(beforeERC1155UserBal + 5, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal + 5, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal - 5, 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(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(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] = wrapperTokenId; + 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, wrapperTokenId); + 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(tokenIds, exchangeAddr, NiftyswapTestHelper.encodeSellTokens(USER, prices[0], block.timestamp)); + + // After bals + 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)); + } + + 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] = wrapperTokenId; + 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, wrapperTokenId); + 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( + tokenIds, + exchangeAddr, + Niftyswap20TestHelper.encodeSellTokens(USER, prices[0], new address[](0), new uint256[](0), block.timestamp) + ); + + // After bals + 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)); + } + + // + // 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 + // + function test_withdraw_happyPath() public withDeposit { + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, 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 TokensWithdrawn(tokenIds); + startMeasuringGas("Withdraw 1"); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); + stopMeasuringGas(); + + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal - 1, erc721.balanceOf(wrapperAddr)); + 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); + 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(tokenIds); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, OPERATOR, "")); + + assertEq(beforeERC1155UserBal - 1, wrapper.balanceOf(USER, wrapperTokenId)); + 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.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); + } + + function test_withdraw_twice() external { + test_withdraw_happyPath(); + + uint256 beforeERC1155UserBal = wrapper.balanceOf(USER, wrapperTokenId); + 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(tokenIds); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, OPERATOR, "")); + + 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, wrapperTokenId); + 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(tokenIds); + startMeasuringGas("Withdraw 2"); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 2, encodeWithdrawRequest(tokenIds, USER, "")); + stopMeasuringGas(); + + assertEq(beforeERC1155UserBal - 2, wrapper.balanceOf(USER, wrapperTokenId)); + assertEq(beforeERC721WrapperBal - 2, erc721.balanceOf(wrapperAddr)); + assertEq(beforeERC721UserBal + 2, erc721.balanceOf(USER)); + } + + function test_withdraw_insufficientBalance() external { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + vm.prank(USER); + vm.expectRevert(stdError.arithmeticError); + wrapper.safeTransferFrom(USER, wrapperAddr, 0, 1, encodeWithdrawRequest(tokenIds, USER, "")); + } + + // + // 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); + + tokenIds[0] = 0; + tokenIds[1] = 1; + vm.prank(USER); + wrapper.deposit(tokenIds, USER, ""); + + tokenIds[0] = 5; + tokenIds[1] = 6; + vm.prank(OPERATOR); + wrapper.deposit(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) + ); + } + + 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. + */ + modifier skipTest() { + // solhint-disable-next-line no-console + console.log("Test skipped"); + if (false) { + // Required for compiler + _; + } + } +} 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..a93ff377 100644 --- a/tests_foundry/WrapAndNiftySwap.test.sol +++ b/tests_foundry/WrapAndNiftySwap.test.sol @@ -2,16 +2,18 @@ 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"; 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/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/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..5a3a2780 100644 --- a/tests_foundry/utils/TestHelperBase.test.sol +++ b/tests_foundry/utils/TestHelperBase.test.sol @@ -4,10 +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 {Constants} from "./Constants.test.sol"; +import {GasHelper} from "./GasHelper.test.sol"; import {Test} from "forge-std/Test.sol"; -abstract contract TestHelperBase is Test, Constants { +abstract contract TestHelperBase is Test, GasHelper { /** * Get token balances. */ diff --git a/yarn.lock b/yarn.lock index 832f136a..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" @@ -973,6 +982,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"