diff --git a/contracts/ForwarderFactoryV4.sol b/contracts/ForwarderFactoryV4.sol new file mode 100644 index 0000000..77d1342 --- /dev/null +++ b/contracts/ForwarderFactoryV4.sol @@ -0,0 +1,80 @@ +// 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 + ); + + 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 in the salt so any contract deployed directly relies on the parent address + bytes32 finalSalt = keccak256(abi.encodePacked(parent, 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..c2c0b53 --- /dev/null +++ b/contracts/ForwarderV4.sol @@ -0,0 +1,353 @@ +// 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 './IForwarder.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, IForwarder { + /// @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 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 address'); + parentAddress = _parentAddress; + require(_feeAddress != address(0x0), 'Invalid 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); + } + + /** + * @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 data is sent but does not match any other function + */ + fallback() external payable { + flush(); + } + + /** + * @notice Default function; Gets called when Ether is deposited with no data, and forwards it to the parent address + */ + receive() external payable { + flush(); + } + + /** + * @inheritdoc IForwarder + */ + function setAutoFlush721(bool autoFlush) + external + virtual + override + onlyAllowedAddress + { + autoFlush721 = autoFlush; + } + + /** + * @inheritdoc IForwarder + */ + 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 allowed 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 callFromAllowedAddress( + address target, + uint256 value, + bytes calldata data + ) external onlyAllowedAddress returns (bytes memory) { + (bool success, bytes memory returnedData) = target.call{ value: value }( + data + ); + require(success, 'Allowed address 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 IForwarder + */ + 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 IForwarder + */ + 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 IForwarder + */ + 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 IForwarder + */ + 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[] memory amounts = new uint256[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + amounts[i] = instance.balanceOf(forwarderAddress, tokenIds[i]); + } + + instance.safeBatchTransferFrom( + forwarderAddress, + parentAddress, + tokenIds, + amounts, + '' + ); + } + + /** + * @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(IForwarder).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/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..e8eed1a --- /dev/null +++ b/test/forwarderV4.js @@ -0,0 +1,874 @@ +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 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; + before(async () => { + await hre.network.provider.send('hardhat_reset'); + accounts = await web3.eth.getAccounts(); + }); + + 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 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 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] }) + ); + }); + + 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)' + ]), + IForwarder: makeInterfaceId.ERC165([ + 'setAutoFlush721(bool)', + 'setAutoFlush1155(bool)', + 'flushTokens(address)', + 'flushERC721Token(address,uint256)', + 'flushERC1155Tokens(address,uint256)', + 'batchFlushERC1155Tokens(address,uint256[])' + ]) + }; + + 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 aef1262..6b2463d 100644 --- a/test/gas.js +++ b/test/gas.js @@ -178,7 +178,7 @@ describe(`Wallet Operations Gas Usage`, function () { it('WalletSimple send batch [ @skip-on-coverage ]', async function () { const gasUsageByBatchSize = [ - 101810, 113113, 124451, 135753, 147126, 158442, 169709, 181023, 192397, + 101810, 113113, 124451, 135753, 147126, 158383, 169709, 181023, 192397, 203641 ];