From 31edb468a351284841ae32ca45d0ff3b31ff17c3 Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Wed, 27 Sep 2023 22:14:21 +0530 Subject: [PATCH] feat(contracts): add new forwarder contracts WIN-506 --- README.md | 24 + contracts/ForwarderFactoryV4.sol | 84 +++ contracts/ForwarderV4.sol | 381 +++++++++++++ contracts/IForwarder.sol | 2 +- contracts/IForwarderV4.sol | 64 +++ test/forwarderFactoryV4.js | 278 ++++++++++ test/forwarderV4.js | 914 +++++++++++++++++++++++++++++++ test/gas.js | 4 +- 8 files changed, 1748 insertions(+), 3 deletions(-) create mode 100644 contracts/ForwarderFactoryV4.sol create mode 100644 contracts/ForwarderV4.sol create mode 100644 contracts/IForwarderV4.sol create mode 100644 test/forwarderFactoryV4.js create mode 100644 test/forwarderV4.js diff --git a/README.md b/README.md index b2e57f9..1a4290d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Multi-sig contract suitable for use as a 2-of-3 multisig wallet. The core functionality of the wallet is implemented in the [WalletSimple](contracts/WalletSimple.sol) contract. It is initialized with 3 signer addresses, two of which must participate in order to execute a transaction from the wallet. Auxillary contracts called [Forwarders](contracts/Forwarder.sol) can be deployed with a WalletSimple contract initialized as its "parent". Any funds that the forwarder receives will be sent on back to the parent wallet. This enables omnibus-style wallets to create many addresses that are all controlled by the same wallet. +New forwarder contracts (contracts/ForwarderV4.sol) can be deployed and initialized with a parent address and fee address. Parent address will be the single sig base address of the wallet. Fee address will be the gas tank address of the wallet. Both parent address and fee address will be allowed to invoke methods of the contract, but any funds that the forwarder receives will be sent on back to the parent address. This enables omnibus-style wallets to create many addresses that are all controlled by the same wallet. + Features of the [wallet contract](contracts/WalletSimple.sol): 1. Functions as a 2-of-3 multisig wallet for sending transactions. @@ -22,6 +24,13 @@ Features of the [forwarder contract](contracts/Forwarder.sol) 2. Automatically flushes any ETH received to the parent address. 3. Able to flush ERC20 tokens received to the parent address through a separate transaction (flushForwarderTokens). +Features of the updated [forwarder contract](contracts/ForwarderV4.sol) + +1. Deployed with a permanent parent address and a fee address. +2. Both parent address and fee address can invoke the methods of the contract. +3. Automatically flushes any ETH received to the parent address. +4. Able to flush ERC20 tokens received to the parent address through a separate transaction (flushTokens). + Note that this suite of contracts is an upgraded version of [eth-multisig-v2](https://github.com/bitgo/eth-multisig-v2). The main changes that were made are as follows: - Wallets and forwarders are deployed as proxy instances to a single implementation, to save on deployment fees. - Wallets and forwarders are deployed using CREATE2 to allow addresses to be generated on demand, but only deployed upon first use. @@ -48,6 +57,13 @@ To deploy forwarders, follow these steps: 3. Call the `createForwarder` function on the factory deployed in step 2. Provide the parent address, and some "salt" which will be used to determine the forwarder's address via [CREATE2](https://eips.ethereum.org/EIPS/eip-1014). 4. Check for the `ForwarderCreated` event from the above transaction. This will include your newly generated forwarder address +**ForwardersV4** +To deploy forwarders, follow these steps: +1. Deploy a forwarder contract ([contracts/ForwarderV4.sol](contracts/Forwarder.sol)) with any address. Take note of the contract's address. +2. Deploy a ForwarderFactory contract ([contracts/ForwarderFactoryV4.sol](contracts/ForwarderFactoryV4.sol)) with any address. Use the address of the contract deployed in step 1 as the `_implementationAddress` parameter. +3. Call the `createForwarder` function on the factory deployed in step 2. Provide the parent address, fee address and some "salt" which will be used to determine the forwarder's address via [CREATE2](https://eips.ethereum.org/EIPS/eip-1014). +4. Check for the `ForwarderCreated` event from the above transaction. This will include your newly generated forwarder address + ## Contracts Brief descriptions of the various contracts provided in this repository. @@ -72,6 +88,14 @@ Factory to create forwarder. Deploys a small proxy which utilizes the implementa Transfer batcher. Takes a list of recipients and amounts, and distributes ETH to them in a single transaction. +[**ForwarderV4**](contracts/ForwarderV4.sol) + +Forwarder function. Initializes with a parent and a fee address. It will forward any ETH that it receives to the parent address. Also has a function to forward ERC20 tokens. This function can be invoked using the parent address or the fee address. + +[**ForwarderFactoryV4**](contracts/ForwarderFactoryV4.sol) + +Factory to create updated forwarder (contracts/ForwarderV4.sol). Deploys a small proxy which utilizes the implementation of a single forwarder contract. + ## Installation NodeJS 16 is required. diff --git a/contracts/ForwarderFactoryV4.sol b/contracts/ForwarderFactoryV4.sol new file mode 100644 index 0000000..2c986bc --- /dev/null +++ b/contracts/ForwarderFactoryV4.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; +import './ForwarderV4.sol'; +import './CloneFactory.sol'; + +/** + * @title ForwarderFactoryV4 + * @notice This contract will deploy new forwarder contracts using the create2 opcode + */ +contract ForwarderFactoryV4 is CloneFactory { + address public implementationAddress; + + /** + * @notice Event triggered when a new forwarder is deployed + * @param newForwarderAddress Address of the newly deployed forwarder + * @param parentAddress Address to which the funds should be forwarded + * @param feeAddress Address which is allowed to call methods on forwarder contract alongwith the parentAddress + * @param shouldAutoFlushERC721 Whether to automatically flush ERC721 tokens or not + * @param shouldAutoFlushERC1155 Whether to automatically flush ERC1155 tokens or not + */ + event ForwarderCreated( + address newForwarderAddress, + address parentAddress, + address feeAddress, + bool shouldAutoFlushERC721, + bool shouldAutoFlushERC1155 + ); + + /** + * @notice Initializes the factory with the address of the current forwarder implementation + * @param _implementationAddress Address of the current forwarder implementation + */ + constructor(address _implementationAddress) { + implementationAddress = _implementationAddress; + } + + /** + * @notice Creates a new forwarder contract + * @param parent Address to which the funds should be forwarded + * @param feeAddress Address which is allowed to call methods on forwarder contract alongwith the parentAddress + * @param salt Salt to be used while deploying the contract + */ + function createForwarder( + address parent, + address feeAddress, + bytes32 salt + ) external { + this.createForwarder(parent, feeAddress, salt, true, true); + } + + /** + * @notice Creates a new forwarder contract + * @param parent Address to which the funds should be forwarded + * @param feeAddress Address which is allowed to call methods on forwarder contract alongwith the parentAddress + * @param salt Salt to be used while deploying the contract + * @param shouldAutoFlushERC721 Whether to automatically flush ERC721 tokens or not + * @param shouldAutoFlushERC1155 Whether to automatically flush ERC1155 tokens or not + */ + function createForwarder( + address parent, + address feeAddress, + bytes32 salt, + bool shouldAutoFlushERC721, + bool shouldAutoFlushERC1155 + ) external { + /// include the parent and fee address in the salt so any contract deployed directly relies on the parent address and the fee address + bytes32 finalSalt = keccak256(abi.encodePacked(parent, feeAddress, salt)); + + address payable clone = createClone(implementationAddress, finalSalt); + ForwarderV4(clone).init( + parent, + feeAddress, + shouldAutoFlushERC721, + shouldAutoFlushERC1155 + ); + emit ForwarderCreated( + clone, + parent, + feeAddress, + shouldAutoFlushERC721, + shouldAutoFlushERC1155 + ); + } +} diff --git a/contracts/ForwarderV4.sol b/contracts/ForwarderV4.sol new file mode 100644 index 0000000..8037fe7 --- /dev/null +++ b/contracts/ForwarderV4.sol @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; +import '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; +import '@openzeppelin/contracts/token/ERC721/IERC721.sol'; +import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol'; +import '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Receiver.sol'; +import './ERC20Interface.sol'; +import './TransferHelper.sol'; +import './IForwarderV4.sol'; + +/** + * @title ForwarderV4 + * @notice This contract will forward any incoming Ether or token to the parent address of the contract + */ +contract ForwarderV4 is IERC721Receiver, ERC1155Receiver, IForwarderV4 { + /// @notice Any funds sent to this contract will be forwarded to this address + address public parentAddress; + /// @notice Address which is allowed to call methods on this contract alongwith the parentAddress + address public feeAddress; + bool public autoFlush721 = true; + bool public autoFlush1155 = true; + + /** + * @notice Event triggered when a deposit is received in the forwarder + * @param from Address from which the deposit is received + * @param value Amount of Ether received + * @param data Data sent along with the deposit + */ + event ForwarderDeposited(address from, uint256 value, bytes data); + + /** + * @notice Modifier that will execute internal code block only if the sender is from the allowed addresses + */ + modifier onlyAllowedAddress() { + require( + msg.sender == parentAddress || msg.sender == feeAddress, + 'Address is not allowed' + ); + _; + } + + /** + * @notice Modifier that will execute internal code block only if the contract has not been initialized yet + */ + modifier onlyUninitialized() { + require(parentAddress == address(0x0), 'Already initialized'); + _; + } + + /** + * @notice Default function; Gets called when Ether is deposited with no data, and forwards it to the parent address + */ + receive() external payable { + flush(); + } + + /** + * @notice Default function; Gets called when data is sent but does not match any other function + */ + fallback() external payable { + flush(); + } + + /** + * @notice Initialize the contract, and sets the destination address to that of the parent address + * @param _parentAddress Address to which the funds should be forwarded + * @param _feeAddress Address which is allowed to call methods on this contract alongwith the parentAddress + * @param _autoFlush721 Whether to automatically flush ERC721 tokens or not + * @param _autoFlush1155 Whether to automatically flush ERC1155 tokens or not + */ + function init( + address _parentAddress, + address _feeAddress, + bool _autoFlush721, + bool _autoFlush1155 + ) external onlyUninitialized { + require(_parentAddress != address(0x0), 'Invalid parent address'); + parentAddress = _parentAddress; + require(_feeAddress != address(0x0), 'Invalid fee address'); + feeAddress = _feeAddress; + + uint256 value = address(this).balance; + + /// @notice set whether we want to automatically flush erc721/erc1155 tokens or not + autoFlush721 = _autoFlush721; + autoFlush1155 = _autoFlush1155; + + if (value == 0) { + return; + } + + (bool success, ) = parentAddress.call{ value: value }(''); + require(success, 'Flush failed'); + + /** + * Since we are forwarding on initialization, + * we don't have the context of the original sender. + * We still emit an event about the forwarding but set + * the sender to the forwarder itself + */ + emit ForwarderDeposited(address(this), value, msg.data); + } + + /** + * @inheritdoc IForwarderV4 + */ + function setAutoFlush721(bool autoFlush) + external + virtual + override + onlyAllowedAddress + { + autoFlush721 = autoFlush; + } + + /** + * @inheritdoc IForwarderV4 + */ + function setAutoFlush1155(bool autoFlush) + external + virtual + override + onlyAllowedAddress + { + autoFlush1155 = autoFlush; + } + + /** + * ERC721 standard callback function for when a ERC721 is transfered. The forwarder will send the nft + * to the base wallet once the nft contract invokes this method after transfering the nft. + * + * @param _operator The address which called `safeTransferFrom` function + * @param _from The address of the sender + * @param _tokenId The token id of the nft + * @param data Additional data with no specified format, sent in call to `_to` + */ + function onERC721Received( + address _operator, + address _from, + uint256 _tokenId, + bytes memory data + ) external virtual override returns (bytes4) { + if (autoFlush721) { + IERC721 instance = IERC721(msg.sender); + require( + instance.supportsInterface(type(IERC721).interfaceId), + 'The caller does not support the ERC721 interface' + ); + /// this won't work for ERC721 re-entrancy + instance.safeTransferFrom(address(this), parentAddress, _tokenId, data); + } + + return this.onERC721Received.selector; + } + + /** + * @notice Method to allow for calls to other contracts. This method can only be called by the parent address + * @param target The target contract address whose method needs to be called + * @param value The amount of Ether to be sent + * @param data The calldata to be sent + */ + function callFromParent( + address target, + uint256 value, + bytes calldata data + ) external returns (bytes memory) { + require(msg.sender == parentAddress, 'Only Parent'); + (bool success, bytes memory returnedData) = target.call{ value: value }( + data + ); + require(success, 'Parent call execution failed'); + + return returnedData; + } + + /** + * @inheritdoc IERC1155Receiver + */ + function onERC1155Received( + address _operator, + address _from, + uint256 id, + uint256 value, + bytes calldata data + ) external virtual override returns (bytes4) { + IERC1155 instance = IERC1155(msg.sender); + require( + instance.supportsInterface(type(IERC1155).interfaceId), + 'The caller does not support the IERC1155 interface' + ); + + if (autoFlush1155) { + instance.safeTransferFrom(address(this), parentAddress, id, value, data); + } + + return this.onERC1155Received.selector; + } + + /** + * @inheritdoc IERC1155Receiver + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external virtual override returns (bytes4) { + IERC1155 instance = IERC1155(msg.sender); + require( + instance.supportsInterface(type(IERC1155).interfaceId), + 'The caller does not support the IERC1155 interface' + ); + + if (autoFlush1155) { + instance.safeBatchTransferFrom( + address(this), + parentAddress, + ids, + values, + data + ); + } + + return this.onERC1155BatchReceived.selector; + } + + /** + * @inheritdoc IForwarderV4 + */ + function flushTokens(address tokenContractAddress) + external + virtual + override + onlyAllowedAddress + { + ERC20Interface instance = ERC20Interface(tokenContractAddress); + address forwarderAddress = address(this); + uint256 forwarderBalance = instance.balanceOf(forwarderAddress); + if (forwarderBalance == 0) { + return; + } + + TransferHelper.safeTransfer( + tokenContractAddress, + parentAddress, + forwarderBalance + ); + } + + /** + * @inheritdoc IForwarderV4 + */ + function flushERC721Token(address tokenContractAddress, uint256 tokenId) + external + virtual + override + onlyAllowedAddress + { + IERC721 instance = IERC721(tokenContractAddress); + require( + instance.supportsInterface(type(IERC721).interfaceId), + 'The tokenContractAddress does not support the ERC721 interface' + ); + + address ownerAddress = instance.ownerOf(tokenId); + instance.transferFrom(ownerAddress, parentAddress, tokenId); + } + + /** + * @inheritdoc IForwarderV4 + */ + function flushERC1155Tokens(address tokenContractAddress, uint256 tokenId) + external + virtual + override + onlyAllowedAddress + { + IERC1155 instance = IERC1155(tokenContractAddress); + require( + instance.supportsInterface(type(IERC1155).interfaceId), + 'The caller does not support the IERC1155 interface' + ); + + address forwarderAddress = address(this); + uint256 forwarderBalance = instance.balanceOf(forwarderAddress, tokenId); + + instance.safeTransferFrom( + forwarderAddress, + parentAddress, + tokenId, + forwarderBalance, + '' + ); + } + + /** + * @inheritdoc IForwarderV4 + */ + function batchFlushERC1155Tokens( + address tokenContractAddress, + uint256[] calldata tokenIds + ) external virtual override onlyAllowedAddress { + IERC1155 instance = IERC1155(tokenContractAddress); + require( + instance.supportsInterface(type(IERC1155).interfaceId), + 'The caller does not support the IERC1155 interface' + ); + + address forwarderAddress = address(this); + uint256 length = tokenIds.length; + uint256[] memory amounts = new uint256[](tokenIds.length); + for (uint256 i; i < length; i++) { + amounts[i] = instance.balanceOf(forwarderAddress, tokenIds[i]); + } + + instance.safeBatchTransferFrom( + forwarderAddress, + parentAddress, + tokenIds, + amounts, + '' + ); + } + + /** + * @inheritdoc IForwarderV4 + */ + function batchFlushERC20Tokens(address[] calldata tokenContractAddresses) + external + virtual + override + onlyAllowedAddress + { + address forwarderAddress = address(this); + uint256 length = tokenContractAddresses.length; + for (uint256 i; i < length; i++) { + ERC20Interface instance = ERC20Interface(tokenContractAddresses[i]); + uint256 forwarderBalance = instance.balanceOf(forwarderAddress); + if (forwarderBalance == 0) { + continue; + } + + TransferHelper.safeTransfer( + tokenContractAddresses[i], + parentAddress, + forwarderBalance + ); + } + } + + /** + * @notice Flush the entire balance of the contract to the parent address. + */ + function flush() public { + uint256 value = address(this).balance; + + if (value == 0) { + return; + } + + (bool success, ) = parentAddress.call{ value: value }(''); + require(success, 'Flush failed'); + emit ForwarderDeposited(msg.sender, value, msg.data); + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC1155Receiver, IERC165) + returns (bool) + { + return + interfaceId == type(IForwarderV4).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/contracts/IForwarder.sol b/contracts/IForwarder.sol index 7e5681e..d35121e 100644 --- a/contracts/IForwarder.sol +++ b/contracts/IForwarder.sol @@ -19,7 +19,7 @@ interface IForwarder is IERC165 { function setAutoFlush1155(bool autoFlush) external; /** - * Execute a token transfer of the full balance from the forwarder token to the parent address + * Execute a token transfer of the full balance from the forwarder to the parent address * * @param tokenContractAddress the address of the erc20 token contract */ diff --git a/contracts/IForwarderV4.sol b/contracts/IForwarderV4.sol new file mode 100644 index 0000000..4c68d73 --- /dev/null +++ b/contracts/IForwarderV4.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import '@openzeppelin/contracts/utils/introspection/IERC165.sol'; + +interface IForwarderV4 is IERC165 { + /** + * Sets the autoflush721 parameter. + * + * @param autoFlush whether to autoflush erc721 tokens + */ + function setAutoFlush721(bool autoFlush) external; + + /** + * Sets the autoflush1155 parameter. + * + * @param autoFlush whether to autoflush erc1155 tokens + */ + function setAutoFlush1155(bool autoFlush) external; + + /** + * Execute a token transfer of the full balance from the forwarder to the parent address + * + * @param tokenContractAddress the address of the erc20 token contract + */ + function flushTokens(address tokenContractAddress) external; + + /** + * Execute a nft transfer from the forwarder to the parent address + * + * @param tokenContractAddress the address of the ERC721 NFT contract + * @param tokenId The token id of the nft + */ + function flushERC721Token(address tokenContractAddress, uint256 tokenId) + external; + + /** + * Execute a nft transfer from the forwarder to the parent address. + * + * @param tokenContractAddress the address of the ERC1155 NFT contract + * @param tokenId The token id of the nft + */ + function flushERC1155Tokens(address tokenContractAddress, uint256 tokenId) + external; + + /** + * Execute a batch nft transfer from the forwarder to the parent address. + * + * @param tokenContractAddress the address of the ERC1155 NFT contract + * @param tokenIds The token ids of the nfts + */ + function batchFlushERC1155Tokens( + address tokenContractAddress, + uint256[] calldata tokenIds + ) external; + + /** + * Execute a batch erc20 transfer from the forwarder to the parent address. + * + * @param tokenContractAddresses the addresses of the ERC20 token contracts + */ + function batchFlushERC20Tokens(address[] calldata tokenContractAddresses) + external; +} diff --git a/test/forwarderFactoryV4.js b/test/forwarderFactoryV4.js new file mode 100644 index 0000000..34b2faf --- /dev/null +++ b/test/forwarderFactoryV4.js @@ -0,0 +1,278 @@ +require('should'); + +const truffleAssert = require('truffle-assertions'); +const helpers = require('./helpers'); +const util = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const BigNumber = require('bignumber.js'); + +const ForwarderV4 = artifacts.require('./ForwarderV4.sol'); +const ForwarderFactoryV4 = artifacts.require('./ForwarderFactoryV4.sol'); + +const hre = require('hardhat'); + +const createForwarderFactory = async () => { + const forwarderContract = await ForwarderV4.new([], {}); + const forwarderFactory = await ForwarderFactoryV4.new( + forwarderContract.address + ); + return { + implementationAddress: forwarderContract.address, + factory: forwarderFactory + }; +}; + +const getBalanceInWei = async (address) => { + return new BigNumber(await web3.eth.getBalance(address)); +}; + +const createForwarder = async ( + factory, + implementationAddress, + parent, + feeAddress, + salt, + shouldAutoFlushERC721 = true, + shouldAutoFlushERC1155 = true +) => { + const inputSalt = util.setLengthLeft( + Buffer.from(util.stripHexPrefix(salt), 'hex'), + 32 + ); + + const calculationSalt = abi.soliditySHA3( + ['address', 'bytes32'], + [parent, inputSalt] + ); + + const initCode = helpers.getInitCode( + util.stripHexPrefix(implementationAddress) + ); + const forwarderAddress = helpers.getNextContractAddressCreate2( + factory.address, + calculationSalt, + initCode + ); + + await factory.createForwarder( + parent, + feeAddress, + inputSalt, + shouldAutoFlushERC721, + shouldAutoFlushERC1155 + ); + + return forwarderAddress; +}; + +describe('ForwarderFactoryV4', function () { + let accounts; + before(async () => { + await hre.network.provider.send('hardhat_reset'); + accounts = await web3.eth.getAccounts(); + }); + + it('Should create a functional forwarder using the factory', async function () { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ); + const startBalance = await getBalanceInWei(parent); + const startForwarderBalance = await getBalanceInWei(forwarderAddress); + + const amount = web3.utils.toWei('2', 'ether'); + await web3.eth.sendTransaction({ + from: accounts[1], + to: forwarderAddress, + value: amount + }); + + const endBalance = await getBalanceInWei(parent); + startBalance.plus(amount).eq(endBalance).should.be.true(); + const endForwarderBalance = await getBalanceInWei(forwarderAddress); + endForwarderBalance.eq(startForwarderBalance).should.be.true(); + }); + + it('Different salt should create forwarders at different addresses', async function () { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ); + + const salt2 = '0x12345678'; + const forwarderAddress2 = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt2, + undefined, + undefined + ); + + forwarderAddress.should.not.equal(forwarderAddress2); + }); + + it('Different creators should create forwarders at different addresses', async function () { + const { factory, implementationAddress } = await createForwarderFactory(); + const { factory: factory2, implementationAddress: implementationAddress2 } = + await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ); + const forwarderAddress2 = await createForwarder( + factory2, + implementationAddress2, + parent, + feeAddress, + salt, + undefined, + undefined + ); + + forwarderAddress.should.not.equal(forwarderAddress2); + }); + + it('Different parents should create forwarders at different addresses', async function () { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ); + + const parent2 = accounts[3]; + const forwarderAddress2 = await createForwarder( + factory, + implementationAddress, + parent2, + feeAddress, + salt, + undefined, + undefined + ); + + forwarderAddress.should.not.equal(forwarderAddress2); + }); + + [ + [true, 'true'], + [false, 'false'] + ].map(([shouldAutoFlush, label]) => { + it(`should assign the create a forwarder with ${label} autoflush721 params`, async () => { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + shouldAutoFlush, + undefined + ); + + const forwarderContract = await hre.ethers.getContractAt( + 'ForwarderV4', + forwarderAddress + ); + const autoFlush721 = await forwarderContract.autoFlush721(); + + autoFlush721.should.equal(shouldAutoFlush); + }); + + it(`should assign the create a forwarder with ${label} autoflush1155 params`, async () => { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + shouldAutoFlush + ); + + const forwarderContract = await hre.ethers.getContractAt( + 'ForwarderV4', + forwarderAddress + ); + const autoFlush1155 = await forwarderContract.autoFlush1155(); + autoFlush1155.should.equal(shouldAutoFlush); + }); + }); + + it('Should fail to create two contracts with the same inputs', async function () { + const { factory, implementationAddress } = await createForwarderFactory(); + + const parent = accounts[0]; + const feeAddress = accounts[2]; + const salt = '0x1234'; + const forwarderAddress = await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ); + await helpers.assertVMException( + async () => + await createForwarder( + factory, + implementationAddress, + parent, + feeAddress, + salt, + undefined, + undefined + ) + ); + }); +}); diff --git a/test/forwarderV4.js b/test/forwarderV4.js new file mode 100644 index 0000000..bcd308b --- /dev/null +++ b/test/forwarderV4.js @@ -0,0 +1,914 @@ +const should = require('should'); +const util = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const hre = require('hardhat'); +const truffleAssert = require('truffle-assertions'); + +const helpers = require('./helpers'); +const BigNumber = require('bignumber.js'); +const { makeInterfaceId } = require('@openzeppelin/test-helpers'); + +const ForwarderV4 = artifacts.require('./ForwarderV4.sol'); +const ForwarderFactoryV4 = artifacts.require('./ForwarderFactoryV4.sol'); +const ERC721 = artifacts.require('./MockERC721'); +const ERC1155 = artifacts.require('./MockERC1155'); +const AlwaysFalseERC165 = artifacts.require('./AlwaysFalseERC165.sol'); +const ReentryForwarder = artifacts.require('./ReentryForwarder'); +const Tether = artifacts.require('./TetherToken.sol'); + +const createForwarder = async (creator, parent, feeAddress) => { + const forwarderContract = await ForwarderV4.new([], { from: creator }); + await forwarderContract.init(parent, feeAddress, true, true); + return forwarderContract; +}; + +const createForwarderFactory = async () => { + const forwarderContract = await ForwarderV4.new([], {}); + const forwarderFactory = await ForwarderFactoryV4.new( + forwarderContract.address + ); + return { + implementationAddress: forwarderContract.address, + factory: forwarderFactory + }; +}; + +const getForwarderAddressCreate2 = async ( + factory, + implementationAddress, + parent, + salt +) => { + const inputSalt = util.setLengthLeft( + Buffer.from(util.stripHexPrefix(salt), 'hex'), + 32 + ); + + const calculationSalt = abi.soliditySHA3( + ['address', 'bytes32'], + [parent, inputSalt] + ); + + const initCode = helpers.getInitCode( + util.stripHexPrefix(implementationAddress) + ); + const forwarderAddress = helpers.getNextContractAddressCreate2( + factory.address, + calculationSalt, + initCode + ); + + return forwarderAddress; +}; + +const getBalanceInWei = async (address) => { + return new BigNumber(await web3.eth.getBalance(address)); +}; + +const assertVMException = (err, expectedErrMsg) => { + err.message.toString().should.containEql('VM Exception'); + if (expectedErrMsg) { + err.message.toString().should.containEql(expectedErrMsg); + } +}; + +const getMethodData = async function (types, values, methodName) { + const id = abi.methodID(methodName, types).toString('hex'); + const data = util.addHexPrefix( + id + abi.rawEncode(types, values).toString('hex') + ); + return data; +}; + +const FORWARDER_DEPOSITED_EVENT = 'ForwarderDeposited'; + +describe('ForwarderV4', function () { + let accounts; + let tetherTokenContract; + before(async () => { + await hre.network.provider.send('hardhat_reset'); + accounts = await web3.eth.getAccounts(); + + tetherTokenContract = await Tether.new('1000000', 'USDT', 'USDT', 6, { + from: accounts[0] + }); + const tetherBalance = await tetherTokenContract.balanceOf.call(accounts[0]); + tetherBalance.toString().should.eql('1000000'); + }); + + it('Basic forwarding test', async function () { + const forwarder = await createForwarder( + accounts[0], + accounts[0], + accounts[2] + ); + const startBalance = await getBalanceInWei(accounts[0]); + const amount = web3.utils.toWei('2', 'ether'); + + const tx = await web3.eth.sendTransaction({ + from: accounts[1], + to: forwarder.address, + value: amount + }); + + const endBalance = await getBalanceInWei(accounts[0]); + startBalance.plus(amount).eq(endBalance).should.be.true(); + const forwardedEvent = await helpers.getEventFromTransaction( + tx.transactionHash, + FORWARDER_DEPOSITED_EVENT + ); + + should.exist(forwardedEvent); + forwardedEvent.from.should.equal(accounts[1]); + forwardedEvent.value.should.equal(amount); + }); + + it('Flush on initialization', async function () { + // determine the forwarder contract address + const amount = web3.utils.toWei('5', 'ether'); + const baseAddress = accounts[1]; + const feeAddress = accounts[2]; + const factory = await createForwarderFactory(); + const forwarderAddress = await getForwarderAddressCreate2( + factory.factory, + factory.implementationAddress, + baseAddress, + '0x1234' + ); + const inputSalt = util.setLengthLeft( + Buffer.from(util.stripHexPrefix('0x1234'), 'hex'), + 32 + ); + + const startBalance = await getBalanceInWei(baseAddress); + + // send funds to the contract address first + await web3.eth.sendTransaction({ + from: accounts[3], + to: forwarderAddress, + value: amount + }); + + // Check that the ether is in the forwarder address and not yet in the base address + (await getBalanceInWei(forwarderAddress)).eq(amount).should.be.true(); + (await getBalanceInWei(baseAddress)).eq(startBalance).should.be.true(); + await factory.factory.createForwarder( + baseAddress, + feeAddress, + inputSalt, + true, + true + ); + + // Check that the ether was automatically flushed to the base address + (await getBalanceInWei(forwarderAddress)).eq(0).should.be.true(); + (await getBalanceInWei(baseAddress)) + .eq(startBalance.plus(amount)) + .should.be.true(); + }); + + it('Should forward with data passed', async function () { + const forwarder = await createForwarder( + accounts[0], + accounts[0], + accounts[2] + ); + const startBalance = await getBalanceInWei(accounts[0]); + const amount = web3.utils.toWei('2', 'ether'); + + await web3.eth.sendTransaction({ + from: accounts[1], + to: forwarder.address, + value: amount, + data: '0x1234abcd' + }); + + const endBalance = await getBalanceInWei(accounts[0]); + startBalance.plus(amount).eq(endBalance).should.be.true(); + }); + + it('Should not init twice', async function () { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + await truffleAssert.reverts( + forwarder.init(baseAddress, feeAddress, true, { from: baseAddress }) + ); + }); + + it('Should not init if fee address is invalid address', async function () { + const baseAddress = accounts[3]; + const feeAddress = '0x0000000000000000000000000000000000000000'; + try { + await createForwarder(baseAddress, baseAddress, feeAddress); + } catch (err) { + assertVMException( + err, + "VM Exception while processing transaction: reverted with reason string 'Invalid fee address'" + ); + } + }); + + it('Should not init if parent address is invalid address', async function () { + const baseAddress = '0x0000000000000000000000000000000000000000'; + const feeAddress = accounts[3]; + + try { + await createForwarder(baseAddress, baseAddress, feeAddress); + } catch (err) { + assertVMException( + err, + "VM Exception while processing transaction: reverted with reason string 'Invalid parent address'" + ); + } + }); + + it('should change autoFlush721 when calling setAutoFlush721 from allowed address', async () => { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + const initialState = await forwarder.autoFlush721(); + await forwarder.setAutoFlush721(!initialState, { from: baseAddress }); + + const newState = await forwarder.autoFlush721(); + initialState.should.equal(!newState); + }); + + it('should fail to toggle autoFlush721 if caller is not allowed address', async () => { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + await truffleAssert.reverts( + forwarder.setAutoFlush721(false, { from: accounts[5] }) + ); + }); + + it('should toggle autoFlush1155 when calling setAutoFlush1155 from allowed address', async () => { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + const initialState = await forwarder.autoFlush1155(); + await forwarder.setAutoFlush1155(!initialState, { from: feeAddress }); + + const newState = await forwarder.autoFlush1155(); + initialState.should.equal(!newState); + }); + + it('should fail to toggle autoFlush1155 if caller is not allowed address', async () => { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + await truffleAssert.reverts( + forwarder.setAutoFlush1155(false, { from: accounts[5] }) + ); + }); + + it('should batch flush erc20 tokens back to parent address when caller is allowed address', async () => { + const baseAddress = accounts[0]; + const feeAddress = accounts[2]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + await tetherTokenContract.transfer(forwarder.address, 100, { + from: accounts[0] + }); + const balance = await tetherTokenContract.balanceOf.call(accounts[0]); + balance.should.eql(web3.utils.toBN(1000000 - 100)); + + const forwarderContractStartTokens = + await tetherTokenContract.balanceOf.call(forwarder.address); + forwarderContractStartTokens.should.eql(web3.utils.toBN(100)); + + await forwarder.batchFlushERC20Tokens([tetherTokenContract.address], { + from: feeAddress + }); + + const forwarderAccountEndTokens = await tetherTokenContract.balanceOf.call( + forwarder.address + ); + forwarderAccountEndTokens.should.eql(web3.utils.toBN(0)); + + const balanceAtEnd = await tetherTokenContract.balanceOf.call(accounts[0]); + balanceAtEnd.should.eql(web3.utils.toBN(1000000)); + }); + + describe('NFT Support', function () { + let token721; + let tokenId = 0; + let baseAddress; + let feeAddress; + let autoFlushForwarder; + let noAutoFlushForwarder; + + before(async function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + token721 = await ERC721.new(name, symbol); + baseAddress = accounts[0]; + feeAddress = accounts[2]; + + autoFlushForwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + noAutoFlushForwarder = await ForwarderV4.new([], { + from: accounts[1] + }); + await noAutoFlushForwarder.init(baseAddress, feeAddress, false, false); + }); + + it('Should support NFT safeTransferFrom function', async function () { + const operator = accounts[0]; + const from = accounts[1]; + tokenId = tokenId + 1; + const data = 0x00; + const methodId = await noAutoFlushForwarder.onERC721Received.call( + operator, + from, + tokenId, + data + ); + methodId.should.eql('0x150b7a02'); + }); + + it('should revert if msg.sender in onERC721Received does not support ERC721', async function () { + const operator = accounts[0]; + const from = accounts[1]; + const tokenId = 123; + + const alwaysFalseERC165 = await AlwaysFalseERC165.new( + autoFlushForwarder.address + ); + + await truffleAssert.reverts( + alwaysFalseERC165.onERC721Received(operator, from, tokenId, [], { + from: operator + }) + ); + }); + + it('Should receive with safeTransferFrom function with auto flush', async function () { + tokenId = tokenId + 1; + const owner = accounts[5]; + await token721.mint(owner, tokenId); + await token721.safeTransferFrom( + owner, + autoFlushForwarder.address, + tokenId, + { from: owner } + ); + expect(await token721.ownerOf(tokenId)).to.be.equal(baseAddress); + }); + + it('Should receive with transferFrom function with auto flush', async function () { + tokenId = tokenId + 1; + const owner = accounts[5]; + await token721.mint(owner, tokenId); + await token721.transferFrom(owner, autoFlushForwarder.address, tokenId, { + from: owner + }); + expect(await token721.ownerOf(tokenId)).to.be.equal( + autoFlushForwarder.address + ); + }); + + it('Should receive with safeTransferFrom function with no auto flush', async function () { + tokenId = tokenId + 1; + const owner = accounts[4]; + await token721.mint(owner, tokenId); + await token721.safeTransferFrom( + owner, + noAutoFlushForwarder.address, + tokenId, + { from: owner } + ); + expect(await token721.ownerOf(tokenId)).to.be.equal( + noAutoFlushForwarder.address + ); + }); + + it('Should receive with transferFrom function with no auto flush', async function () { + tokenId = tokenId + 1; + const owner = accounts[4]; + await token721.mint(owner, tokenId); + await token721.transferFrom( + owner, + noAutoFlushForwarder.address, + tokenId, + { from: owner } + ); + expect(await token721.ownerOf(tokenId)).to.be.equal( + noAutoFlushForwarder.address + ); + }); + + it('Should be able to flush ERC721 tokens when forwarder is owner', async function () { + tokenId = tokenId + 1; + const owner = accounts[4]; + await token721.mint(owner, tokenId); + await token721.safeTransferFrom( + owner, + noAutoFlushForwarder.address, + tokenId, + { from: owner } + ); + expect(await token721.ownerOf(tokenId)).to.be.equal( + noAutoFlushForwarder.address + ); + + await noAutoFlushForwarder.flushERC721Token(token721.address, tokenId, { + from: feeAddress + }); + expect(await token721.ownerOf(tokenId)).to.be.equal(baseAddress); + }); + + it('Should be able to flush ERC721 tokens when forwarder is approved', async function () { + tokenId = tokenId + 1; + const owner = accounts[5]; + await token721.mint(owner, tokenId); + await token721.approve(autoFlushForwarder.address, tokenId, { + from: owner + }); + expect(await token721.getApproved(tokenId)).to.be.equal( + autoFlushForwarder.address + ); + + await autoFlushForwarder.flushERC721Token(token721.address, tokenId, { + from: feeAddress + }); + expect(await token721.ownerOf(tokenId)).to.be.equal(baseAddress); + }); + + it('Should fail to flush ERC721 tokens when forwarder is not owner or approved', async function () { + tokenId = tokenId + 1; + const owner = accounts[4]; + await token721.mint(owner, tokenId); + + try { + await autoFlushForwarder.flushERC721Token(token721.address, tokenId, { + from: feeAddress + }); + } catch (err) { + assertVMException(err); + } + }); + + describe('ERC1155', () => { + let owner; + let token1155; + + beforeEach(async function () { + owner = baseAddress; + token1155 = await ERC1155.new({ from: owner }); + }); + + const mint = async (to, tokenId, amount) => { + await token1155.mint(to, tokenId, amount, [], { from: owner }); + }; + + const transferERC1155 = async (from, to, tokenId, amount) => { + await token1155.safeTransferFrom(from, to, tokenId, amount, [], { + from + }); + }; + + const transferBatchERC1155 = async (from, to, tokenIds, amounts) => { + await token1155.safeBatchTransferFrom(from, to, tokenIds, amounts, [], { + from + }); + }; + + const assertBalances = async (tokenId, accounts, balances) => { + accounts.length.should.equal(balances.length); + for (let i = 0; i < accounts.length; i++) { + const balance = await token1155.balanceOf(accounts[i], tokenId); + balance.toNumber().should.equal(balances[i]); + } + }; + + describe('Flush', () => { + it('should flush erc1155 tokens back to parent address when caller is feeAddress', async () => { + const erc1155TokenId = 1; + const amount = 10; + await mint(noAutoFlushForwarder.address, erc1155TokenId, amount); + + const forwarderBalancePreFlush = await token1155.balanceOf( + noAutoFlushForwarder.address, + erc1155TokenId + ); + forwarderBalancePreFlush.toNumber().should.equal(amount); + + await noAutoFlushForwarder.flushERC1155Tokens( + token1155.address, + erc1155TokenId, + { from: feeAddress } + ); + + const forwarderBalancePostFlush = await token1155.balanceOf( + noAutoFlushForwarder.address, + erc1155TokenId + ); + forwarderBalancePostFlush.toNumber().should.equal(0); + }); + + it('should fail to flush erc1155 tokens when caller is not allowed address', async () => { + const owner = baseAddress; + const token1155 = await ERC1155.new({ from: owner }); + + const erc1155TokenId = 1; + await truffleAssert.reverts( + noAutoFlushForwarder.flushERC1155Tokens( + token1155.address, + erc1155TokenId, + { from: accounts[3] } + ) + ); + }); + + it('should batch flush erc1155 tokens back to parent address when caller is allowed address', async () => { + const erc1155TokenIds = [1, 2, 3]; + const amounts = [10, 20, 30]; + + for (let i = 0; i < erc1155TokenIds.length; i++) { + await mint( + noAutoFlushForwarder.address, + erc1155TokenIds[i], + amounts[i] + ); + } + + for (let i = 0; i < erc1155TokenIds.length; i++) { + await assertBalances( + erc1155TokenIds[i], + [noAutoFlushForwarder.address], + [amounts[i]] + ); + } + + await noAutoFlushForwarder.batchFlushERC1155Tokens( + token1155.address, + erc1155TokenIds, + { from: feeAddress } + ); + + for (let i = 0; i < erc1155TokenIds.length; i++) { + await assertBalances( + erc1155TokenIds[i], + [owner, noAutoFlushForwarder.address], + [amounts[i], 0] + ); + } + }); + + it('should fail to batch flush erc1155 tokens when caller is not allowed address', async () => { + const owner = baseAddress; + const token1155 = await ERC1155.new({ from: owner }); + + await truffleAssert.reverts( + noAutoFlushForwarder.batchFlushERC1155Tokens( + token1155.address, + [], + { + from: accounts[3] + } + ) + ); + }); + }); + + describe('ERC1155Receiver', () => { + it('should receive erc1155 tokens with autoflush off', async function () { + const erc1155TokenId = 1; + + const sender = accounts[1]; + await mint(sender, erc1155TokenId, 100); + + await transferERC1155( + sender, + noAutoFlushForwarder.address, + erc1155TokenId, + 10 + ); + + await assertBalances( + erc1155TokenId, + [owner, sender, noAutoFlushForwarder.address], + [0, 90, 10] + ); + }); + + it('should receive erc1155 tokens with autoflush on', async function () { + const erc1155TokenId = 1; + + const sender = accounts[1]; + await mint(sender, erc1155TokenId, 100); + + await transferERC1155( + sender, + autoFlushForwarder.address, + erc1155TokenId, + 10 + ); + + await assertBalances( + erc1155TokenId, + [owner, sender, autoFlushForwarder.address], + [10, 90, 0] + ); + }); + + it('should receive batch erc1155 tokens with autoflush off', async function () { + const erc1155TokenId1 = 1; + const erc1155TokenId2 = 2; + + const sender = accounts[1]; + await mint(sender, erc1155TokenId1, 100); + await mint(sender, erc1155TokenId2, 50); + + await transferBatchERC1155( + sender, + noAutoFlushForwarder.address, + [erc1155TokenId1, erc1155TokenId2], + [10, 20] + ); + + await assertBalances( + erc1155TokenId1, + [owner, sender, noAutoFlushForwarder.address], + [0, 90, 10] + ); + + await assertBalances( + erc1155TokenId2, + [owner, sender, noAutoFlushForwarder.address], + [0, 30, 20] + ); + }); + + it('should receive batch erc1155 tokens with autoflush on', async function () { + const erc1155TokenId1 = 1; + const erc1155TokenId2 = 2; + + const sender = accounts[1]; + await mint(sender, erc1155TokenId1, 100); + await mint(sender, erc1155TokenId2, 50); + + await transferBatchERC1155( + sender, + autoFlushForwarder.address, + [erc1155TokenId1, erc1155TokenId2], + [10, 20] + ); + + await assertBalances( + erc1155TokenId1, + [owner, sender, autoFlushForwarder.address], + [10, 90, 0] + ); + + await assertBalances( + erc1155TokenId2, + [owner, sender, autoFlushForwarder.address], + [20, 30, 0] + ); + }); + + it('should revert if msg.sender does not support IERC1155', async () => { + await truffleAssert.reverts( + noAutoFlushForwarder.onERC1155Received( + accounts[0], + accounts[1], + 0, + 0, + [], + { + from: accounts[0] + } + ) + ); + + await truffleAssert.reverts( + noAutoFlushForwarder.onERC1155BatchReceived( + accounts[0], + accounts[1], + [], + [], + [], + { + from: accounts[0] + } + ) + ); + }); + }); + }); + }); + + describe('ERC165', function () { + const INTERFACE_IDS = { + IERC1155Receiver: makeInterfaceId.ERC165([ + 'onERC1155Received(address,address,uint256,uint256,bytes)', + 'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)' + ]), + IForwarderV4: makeInterfaceId.ERC165([ + 'setAutoFlush721(bool)', + 'setAutoFlush1155(bool)', + 'flushTokens(address)', + 'flushERC721Token(address,uint256)', + 'flushERC1155Tokens(address,uint256)', + 'batchFlushERC1155Tokens(address,uint256[])', + 'batchFlushERC20Tokens(address[])' + ]) + }; + + Object.entries(INTERFACE_IDS).map(([eipInterface, interfaceId]) => { + it(`should support ${eipInterface}`, async function () { + const baseAddress = accounts[3]; + const feeAddress = accounts[4]; + const forwarder = await createForwarder( + baseAddress, + baseAddress, + feeAddress + ); + + const supportsInterface = await forwarder.supportsInterface( + interfaceId + ); + supportsInterface.should.equal(true); + }); + }); + }); + + describe('Re-Entrancy', function () { + let reentryForwarderInstance; + let forwarder; + let tokenId = 1; + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + let owner; + let token1155; + let token721; + + before(async function () { + reentryForwarderInstance = await ReentryForwarder.new(); + forwarder = await createForwarder( + accounts[0], + reentryForwarderInstance.address, + accounts[1] + ); + }); + beforeEach(async function () { + owner = accounts[0]; + token1155 = await ERC1155.new({ from: owner }); + token721 = await ERC721.new(name, symbol); + }); + + it('should fail with reentry set to true for onERC721Received function', async function () { + let to = forwarder.address; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(true); + + try { + await token721.mint(to, tokenId); + } catch (err) { + assertVMException( + err, + 'ReentryForwarder: onERC721Received failed call' + ); + } + }); + + it('should pass with reentry set to false for onERC721Received function', async function () { + let to = forwarder.address; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(false); + + await token721.mint(to, tokenId); + assert.equal( + await token721.ownerOf(tokenId), + reentryForwarderInstance.address + ); + }); + + it('should fail with reentry set to true for onERC1155Received function', async function () { + let to = forwarder.address; + + let amount = 1; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(true); + + try { + await token1155.mint(to, tokenId, amount, [], { from: owner }); + } catch (err) { + assertVMException( + err, + 'ReentryForwarder: onERC1155Received failed call' + ); + } + }); + + it('should pass with reentry set to false for onERC1155Received function', async function () { + let to = forwarder.address; + + let amount = 1; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(false); + + await token1155.mint(to, tokenId, amount, [], { from: owner }); + + assert.equal( + await token1155.balanceOf(reentryForwarderInstance.address, tokenId), + 1 + ); + }); + + it('should fail with reentry set to true for onERC1155BatchReceived function', async function () { + let to = forwarder.address; + + let amount = 1; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(true); + + try { + await token1155.mintBatch(to, [tokenId], [amount], [], { from: owner }); + } catch (err) { + assertVMException( + err, + 'ReentryForwarder: onERC1155BatchReceived failed call' + ); + } + }); + + it('should pass with reentry set to false for onERC1155BatchReceived function', async function () { + let to = forwarder.address; + + let amount = 1; + await reentryForwarderInstance.setForwarder(to); + await reentryForwarderInstance.setReentry(false); + + await token1155.mintBatch(to, [tokenId], [amount], [], { from: owner }); + + assert.equal( + await token1155.balanceOf(reentryForwarderInstance.address, tokenId), + 1 + ); + }); + + it('should fail with reentry set to true for callFromParent function', async function () { + let parent = reentryForwarderInstance.address; + + await reentryForwarderInstance.setForwarder(forwarder.address); + await reentryForwarderInstance.setReentry(true); + let types = ['address', 'uint256', 'bytes']; + let values = [parent, 0, '']; + let methodName = 'callFromParent'; + let data = await getMethodData(types, values, methodName); + try { + await reentryForwarderInstance.dataCall(parent, 0, data); + } catch (err) { + assertVMException(err, 'dataCall execution failed'); + } + }); + + it('should pass with reentry set to false for callFromParent function', async function () { + let parent = reentryForwarderInstance.address; + + await reentryForwarderInstance.setForwarder(forwarder.address); + await reentryForwarderInstance.setReentry(false); + let types = ['address', 'uint256', 'bytes']; + let values = [parent, 0, '']; + let methodName = 'callFromParent'; + let data = await getMethodData(types, values, methodName); + + await reentryForwarderInstance.dataCall(parent, 0, data); + }); + }); +}); diff --git a/test/gas.js b/test/gas.js index b08db2f..3d9b56f 100644 --- a/test/gas.js +++ b/test/gas.js @@ -180,8 +180,8 @@ describe(`Wallet Operations Gas Usage`, function () { it('WalletSimple send batch [ @skip-on-coverage ]', async function () { const gasUsageByBatchSize = [ - 103386, 114701, 126027, 137341, 148655, 159971, 171285, 182599, 193902, - 205205 + 103374, 114689, 126027, 137341, 148655, 159947, 171285, 182599, 193914, + 205217 ]; for (let batchSize = 1; batchSize <= 10; batchSize++) {