From a5f80e95d438003a51f0b64474dab7573e4b2715 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 24 May 2023 12:44:23 +1200 Subject: [PATCH 1/4] Add upgradeable contracts --- ...taMintBurnPackedBalanceUpgradeableMock.sol | 99 ++ .../ERC1155MetaMintBurnUpgradeableMock.sol | 94 ++ .../mocks/ERC1155MetadataUpgradeableMock.sol | 77 ++ ...55MintBurnPackedBalanceUpgradeableMock.sol | 98 ++ .../mocks/ERC1155MintBurnUpgradeableMock.sol | 93 ++ .../ERC1155MintBurnUpgradeableMockOwned.sol | 92 ++ .../mocks/ProxyUpgradeableDeployerMock.sol | 19 + src/contracts/proxies/ERC1967/IERC1967.sol | 8 + src/contracts/proxies/ERC1967/Proxy.sol | 62 ++ .../proxies/ERC1967/ProxyDeployer.sol | 72 ++ .../proxies/ERC1967/ProxyDeployerErrors.sol | 10 + .../proxies/ERC1967/ProxyUpgradeable.sol | 52 + .../ERC1967/ProxyUpgradeableDeployer.sol | 75 ++ .../ERC1155MetaPackedBalanceUpgradeable.sol | 381 ++++++++ ...RC1155MintBurnPackedBalanceUpgradeable.sol | 137 +++ .../ERC1155PackedBalanceUpgradeable.sol | 398 ++++++++ .../ERC1155MetaUpgradeable.sol | 376 ++++++++ .../ERC1155MetadataUpgradeable.sol | 115 +++ .../ERC1155MintBurnUpgradeable.sol | 108 +++ .../ERC1155Upgradeable/ERC1155Upgradeable.sol | 233 +++++ src/contracts/utils/ContextUpgradeable.sol | 37 + src/contracts/utils/Initializable.sol | 139 +++ src/contracts/utils/StorageSlot.sol | 64 ++ tests/ERC1155.spec.ts | 911 +++++++++--------- tests/ERC1155Metadata.spec.ts | 239 +++-- 25 files changed, 3443 insertions(+), 546 deletions(-) create mode 100644 src/contracts/mocks/ERC1155MetaMintBurnPackedBalanceUpgradeableMock.sol create mode 100644 src/contracts/mocks/ERC1155MetaMintBurnUpgradeableMock.sol create mode 100644 src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol create mode 100644 src/contracts/mocks/ERC1155MintBurnPackedBalanceUpgradeableMock.sol create mode 100644 src/contracts/mocks/ERC1155MintBurnUpgradeableMock.sol create mode 100644 src/contracts/mocks/ERC1155MintBurnUpgradeableMockOwned.sol create mode 100644 src/contracts/mocks/ProxyUpgradeableDeployerMock.sol create mode 100644 src/contracts/proxies/ERC1967/IERC1967.sol create mode 100644 src/contracts/proxies/ERC1967/Proxy.sol create mode 100644 src/contracts/proxies/ERC1967/ProxyDeployer.sol create mode 100644 src/contracts/proxies/ERC1967/ProxyDeployerErrors.sol create mode 100644 src/contracts/proxies/ERC1967/ProxyUpgradeable.sol create mode 100644 src/contracts/proxies/ERC1967/ProxyUpgradeableDeployer.sol create mode 100644 src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol create mode 100644 src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol create mode 100644 src/contracts/utils/ContextUpgradeable.sol create mode 100644 src/contracts/utils/Initializable.sol create mode 100644 src/contracts/utils/StorageSlot.sol diff --git a/src/contracts/mocks/ERC1155MetaMintBurnPackedBalanceUpgradeableMock.sol b/src/contracts/mocks/ERC1155MetaMintBurnPackedBalanceUpgradeableMock.sol new file mode 100644 index 0000000..ba3929e --- /dev/null +++ b/src/contracts/mocks/ERC1155MetaMintBurnPackedBalanceUpgradeableMock.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol"; +import "../tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; + + +contract ERC1155MetaMintBurnPackedBalanceUpgradeableMock is ERC1155MintBurnPackedBalanceUpgradeable, ERC1155MetaPackedBalanceUpgradeable, ERC1155MetadataUpgradeable { + + /***********************************| + | ERC165 | + |__________________________________*/ + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155PackedBalanceUpgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Minting Functions | + |__________________________________*/ + + /** + * @dev Mint _value of tokens of a given id + * @param _to The address to mint tokens to. + * @param _id token id to mint + * @param _value The amount to be minted + * @param _data Data to be passed if receiver is contract + */ + function mintMock(address _to, uint256 _id, uint256 _value, bytes memory _data) + public + { + _mint(_to, _id, _value, _data); + } + + /** + * @dev Mint tokens for each ids in _ids + * @param _to The address to mint tokens to. + * @param _ids Array of ids to mint + * @param _values Array of amount of tokens to mint per id + * @param _data Data to be passed if receiver is contract + */ + function batchMintMock(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) + public + { + _batchMint(_to, _ids, _values, _data); + } + + + /***********************************| + | Burning Functions | + |__________________________________*/ + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _id token id to burn + * @param _value The amount to be burned + */ + function burnMock(address _from, uint256 _id, uint256 _value) + public + { + _burn(_from, _id, _value); + } + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _ids Array of token ids to burn + * @param _values Array of the amount to be burned + */ + function batchBurnMock(address _from, uint256[] memory _ids, uint256[] memory _values) + public + { + _batchBurn(_from, _ids, _values); + } + + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () external { + revert("ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD"); + } +} diff --git a/src/contracts/mocks/ERC1155MetaMintBurnUpgradeableMock.sol b/src/contracts/mocks/ERC1155MetaMintBurnUpgradeableMock.sol new file mode 100644 index 0000000..4ee41f5 --- /dev/null +++ b/src/contracts/mocks/ERC1155MetaMintBurnUpgradeableMock.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; + + +contract ERC1155MetaMintBurnUpgradeableMock is ERC1155MetaUpgradeable, ERC1155MintBurnUpgradeable, ERC1155MetadataUpgradeable { + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155Upgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Minting Functions | + |__________________________________*/ + + /** + * @dev Mint _value of tokens of a given id + * @param _to The address to mint tokens to. + * @param _id token id to mint + * @param _value The amount to be minted + * @param _data Data to be passed if receiver is contract + */ + function mintMock(address _to, uint256 _id, uint256 _value, bytes memory _data) + public + { + super._mint(_to, _id, _value, _data); + } + + /** + * @dev Mint tokens for each ids in _ids + * @param _to The address to mint tokens to. + * @param _ids Array of ids to mint + * @param _values Array of amount of tokens to mint per id + * @param _data Data to be passed if receiver is contract + */ + function batchMintMock(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) + public + { + super._batchMint(_to, _ids, _values, _data); + } + + + /***********************************| + | Burning Functions | + |__________________________________*/ + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _id token id to burn + * @param _value The amount to be burned + */ + function burnMock(address _from, uint256 _id, uint256 _value) + public + { + super._burn(_from, _id, _value); + } + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _ids Array of token ids to burn + * @param _values Array of the amount to be burned + */ + function batchBurnMock(address _from, uint256[] memory _ids, uint256[] memory _values) + public + { + super._batchBurn(_from, _ids, _values); + } + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () virtual external { + revert("ERC1155MetaMintBurnMock: INVALID_METHOD"); + } +} diff --git a/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol b/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol new file mode 100644 index 0000000..e35c1c3 --- /dev/null +++ b/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; + +contract ERC1155MetadataUpgradeableMock is ERC1155MintBurnUpgradeable, ERC1155MetadataUpgradeable { + + /***********************************| + | Base URI Functions | + |__________________________________*/ + + /** + * @notice Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function setBaseMetadataURI(string memory _newBaseMetadataURI) public { + super._setBaseMetadataURI(_newBaseMetadataURI); + } + + /***********************************| + | Log URI Functions | + |__________________________________*/ + + /** + * @notice Will emit default URI log event for corresponding token _id + * @param _tokenIDs Array of IDs of tokens to log default URI + */ + function logURIsMock(uint256[] memory _tokenIDs) public { + super._logURIs(_tokenIDs); + } + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () external { + revert('ERC1155MetadataMock: INVALID_METHOD'); + } + + + /***********************************| + | ERC-165 Functions | + |__________________________________*/ + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155Upgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } +} + +/** + * A v2 implementation to test upgradeability. + */ +contract ERC1155MetadataUpgradeableMockV2 is ERC1155MetadataUpgradeableMock { + mapping(uint256 => uint256) private idMapping; + + function setIdMapping(uint256 _id, uint256 _mappedId) public { + idMapping[_id] = _mappedId; + } + + function uri(uint256 _id) public override view returns (string memory) { + return string(abi.encodePacked(baseURI, _uint2str(idMapping[_id]))); // Removes .json extension, swaps ids + } +} diff --git a/src/contracts/mocks/ERC1155MintBurnPackedBalanceUpgradeableMock.sol b/src/contracts/mocks/ERC1155MintBurnPackedBalanceUpgradeableMock.sol new file mode 100644 index 0000000..a9e5a2a --- /dev/null +++ b/src/contracts/mocks/ERC1155MintBurnPackedBalanceUpgradeableMock.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; + + +contract ERC1155MintBurnPackedBalanceUpgradeableMock is ERC1155MintBurnPackedBalanceUpgradeable, ERC1155MetadataUpgradeable { + + /***********************************| + | ERC165 | + |__________________________________*/ + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155PackedBalanceUpgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Minting Functions | + |__________________________________*/ + + /** + * @dev Mint _value of tokens of a given id + * @param _to The address to mint tokens to. + * @param _id token id to mint + * @param _value The amount to be minted + * @param _data Data to be passed if receiver is contract + */ + function mintMock(address _to, uint256 _id, uint256 _value, bytes memory _data) + public + { + _mint(_to, _id, _value, _data); + } + + /** + * @dev Mint tokens for each ids in _ids + * @param _to The address to mint tokens to. + * @param _ids Array of ids to mint + * @param _values Array of amount of tokens to mint per id + * @param _data Data to be passed if receiver is contract + */ + function batchMintMock(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) + public + { + _batchMint(_to, _ids, _values, _data); + } + + + /***********************************| + | Burning Functions | + |__________________________________*/ + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _id token id to burn + * @param _value The amount to be burned + */ + function burnMock(address _from, uint256 _id, uint256 _value) + public + { + _burn(_from, _id, _value); + } + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _ids Array of token ids to burn + * @param _values Array of the amount to be burned + */ + function batchBurnMock(address _from, uint256[] memory _ids, uint256[] memory _values) + public + { + _batchBurn(_from, _ids, _values); + } + + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () external { + revert("ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD"); + } +} diff --git a/src/contracts/mocks/ERC1155MintBurnUpgradeableMock.sol b/src/contracts/mocks/ERC1155MintBurnUpgradeableMock.sol new file mode 100644 index 0000000..6806640 --- /dev/null +++ b/src/contracts/mocks/ERC1155MintBurnUpgradeableMock.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; + + +contract ERC1155MintBurnUpgradeableMock is ERC1155MintBurnUpgradeable, ERC1155MetadataUpgradeable { + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155Upgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Minting Functions | + |__________________________________*/ + + /** + * @dev Mint _value of tokens of a given id + * @param _to The address to mint tokens to. + * @param _id token id to mint + * @param _value The amount to be minted + * @param _data Data to be passed if receiver is contract + */ + function mintMock(address _to, uint256 _id, uint256 _value, bytes memory _data) + public + { + super._mint(_to, _id, _value, _data); + } + + /** + * @dev Mint tokens for each ids in _ids + * @param _to The address to mint tokens to. + * @param _ids Array of ids to mint + * @param _values Array of amount of tokens to mint per id + * @param _data Data to be passed if receiver is contract + */ + function batchMintMock(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) + public + { + super._batchMint(_to, _ids, _values, _data); + } + + + /***********************************| + | Burning Functions | + |__________________________________*/ + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _id token id to burn + * @param _value The amount to be burned + */ + function burnMock(address _from, uint256 _id, uint256 _value) + public + { + super._burn(_from, _id, _value); + } + + /** + * @dev burn _value of tokens of a given token id + * @param _from The address to burn tokens from. + * @param _ids Array of token ids to burn + * @param _values Array of the amount to be burned + */ + function batchBurnMock(address _from, uint256[] memory _ids, uint256[] memory _values) + public + { + super._batchBurn(_from, _ids, _values); + } + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () virtual external { + revert("ERC1155MintBurnMock: INVALID_METHOD"); + } +} diff --git a/src/contracts/mocks/ERC1155MintBurnUpgradeableMockOwned.sol b/src/contracts/mocks/ERC1155MintBurnUpgradeableMockOwned.sol new file mode 100644 index 0000000..9229107 --- /dev/null +++ b/src/contracts/mocks/ERC1155MintBurnUpgradeableMockOwned.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol"; +import "../tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol"; +import "../utils/Ownable.sol"; + + +contract ERC1155MintBurnUpgradeableMockOwned is ERC1155MintBurnUpgradeable, Ownable, ERC1155MetadataUpgradeable { + + /** + * @notice Query if a contract implements an interface + * @dev Parent contract inheriting multiple contracts with supportsInterface() + * need to implement an overriding supportsInterface() function specifying + * all inheriting contracts that have a supportsInterface() function. + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` + */ + function supportsInterface( + bytes4 _interfaceID + ) public override( + ERC1155Upgradeable, + ERC1155MetadataUpgradeable + ) view virtual returns (bool) { + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Minting Functions | + |__________________________________*/ + + /** + * @dev Mint _value of tokens of a given id + * @param _to The address to mint tokens to. + * @param _id token id to mint + * @param _value The amount to be minted + * @param _data Data to be passed if receiver is contract + */ + function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) onlyOwner + public + { + super._mint(_to, _id, _value, _data); + } + + /** + * @dev Mint tokens for each ids in _ids + * @param _to The address to mint tokens to. + * @param _ids Array of ids to mint + * @param _values Array of amount of tokens to mint per id + * @param _data Data to be passed if receiver is contract + */ + function batchMint(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) onlyOwner + public + { + super._batchMint(_to, _ids, _values, _data); + } + + + /***********************************| + | Burning Functions | + |__________________________________*/ + + /** + * @dev burn _value of tokens of a given token id + * @param _id token id to burn + * @param _value The amount to be burned + */ + function burn(uint256 _id, uint256 _value) + public + { + super._burn(msg.sender, _id, _value); + } + + /** + * @dev burn _value of tokens of a given token id + * @param _ids Array of token ids to burn + * @param _values Array of the amount to be burned + */ + function batchBurn(uint256[] memory _ids, uint256[] memory _values) + public + { + super._batchBurn(msg.sender, _ids, _values); + } + + /***********************************| + | Unsupported Functions | + |__________________________________*/ + + fallback () virtual external { + revert("ERC1155MintBurnMockOwned: INVALID_METHOD"); + } +} diff --git a/src/contracts/mocks/ProxyUpgradeableDeployerMock.sol b/src/contracts/mocks/ProxyUpgradeableDeployerMock.sol new file mode 100644 index 0000000..65f3b3d --- /dev/null +++ b/src/contracts/mocks/ProxyUpgradeableDeployerMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {ProxyUpgradeableDeployer} from "../proxies/ERC1967/ProxyUpgradeableDeployer.sol"; + +contract ProxyUpgradeableDeployerMock is ProxyUpgradeableDeployer { + + /** + * Creates an upgradeable proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @param salt The deployment salt + * @param adminAddr The proxy admin address + * @return proxyAddr The address of the deployed proxy + */ + function createProxy(address implAddr, bytes32 salt, address adminAddr) public returns (address proxyAddr) { + return super.deployProxy(implAddr, salt, adminAddr); + } + +} diff --git a/src/contracts/proxies/ERC1967/IERC1967.sol b/src/contracts/proxies/ERC1967/IERC1967.sol new file mode 100644 index 0000000..058f89b --- /dev/null +++ b/src/contracts/proxies/ERC1967/IERC1967.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +interface IERC1967 { + event Upgraded(address indexed implementation); + event AdminChanged(address previousAdmin, address newAdmin); + event BeaconUpgraded(address indexed beacon); +} diff --git a/src/contracts/proxies/ERC1967/Proxy.sol b/src/contracts/proxies/ERC1967/Proxy.sol new file mode 100644 index 0000000..833e49e --- /dev/null +++ b/src/contracts/proxies/ERC1967/Proxy.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC1967} from "./IERC1967.sol"; +import {StorageSlot} from "../../utils/StorageSlot.sol"; + +contract Proxy is IERC1967 { + bytes32 internal constant _IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); + + /** + * Initializes the contract, setting proxy implementation address. + */ + constructor(address _implementation) { + _setImplementation(_implementation); + emit Upgraded(_implementation); + } + + /** + * Forward calls to the proxy implementation contract. + */ + receive() external payable { + proxy(); + } + + /** + * Forward calls to the proxy implementation contract. + */ + fallback() external payable { + proxy(); + } + + /** + * Forward calls to the proxy implementation contract. + */ + function proxy() private { + address target = _getImplementation(); + assembly { + let ptr := mload(0x40) + calldatacopy(ptr, 0, calldatasize()) + let result := delegatecall(gas(), target, ptr, calldatasize(), 0, 0) + let size := returndatasize() + returndatacopy(ptr, 0, size) + switch result + case 0 { revert(ptr, size) } + default { return(ptr, size) } + } + } + + /** + * Set the implementation address. + */ + function _setImplementation(address _implementation) internal { + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = _implementation; + } + + /** + * Returns the address of the current implementation. + */ + function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } +} diff --git a/src/contracts/proxies/ERC1967/ProxyDeployer.sol b/src/contracts/proxies/ERC1967/ProxyDeployer.sol new file mode 100644 index 0000000..dccae9e --- /dev/null +++ b/src/contracts/proxies/ERC1967/ProxyDeployer.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {ProxyDeployerErrors} from "./ProxyDeployerErrors.sol"; +import {Proxy} from "./Proxy.sol"; + +abstract contract ProxyDeployer is ProxyDeployerErrors { + /** + * Creates a proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @param salt The deployment salt + * @return proxyAddr The address of the deployed proxy + */ + function deployProxy(address implAddr, bytes32 salt) internal returns (address proxyAddr) { + bytes memory code = getProxyCode(implAddr); + + // Deploy it + assembly { + proxyAddr := create2(0, add(code, 32), mload(code), salt) + } + if (proxyAddr == address(0)) { + revert ProxyCreationFailed(); + } + return proxyAddr; + } + + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param implAddr The address of the proxy implementation + * @param salt The deployment salt + * @return proxyAddr The address of the deployed wrapper + */ + function predictProxyAddress(address implAddr, bytes32 salt) public view returns (address proxyAddr) { + bytes memory code = getProxyCode(implAddr); + return predictProxyAddress(code, salt); + } + + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param code The code of the wrapper implementation + * @param salt The deployment salt + * @return proxyAddr The address of the deployed wrapper + */ + function predictProxyAddress(bytes memory code, bytes32 salt) private view returns (address proxyAddr) { + address deployer = address(this); + bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, keccak256(code))); + return address(uint160(uint256(_data))); + } + + /** + * Returns the code of the proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @return code The code of the proxy contract + */ + function getProxyCode(address implAddr) private pure returns (bytes memory code) { + return abi.encodePacked(type(Proxy).creationCode, abi.encode(implAddr)); + } + + /** + * Checks if an address is a contract + * @param addr The address to check + * @return result True if the address is a contract + */ + function isContract(address addr) internal view returns (bool result) { + uint256 csize; + // solhint-disable-next-line no-inline-assembly + assembly { + csize := extcodesize(addr) + } + return csize != 0; + } +} diff --git a/src/contracts/proxies/ERC1967/ProxyDeployerErrors.sol b/src/contracts/proxies/ERC1967/ProxyDeployerErrors.sol new file mode 100644 index 0000000..5870e20 --- /dev/null +++ b/src/contracts/proxies/ERC1967/ProxyDeployerErrors.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +/** + * Errors for the Proxy Deployer contract. + */ +abstract contract ProxyDeployerErrors { + // Factories + error ProxyCreationFailed(); +} diff --git a/src/contracts/proxies/ERC1967/ProxyUpgradeable.sol b/src/contracts/proxies/ERC1967/ProxyUpgradeable.sol new file mode 100644 index 0000000..9e1bc72 --- /dev/null +++ b/src/contracts/proxies/ERC1967/ProxyUpgradeable.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {Proxy} from "./Proxy.sol"; +import {StorageSlot} from "../../utils/StorageSlot.sol"; + +error InvalidCaller(); + +contract ProxyUpgradeable is Proxy { + bytes32 internal constant _ADMIN_SLOT = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); + + constructor(address _implementation, address _admin) Proxy(_implementation) { + _setAdmin(_admin); + } + + modifier onlyAdmin() { + if (_getAdmin() != msg.sender) { + revert InvalidCaller(); + } + _; + } + + // + // Upgrade logic + // + function upgradeTo(address _implementation) external onlyAdmin { + _setImplementation(_implementation); + emit Upgraded(_implementation); + } + + // + // Admin logic + // + function updateAdmin(address _admin) external onlyAdmin { + emit AdminChanged(_getAdmin(), _admin); + _setAdmin(_admin); + } + + /** + * Set the admin address. + */ + function _setAdmin(address _admin) internal { + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = _admin; + } + + /** + * @dev Returns the current admin. + */ + function _getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + } +} diff --git a/src/contracts/proxies/ERC1967/ProxyUpgradeableDeployer.sol b/src/contracts/proxies/ERC1967/ProxyUpgradeableDeployer.sol new file mode 100644 index 0000000..544ff48 --- /dev/null +++ b/src/contracts/proxies/ERC1967/ProxyUpgradeableDeployer.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import {ProxyDeployerErrors} from "./ProxyDeployerErrors.sol"; +import {ProxyUpgradeable} from "./ProxyUpgradeable.sol"; + +abstract contract ProxyUpgradeableDeployer is ProxyDeployerErrors { + /** + * Creates an upgradeable proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @param salt The deployment salt + * @param adminAddr The proxy admin address + * @return proxyAddr The address of the deployed proxy + */ + function deployProxy(address implAddr, bytes32 salt, address adminAddr) internal returns (address proxyAddr) { + bytes memory code = getProxyCode(implAddr, adminAddr); + + // Deploy it + assembly { + proxyAddr := create2(0, add(code, 32), mload(code), salt) + } + if (proxyAddr == address(0)) { + revert ProxyCreationFailed(); + } + return proxyAddr; + } + + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param implAddr The address of the proxy implementation + * @param salt The deployment salt + * @param adminAddr The address of the proxy admin + * @return proxyAddr The address of the deployed wrapper + */ + function predictProxyAddress(address implAddr, bytes32 salt, address adminAddr) public view returns (address proxyAddr) { + bytes memory code = getProxyCode(implAddr, adminAddr); + return predictProxyAddress(code, salt); + } + + /** + * Predict the deployed wrapper proxy address for a given implementation. + * @param code The code of the wrapper implementation + * @param salt The deployment salt + * @return proxyAddr The address of the deployed wrapper + */ + function predictProxyAddress(bytes memory code, bytes32 salt) private view returns (address proxyAddr) { + address deployer = address(this); + bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, keccak256(code))); + return address(uint160(uint256(_data))); + } + + /** + * Returns the code of the upgradeable proxy contract for a given implementation + * @param implAddr The address of the proxy implementation + * @param adminAddr The address of the proxy admin + * @return code The code of the proxy contract + */ + function getProxyCode(address implAddr, address adminAddr) private pure returns (bytes memory code) { + return abi.encodePacked(type(ProxyUpgradeable).creationCode, abi.encode(implAddr, adminAddr)); + } + + /** + * Checks if an address is a contract. + * @param addr The address to check + * @return result True if the address is a contract + */ + function isContract(address addr) internal view returns (bool result) { + uint256 csize; + // solhint-disable-next-line no-inline-assembly + assembly { + csize := extcodesize(addr) + } + return csize != 0; + } +} diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol new file mode 100644 index 0000000..f55e13d --- /dev/null +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ERC1155PackedBalanceUpgradeable.sol"; +import "../../interfaces/IERC20.sol"; +import "../../interfaces/IERC1155.sol"; +import "../../utils/LibBytes.sol"; +import "../../utils/SignatureValidator.sol"; + + +/** + * @dev ERC-1155 with native metatransaction methods. These additional functions allow users + * to presign function calls and allow third parties to execute these on their behalf. + * This contract uses an upgradeable context. + * + * Note: This contract is identical to the ERC1155Meta.sol contract, + * except for the ERC1155PackedBalanceUpgradeable parent contract. + */ +contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, SignatureValidator { + using LibBytes for bytes; + + /***********************************| + | Variables and Structs | + |__________________________________*/ + + /** + * Gas Receipt + * feeTokenData : (bool, address, ?unit256) + * 1st element should be the address of the token + * 2nd argument (if ERC-1155) should be the ID of the token + * Last element should be a 0x0 if ERC-20 and 0x1 for ERC-1155 + */ + struct GasReceipt { + uint256 gasFee; // Fixed cost for the tx + uint256 gasLimitCallback; // Maximum amount of gas the callback in transfer functions can use + address feeRecipient; // Address to send payment to + bytes feeTokenData; // Data for token to pay for gas + } + + // Which token standard is used to pay gas fee + enum FeeTokenType { + ERC1155, // 0x00, ERC-1155 token - DEFAULT + ERC20, // 0x01, ERC-20 token + NTypes // 0x02, number of signature types. Always leave at end. + } + + // Signature nonce per address + mapping (address => uint256) internal nonces; + + + /***********************************| + | Events | + |__________________________________*/ + + event NonceChange(address indexed signer, uint256 newNonce); + + + /****************************************| + | Public Meta Transfer Functions | + |_______________________________________*/ + + /** + * @notice Allows anyone with a valid signature to transfer _amount amount of a token _id on the bahalf of _from + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _isGasFee Whether gas is reimbursed to executor or not + * @param _data Encodes a meta transfer indicator, signature, gas payment receipt and extra transfer data + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSafeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bool _isGasFee, + bytes memory _data) + public + { + require(_to != address(0), "ERC1155MetaPackedBalance#metaSafeTransferFrom: INVALID_RECIPIENT"); + + // Initializing + bytes memory transferData; + GasReceipt memory gasReceipt; + + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _from, + _data, + abi.encode( + META_TX_TYPEHASH, + _from, // Address as uint256 + _to, // Address as uint256 + _id, + _amount, + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Transfer asset + _safeTransferFrom(_from, _to, _id, _amount); + + // If Gas is being reimbursed + if (_isGasFee) { + (gasReceipt, transferData) = abi.decode(signedData, (GasReceipt, bytes)); + + // We need to somewhat protect relayers against gas griefing attacks in recipient contract. + // Hence we only pass the gasLimit to the recipient such that the relayer knows the griefing + // limit. Nothing can prevent the receiver to revert the transaction as close to the gasLimit as + // possible, but the relayer can now only accept meta-transaction gasLimit within a certain range. + _callonERC1155Received(_from, _to, _id, _amount, gasReceipt.gasLimitCallback, transferData); + + // Transfer gas cost + _transferGasFee(_from, gasReceipt); + + } else { + _callonERC1155Received(_from, _to, _id, _amount, gasleft(), signedData); + } + } + + /** + * @notice Allows anyone with a valid signature to transfer multiple types of tokens on the bahalf of _from + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _isGasFee Whether gas is reimbursed to executor or not + * @param _data Encodes a meta transfer indicator, signature, gas payment receipt and extra transfer data + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSafeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bool _isGasFee, + bytes memory _data) + public + { + require(_to != address(0), "ERC1155MetaPackedBalance#metaSafeBatchTransferFrom: INVALID_RECIPIENT"); + + // Initializing + bytes memory transferData; + GasReceipt memory gasReceipt; + + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _from, + _data, + abi.encode( + META_BATCH_TX_TYPEHASH, + _from, // Address as uint256 + _to, // Address as uint256 + keccak256(abi.encodePacked(_ids)), + keccak256(abi.encodePacked(_amounts)), + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Transfer assets + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + + // If gas fee being reimbursed + if (_isGasFee) { + (gasReceipt, transferData) = abi.decode(signedData, (GasReceipt, bytes)); + + // We need to somewhat protect relayers against gas griefing attacks in recipient contract. + // Hence we only pass the gasLimit to the recipient such that the relayer knows the griefing + // limit. Nothing can prevent the receiver to revert the transaction as close to the gasLimit as + // possible, but the relayer can now only accept meta-transaction gasLimit within a certain range. + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasReceipt.gasLimitCallback, transferData); + + // Handle gas reimbursement + _transferGasFee(_from, gasReceipt); + + } else { + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasleft(), signedData); + } + } + + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Approve the passed address to spend on behalf of _from if valid signature is provided + * @param _owner Address that wants to set operator status _spender + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + * @param _isGasFee Whether gas will be reimbursed or not, with vlid signature + * @param _data Encodes signature and gas payment receipt + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSetApprovalForAll( + address _owner, + address _operator, + bool _approved, + bool _isGasFee, + bytes memory _data) + public + { + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _owner, + _data, + abi.encode( + META_APPROVAL_TYPEHASH, + _owner, // Address as uint256 + _operator, // Address as uint256 + _approved ? uint256(1) : uint256(0), // Boolean as uint256 + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Update operator status + operators[_owner][_operator] = _approved; + + // Emit event + emit ApprovalForAll(_owner, _operator, _approved); + + // Handle gas reimbursement + if (_isGasFee) { + GasReceipt memory gasReceipt = abi.decode(signedData, (GasReceipt)); + _transferGasFee(_owner, gasReceipt); + } + } + + + + /****************************************| + | Signature Validation Functions | + |_______________________________________*/ + + // keccak256( + // "metaSafeTransferFrom(address,address,uint256,uint256,bool,bytes)" + // ); + bytes32 internal constant META_TX_TYPEHASH = 0xce0b514b3931bdbe4d5d44e4f035afe7113767b7db71949271f6a62d9c60f558; + + // keccak256( + // "metaSafeBatchTransferFrom(address,address,uint256[],uint256[],bool,bytes)" + // ); + bytes32 internal constant META_BATCH_TX_TYPEHASH = 0xa3d4926e8cf8fe8e020cd29f514c256bc2eec62aa2337e415f1a33a4828af5a0; + + // keccak256( + // "metaSetApprovalForAll(address,address,bool,bool,bytes)" + // ); + bytes32 internal constant META_APPROVAL_TYPEHASH = 0xf5d4c820494c8595de274c7ff619bead38aac4fbc3d143b5bf956aa4b84fa524; + + /** + * @notice Verifies signatures for this contract + * @param _signer Address of signer + * @param _sigData Encodes signature, gas payment receipt and transfer data (if any) + * @param _encMembers Encoded EIP-712 type members (except nonce and _data), all need to be 32 bytes size + * @dev _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + * @dev A valid nonce is a nonce that is within 100 value from the current nonce + */ + function _signatureValidation( + address _signer, + bytes memory _sigData, + bytes memory _encMembers) + internal returns (bytes memory signedData) + { + bytes memory sig; + + // Get signature and data to sign + (sig, signedData) = abi.decode(_sigData, (bytes, bytes)); + + // Get current nonce and nonce used for signature + uint256 currentNonce = nonces[_signer]; // Lowest valid nonce for signer + uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object + + // Verify if nonce is valid + require( + (nonce >= currentNonce) && (nonce < (currentNonce + 100)), + "ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE" + ); + + // Take hash of bytes arrays + bytes32 hash = hashEIP712Message(keccak256(abi.encodePacked(_encMembers, nonce, keccak256(signedData)))); + + // Complete data to pass to signer verifier + bytes memory fullData = abi.encodePacked(_encMembers, nonce, signedData); + + //Update signature nonce + nonces[_signer] = nonce + 1; + emit NonceChange(_signer, nonce + 1); + + // Verify if _from is the signer + require(isValidSignature(_signer, hash, fullData, sig), "ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE"); + return signedData; + } + + /** + * @notice Returns the current nonce associated with a given address + * @param _signer Address to query signature nonce for + */ + function getNonce(address _signer) + public view returns (uint256 nonce) + { + return nonces[_signer]; + } + + + /***********************************| + | Gas Reimbursement Functions | + |__________________________________*/ + + /** + * @notice Will reimburse tx.origin or fee recipient for the gas spent execution a transaction + * Can reimbuse in any ERC-20 or ERC-1155 token + * @param _from Address from which the payment will be made from + * @param _g GasReceipt object that contains gas reimbursement information + */ + function _transferGasFee(address _from, GasReceipt memory _g) + internal + { + // Pop last byte to get token fee type + uint8 feeTokenTypeRaw = uint8(_g.feeTokenData.popLastByte()); + + // Ensure valid fee token type + require( + feeTokenTypeRaw < uint8(FeeTokenType.NTypes), + "ERC1155MetaPackedBalance#_transferGasFee: UNSUPPORTED_TOKEN" + ); + + // Convert to FeeTokenType corresponding value + FeeTokenType feeTokenType = FeeTokenType(feeTokenTypeRaw); + + // Declarations + address tokenAddress; + address feeRecipient; + uint256 tokenID; + uint256 fee = _g.gasFee; + + // If receiver is 0x0, then anyone can claim, otherwise, refund addresse provided + feeRecipient = _g.feeRecipient == address(0) ? _msgSender() : _g.feeRecipient; + + // Fee token is ERC1155 + if (feeTokenType == FeeTokenType.ERC1155) { + (tokenAddress, tokenID) = abi.decode(_g.feeTokenData, (address, uint256)); + + // Fee is paid from this ERC1155 contract + if (tokenAddress == address(this)) { + _safeTransferFrom(_from, feeRecipient, tokenID, fee); + + // No need to protect against griefing since recipient (if contract) is most likely owned by the relayer + _callonERC1155Received(_from, feeRecipient, tokenID, gasleft(), fee, ""); + + // Fee is paid from another ERC-1155 contract + } else { + IERC1155(tokenAddress).safeTransferFrom(_from, feeRecipient, tokenID, fee, ""); + } + + // Fee token is ERC20 + } else { + tokenAddress = abi.decode(_g.feeTokenData, (address)); + require( + IERC20(tokenAddress).transferFrom(_from, feeRecipient, fee), + "ERC1155MetaPackedBalance#_transferGasFee: ERC20_TRANSFER_FAILED" + ); + } + } +} diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol new file mode 100644 index 0000000..de041f4 --- /dev/null +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ERC1155PackedBalanceUpgradeable.sol"; + +/** + * @dev Multi-Fungible Tokens with minting and burning methods. These methods assume + * a parent contract to be executed as they are `internal` functions. This contract + * uses an upgradeable context. + */ +contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable { + + /****************************************| + | Minting Functions | + |_______________________________________*/ + + /** + * @notice Mint _amount of tokens of a given id + * @param _to The address to mint tokens to + * @param _id Token id to mint + * @param _amount The amount to be minted + * @param _data Data to pass if receiver is contract + */ + function _mint(address _to, uint256 _id, uint256 _amount, bytes memory _data) + internal + { + //Add _amount + _updateIDBalance(_to, _id, _amount, Operations.Add); // Add amount to recipient + + // Emit event + emit TransferSingle(_msgSender(), address(0x0), _to, _id, _amount); + + // Calling onReceive method if recipient is contract + _callonERC1155Received(address(0x0), _to, _id, _amount, gasleft(), _data); + } + + /** + * @notice Mint tokens for each (_ids[i], _amounts[i]) pair + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function _batchMint(address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) + internal + { + require(_ids.length == _amounts.length, "ERC1155MintBurnPackedBalance#_batchMint: INVALID_ARRAYS_LENGTH"); + + if (_ids.length > 0) { + // Load first bin and index where the token ID balance exists + (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); + + // Balance for current bin in memory (initialized with first transfer) + uint256 balTo = _viewUpdateBinValue(balances[_to][bin], index, _amounts[0], Operations.Add); + + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // Last bin updated + uint256 lastBin = bin; + + for (uint256 i = 1; i < nTransfer; i++) { + (bin, index) = getIDBinIndex(_ids[i]); + + // If new bin + if (bin != lastBin) { + // Update storage balance of previous bin + balances[_to][lastBin] = balTo; + balTo = balances[_to][bin]; + + // Bin will be the most recent bin + lastBin = bin; + } + + // Update memory balance + balTo = _viewUpdateBinValue(balTo, index, _amounts[i], Operations.Add); + } + + // Update storage of the last bin visited + balances[_to][bin] = balTo; + } + + // //Emit event + emit TransferBatch(_msgSender(), address(0x0), _to, _ids, _amounts); + + // Calling onReceive method if recipient is contract + _callonERC1155BatchReceived(address(0x0), _to, _ids, _amounts, gasleft(), _data); + } + + + /****************************************| + | Burning Functions | + |_______________________________________*/ + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function _burn(address _from, uint256 _id, uint256 _amount) + internal + { + // Substract _amount + _updateIDBalance(_from, _id, _amount, Operations.Sub); + + // Emit event + emit TransferSingle(_msgSender(), _from, address(0x0), _id, _amount); + } + + /** + * @notice Burn tokens of given token id for each (_ids[i], _amounts[i]) pair + * @dev This batchBurn method does not implement the most efficient way of updating + * balances to reduce the potential bug surface as this function is expected to + * be less common than transfers. EIP-2200 makes this method significantly + * more efficient already for packed balances. + * @param _from The address to burn tokens from + * @param _ids Array of token ids to burn + * @param _amounts Array of the amount to be burned + */ + function _batchBurn(address _from, uint256[] memory _ids, uint256[] memory _amounts) + internal + { + // Number of burning to execute + uint256 nBurn = _ids.length; + require(nBurn == _amounts.length, "ERC1155MintBurnPackedBalance#batchBurn: INVALID_ARRAYS_LENGTH"); + + // Executing all burning + for (uint256 i = 0; i < nBurn; i++) { + // Update storage balance + _updateIDBalance(_from, _ids[i], _amounts[i], Operations.Sub); // Add amount to recipient + } + + // Emit batch burn event + emit TransferBatch(_msgSender(), _from, address(0x0), _ids, _amounts); + } +} diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol new file mode 100644 index 0000000..19e83b1 --- /dev/null +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../interfaces/IERC1155TokenReceiver.sol"; +import "../../interfaces/IERC1155.sol"; +import "../../utils/Address.sol"; +import "../../utils/ContextUpgradeable.sol"; +import "../../utils/ERC165.sol"; + +/** + * @dev Implementation of Multi-Token Standard contract. This implementation of the ERC-1155 standard + * utilizes the fact that balances of different token ids can be concatenated within individual + * uint256 storage slots. This allows the contract to batch transfer tokens more efficiently at + * the cost of limiting the maximum token balance each address can hold. This limit is + * 2^IDS_BITS_SIZE, which can be adjusted below. In practice, using IDS_BITS_SIZE smaller than 16 + * did not lead to major efficiency gains. This contract uses an upgradeable context. + */ +contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 { + using Address for address; + + /***********************************| + | Variables and Events | + |__________________________________*/ + + // onReceive function signatures + bytes4 constant internal ERC1155_RECEIVED_VALUE = 0xf23a6e61; + bytes4 constant internal ERC1155_BATCH_RECEIVED_VALUE = 0xbc197c81; + + // Constants regarding bin sizes for balance packing + // IDS_BITS_SIZE **MUST** be a power of 2 (e.g. 2, 4, 8, 16, 32, 64, 128) + uint256 internal constant IDS_BITS_SIZE = 32; // Max balance amount in bits per token ID + uint256 internal constant IDS_PER_UINT256 = 256 / IDS_BITS_SIZE; // Number of ids per uint256 + + // Operations for _updateIDBalance + enum Operations { Add, Sub } + + // Token IDs balances ; balances[address][id] => balance (using array instead of mapping for efficiency) + mapping (address => mapping(uint256 => uint256)) internal balances; + + // Operators + mapping (address => mapping(address => bool)) internal operators; + + + /***********************************| + | Public Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) + public override + { + // Requirements + require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR"); + require(_to != address(0),"ERC1155PackedBalance#safeTransferFrom: INVALID_RECIPIENT"); + // require(_amount <= balances); Not necessary since checked with _viewUpdateBinValue() checks + + _safeTransferFrom(_from, _to, _id, _amount); + _callonERC1155Received(_from, _to, _id, _amount, gasleft(), _data); + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @dev Arrays should be sorted so that all ids in a same storage slot are adjacent (more efficient) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) + public override + { + // Requirements + require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155PackedBalance#safeBatchTransferFrom: INVALID_OPERATOR"); + require(_to != address(0),"ERC1155PackedBalance#safeBatchTransferFrom: INVALID_RECIPIENT"); + + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasleft(), _data); + } + + + /***********************************| + | Internal Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + */ + function _safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount) + internal + { + //Update balances + _updateIDBalance(_from, _id, _amount, Operations.Sub); // Subtract amount from sender + _updateIDBalance(_to, _id, _amount, Operations.Add); // Add amount to recipient + + // Emit event + emit TransferSingle(_msgSender(), _from, _to, _id, _amount); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) + */ + function _callonERC1155Received(address _from, address _to, uint256 _id, uint256 _amount, uint256 _gasLimit, bytes memory _data) + internal + { + // Check if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155Received{gas:_gasLimit}(_msgSender(), _from, _id, _amount, _data); + require(retval == ERC1155_RECEIVED_VALUE, "ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE"); + } + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @dev Arrays should be sorted so that all ids in a same storage slot are adjacent (more efficient) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + */ + function _safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) + internal + { + uint256 nTransfer = _ids.length; // Number of transfer to execute + require(nTransfer == _amounts.length, "ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); + + if (_from != _to && nTransfer > 0) { + // Load first bin and index where the token ID balance exists + (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); + + // Balance for current bin in memory (initialized with first transfer) + uint256 balFrom = _viewUpdateBinValue(balances[_from][bin], index, _amounts[0], Operations.Sub); + uint256 balTo = _viewUpdateBinValue(balances[_to][bin], index, _amounts[0], Operations.Add); + + // Last bin updated + uint256 lastBin = bin; + + for (uint256 i = 1; i < nTransfer; i++) { + (bin, index) = getIDBinIndex(_ids[i]); + + // If new bin + if (bin != lastBin) { + // Update storage balance of previous bin + balances[_from][lastBin] = balFrom; + balances[_to][lastBin] = balTo; + + balFrom = balances[_from][bin]; + balTo = balances[_to][bin]; + + // Bin will be the most recent bin + lastBin = bin; + } + + // Update memory balance + balFrom = _viewUpdateBinValue(balFrom, index, _amounts[i], Operations.Sub); + balTo = _viewUpdateBinValue(balTo, index, _amounts[i], Operations.Add); + } + + // Update storage of the last bin visited + balances[_from][bin] = balFrom; + balances[_to][bin] = balTo; + + // If transfer to self, just make sure all amounts are valid + } else { + for (uint256 i = 0; i < nTransfer; i++) { + require(balanceOf(_from, _ids[i]) >= _amounts[i], "ERC1155PackedBalance#_safeBatchTransferFrom: UNDERFLOW"); + } + } + + // Emit event + emit TransferBatch(_msgSender(), _from, _to, _ids, _amounts); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) + */ + function _callonERC1155BatchReceived(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, uint256 _gasLimit, bytes memory _data) + internal + { + // Pass data if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155BatchReceived{gas: _gasLimit}(_msgSender(), _from, _ids, _amounts, _data); + require(retval == ERC1155_BATCH_RECEIVED_VALUE, "ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE"); + } + } + + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) + external override + { + // Update operator status + operators[_msgSender()][_operator] = _approved; + emit ApprovalForAll(_msgSender(), _operator, _approved); + } + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return isOperator True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + public override view returns (bool isOperator) + { + return operators[_owner][_operator]; + } + + + /***********************************| + | Public Balance Functions | + |__________________________________*/ + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + public override view returns (uint256) + { + uint256 bin; + uint256 index; + + //Get bin and index of _id + (bin, index) = getIDBinIndex(_id); + return getValueInBin(balances[_owner][bin], index); + } + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders (sorted owners will lead to less gas usage) + * @param _ids ID of the Tokens (sorted ids will lead to less gas usage + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) + public override view returns (uint256[] memory) + { + uint256 n_owners = _owners.length; + require(n_owners == _ids.length, "ERC1155PackedBalance#balanceOfBatch: INVALID_ARRAY_LENGTH"); + + // First values + (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); + uint256 balance_bin = balances[_owners[0]][bin]; + uint256 last_bin = bin; + + // Initialization + uint256[] memory batchBalances = new uint256[](n_owners); + batchBalances[0] = getValueInBin(balance_bin, index); + + // Iterate over each owner and token ID + for (uint256 i = 1; i < n_owners; i++) { + (bin, index) = getIDBinIndex(_ids[i]); + + // SLOAD if bin changed for the same owner or if owner changed + if (bin != last_bin || _owners[i-1] != _owners[i]) { + balance_bin = balances[_owners[i]][bin]; + last_bin = bin; + } + + batchBalances[i] = getValueInBin(balance_bin, index); + } + + return batchBalances; + } + + + /***********************************| + | Packed Balance Functions | + |__________________________________*/ + + /** + * @notice Update the balance of a id for a given address + * @param _address Address to update id balance + * @param _id Id to update balance of + * @param _amount Amount to update the id balance + * @param _operation Which operation to conduct : + * Operations.Add: Add _amount to id balance + * Operations.Sub: Substract _amount from id balance + */ + function _updateIDBalance(address _address, uint256 _id, uint256 _amount, Operations _operation) + internal + { + uint256 bin; + uint256 index; + + // Get bin and index of _id + (bin, index) = getIDBinIndex(_id); + + // Update balance + balances[_address][bin] = _viewUpdateBinValue(balances[_address][bin], index, _amount, _operation); + } + + /** + * @notice Update a value in _binValues + * @param _binValues Uint256 containing values of size IDS_BITS_SIZE (the token balances) + * @param _index Index of the value in the provided bin + * @param _amount Amount to update the id balance + * @param _operation Which operation to conduct : + * Operations.Add: Add _amount to value in _binValues at _index + * Operations.Sub: Substract _amount from value in _binValues at _index + */ + function _viewUpdateBinValue(uint256 _binValues, uint256 _index, uint256 _amount, Operations _operation) + internal pure returns (uint256 newBinValues) + { + uint256 shift = IDS_BITS_SIZE * _index; + uint256 mask = (uint256(1) << IDS_BITS_SIZE) - 1; + + if (_operation == Operations.Add) { + newBinValues = _binValues + (_amount << shift); + require( + ((_binValues >> shift) & mask) + _amount < 2**IDS_BITS_SIZE, // Checks that no other id changed + "ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW" + ); + + } else if (_operation == Operations.Sub) { + newBinValues = _binValues - (_amount << shift); + require( + ((_binValues >> shift) & mask) >= _amount, // Checks that no other id changed + "ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW" + ); + + } else { + revert("ERC1155PackedBalance#_viewUpdateBinValue: INVALID_BIN_WRITE_OPERATION"); // Bad operation + } + + return newBinValues; + } + + /** + * @notice Return the bin number and index within that bin where ID is + * @param _id Token id + * @return bin index (Bin number, ID"s index within that bin) + */ + function getIDBinIndex(uint256 _id) + public pure returns (uint256 bin, uint256 index) + { + bin = _id / IDS_PER_UINT256; + index = _id % IDS_PER_UINT256; + return (bin, index); + } + + /** + * @notice Return amount in _binValues at position _index + * @param _binValues uint256 containing the balances of IDS_PER_UINT256 ids + * @param _index Index at which to retrieve amount + * @return amount at given _index in _bin + */ + function getValueInBin(uint256 _binValues, uint256 _index) + public pure returns (uint256) + { + // require(_index < IDS_PER_UINT256) is not required since getIDBinIndex ensures `_index < IDS_PER_UINT256` + + // Mask to retrieve data for a given binData + uint256 mask = (uint256(1) << IDS_BITS_SIZE) - 1; + + // Shift amount + uint256 rightShift = IDS_BITS_SIZE * _index; + return (_binValues >> rightShift) & mask; + } + + + /***********************************| + | ERC165 Functions | + |__________________________________*/ + + /** + * @notice Query if a contract implements an interface + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` and + */ + function supportsInterface(bytes4 _interfaceID) public override(ERC165, IERC165) virtual view returns (bool) { + if (_interfaceID == type(IERC1155).interfaceId) { + return true; + } + return super.supportsInterface(_interfaceID); + } +} diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol new file mode 100644 index 0000000..b4ff694 --- /dev/null +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ERC1155Upgradeable.sol"; +import "../../interfaces/IERC20.sol"; +import "../../interfaces/IERC1155.sol"; +import "../../utils/LibBytes.sol"; +import "../../utils/SignatureValidator.sol"; + +/** + * @dev ERC-1155 with native metatransaction methods. These additional functions allow users + * to presign function calls and allow third parties to execute these on their behalf. + * This contract uses an upgradeable context. + */ +contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { + using LibBytes for bytes; + + /***********************************| + | Variables and Structs | + |__________________________________*/ + + /** + * Gas Receipt + * feeTokenData : (bool, address, ?unit256) + * 1st element should be the address of the token + * 2nd argument (if ERC-1155) should be the ID of the token + * Last element should be a 0x0 if ERC-20 and 0x1 for ERC-1155 + */ + struct GasReceipt { + uint256 gasFee; // Fixed cost for the tx + uint256 gasLimitCallback; // Maximum amount of gas the callback in transfer functions can use + address feeRecipient; // Address to send payment to + bytes feeTokenData; // Data for token to pay for gas + } + + // Which token standard is used to pay gas fee + enum FeeTokenType { + ERC1155, // 0x00, ERC-1155 token - DEFAULT + ERC20, // 0x01, ERC-20 token + NTypes // 0x02, number of signature types. Always leave at end. + } + + // Signature nonce per address + mapping (address => uint256) internal nonces; + + + /***********************************| + | Events | + |__________________________________*/ + + event NonceChange(address indexed signer, uint256 newNonce); + + + /****************************************| + | Public Meta Transfer Functions | + |_______________________________________*/ + + /** + * @notice Allows anyone with a valid signature to transfer _amount amount of a token _id on the bahalf of _from + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _isGasFee Whether gas is reimbursed to executor or not + * @param _data Encodes a meta transfer indicator, signature, gas payment receipt and extra transfer data + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSafeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bool _isGasFee, + bytes memory _data) + public + { + require(_to != address(0), "ERC1155Meta#metaSafeTransferFrom: INVALID_RECIPIENT"); + + // Initializing + bytes memory transferData; + GasReceipt memory gasReceipt; + + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _from, + _data, + abi.encode( + META_TX_TYPEHASH, + _from, // Address as uint256 + _to, // Address as uint256 + _id, + _amount, + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Transfer asset + _safeTransferFrom(_from, _to, _id, _amount); + + // If Gas is being reimbursed + if (_isGasFee) { + (gasReceipt, transferData) = abi.decode(signedData, (GasReceipt, bytes)); + + // We need to somewhat protect relayers against gas griefing attacks in recipient contract. + // Hence we only pass the gasLimit to the recipient such that the relayer knows the griefing + // limit. Nothing can prevent the receiver to revert the transaction as close to the gasLimit as + // possible, but the relayer can now only accept meta-transaction gasLimit within a certain range. + _callonERC1155Received(_from, _to, _id, _amount, gasReceipt.gasLimitCallback, transferData); + + // Transfer gas cost + _transferGasFee(_from, gasReceipt); + + } else { + _callonERC1155Received(_from, _to, _id, _amount, gasleft(), signedData); + } + } + + /** + * @notice Allows anyone with a valid signature to transfer multiple types of tokens on the bahalf of _from + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _isGasFee Whether gas is reimbursed to executor or not + * @param _data Encodes a meta transfer indicator, signature, gas payment receipt and extra transfer data + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSafeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bool _isGasFee, + bytes memory _data) + public + { + require(_to != address(0), "ERC1155Meta#metaSafeBatchTransferFrom: INVALID_RECIPIENT"); + + // Initializing + bytes memory transferData; + GasReceipt memory gasReceipt; + + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _from, + _data, + abi.encode( + META_BATCH_TX_TYPEHASH, + _from, // Address as uint256 + _to, // Address as uint256 + keccak256(abi.encodePacked(_ids)), + keccak256(abi.encodePacked(_amounts)), + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Transfer assets + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + + // If gas fee being reimbursed + if (_isGasFee) { + (gasReceipt, transferData) = abi.decode(signedData, (GasReceipt, bytes)); + + // We need to somewhat protect relayers against gas griefing attacks in recipient contract. + // Hence we only pass the gasLimit to the recipient such that the relayer knows the griefing + // limit. Nothing can prevent the receiver to revert the transaction as close to the gasLimit as + // possible, but the relayer can now only accept meta-transaction gasLimit within a certain range. + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasReceipt.gasLimitCallback, transferData); + + // Handle gas reimbursement + _transferGasFee(_from, gasReceipt); + + } else { + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasleft(), signedData); + } + } + + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Approve the passed address to spend on behalf of _from if valid signature is provided + * @param _owner Address that wants to set operator status _spender + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + * @param _isGasFee Whether gas will be reimbursed or not, with vlid signature + * @param _data Encodes signature and gas payment receipt + * _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + */ + function metaSetApprovalForAll( + address _owner, + address _operator, + bool _approved, + bool _isGasFee, + bytes memory _data) + public + { + // Verify signature and extract the signed data + bytes memory signedData = _signatureValidation( + _owner, + _data, + abi.encode( + META_APPROVAL_TYPEHASH, + _owner, // Address as uint256 + _operator, // Address as uint256 + _approved ? uint256(1) : uint256(0), // Boolean as uint256 + _isGasFee ? uint256(1) : uint256(0) // Boolean as uint256 + ) + ); + + // Update operator status + operators[_owner][_operator] = _approved; + + // Emit event + emit ApprovalForAll(_owner, _operator, _approved); + + // Handle gas reimbursement + if (_isGasFee) { + GasReceipt memory gasReceipt = abi.decode(signedData, (GasReceipt)); + _transferGasFee(_owner, gasReceipt); + } + } + + + /****************************************| + | Signature Validation Functions | + |_______________________________________*/ + + // keccak256( + // "metaSafeTransferFrom(address,address,uint256,uint256,bool,bytes)" + // ); + bytes32 internal constant META_TX_TYPEHASH = 0xce0b514b3931bdbe4d5d44e4f035afe7113767b7db71949271f6a62d9c60f558; + + // keccak256( + // "metaSafeBatchTransferFrom(address,address,uint256[],uint256[],bool,bytes)" + // ); + bytes32 internal constant META_BATCH_TX_TYPEHASH = 0xa3d4926e8cf8fe8e020cd29f514c256bc2eec62aa2337e415f1a33a4828af5a0; + + // keccak256( + // "metaSetApprovalForAll(address,address,bool,bool,bytes)" + // ); + bytes32 internal constant META_APPROVAL_TYPEHASH = 0xf5d4c820494c8595de274c7ff619bead38aac4fbc3d143b5bf956aa4b84fa524; + + /** + * @notice Verifies signatures for this contract + * @param _signer Address of signer + * @param _sigData Encodes signature, gas payment receipt and transfer data (if any) + * @param _encMembers Encoded EIP-712 type members (except nonce and _data), all need to be 32 bytes size + * @dev _data should be encoded as ( + * (bytes32 r, bytes32 s, uint8 v, uint256 nonce, SignatureType sigType), + * (GasReceipt g, ?bytes transferData) + * ) + * i.e. high level encoding should be (bytes, bytes), where the latter bytes array is a nested bytes array + * @dev A valid nonce is a nonce that is within 100 value from the current nonce + */ + function _signatureValidation( + address _signer, + bytes memory _sigData, + bytes memory _encMembers) + internal returns (bytes memory signedData) + { + bytes memory sig; + + // Get signature and data to sign + (sig, signedData) = abi.decode(_sigData, (bytes, bytes)); + + // Get current nonce and nonce used for signature + uint256 currentNonce = nonces[_signer]; // Lowest valid nonce for signer + uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object + + // Verify if nonce is valid + require( + (nonce >= currentNonce) && (nonce < (currentNonce + 100)), + "ERC1155Meta#_signatureValidation: INVALID_NONCE" + ); + + // Take hash of bytes arrays + bytes32 hash = hashEIP712Message(keccak256(abi.encodePacked(_encMembers, nonce, keccak256(signedData)))); + + // Complete data to pass to signer verifier + bytes memory fullData = abi.encodePacked(_encMembers, nonce, signedData); + + //Update signature nonce + nonces[_signer] = nonce + 1; + emit NonceChange(_signer, nonce + 1); + + // Verify if _from is the signer + require(isValidSignature(_signer, hash, fullData, sig), "ERC1155Meta#_signatureValidation: INVALID_SIGNATURE"); + return signedData; + } + + /** + * @notice Returns the current nonce associated with a given address + * @param _signer Address to query signature nonce for + */ + function getNonce(address _signer) + public view returns (uint256 nonce) + { + return nonces[_signer]; + } + + + /***********************************| + | Gas Reimbursement Functions | + |__________________________________*/ + + /** + * @notice Will reimburse tx.origin or fee recipient for the gas spent execution a transaction + * Can reimbuse in any ERC-20 or ERC-1155 token + * @param _from Address from which the payment will be made from + * @param _g GasReceipt object that contains gas reimbursement information + */ + function _transferGasFee(address _from, GasReceipt memory _g) + internal + { + // Pop last byte to get token fee type + uint8 feeTokenTypeRaw = uint8(_g.feeTokenData.popLastByte()); + + // Ensure valid fee token type + require( + feeTokenTypeRaw < uint8(FeeTokenType.NTypes), + "ERC1155Meta#_transferGasFee: UNSUPPORTED_TOKEN" + ); + + // Convert to FeeTokenType corresponding value + FeeTokenType feeTokenType = FeeTokenType(feeTokenTypeRaw); + + // Declarations + address tokenAddress; + address feeRecipient; + uint256 tokenID; + uint256 fee = _g.gasFee; + + // If receiver is 0x0, then anyone can claim, otherwise, refund addresse provided + feeRecipient = _g.feeRecipient == address(0) ? _msgSender() : _g.feeRecipient; + + // Fee token is ERC1155 + if (feeTokenType == FeeTokenType.ERC1155) { + (tokenAddress, tokenID) = abi.decode(_g.feeTokenData, (address, uint256)); + + // Fee is paid from this ERC1155 contract + if (tokenAddress == address(this)) { + _safeTransferFrom(_from, feeRecipient, tokenID, fee); + + // No need to protect against griefing since recipient (if contract) is most likely owned by the relayer + _callonERC1155Received(_from, feeRecipient, tokenID, gasleft(), fee, ""); + + // Fee is paid from another ERC-1155 contract + } else { + IERC1155(tokenAddress).safeTransferFrom(_from, feeRecipient, tokenID, fee, ""); + } + + // Fee token is ERC20 + } else { + tokenAddress = abi.decode(_g.feeTokenData, (address)); + require( + IERC20(tokenAddress).transferFrom(_from, feeRecipient, fee), + "ERC1155Meta#_transferGasFee: ERC20_TRANSFER_FAILED" + ); + } + } +} diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol new file mode 100644 index 0000000..6db96ce --- /dev/null +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +import '../../interfaces/IERC1155Metadata.sol'; +import '../../utils/ERC165.sol'; +import '../../utils/Initializable.sol'; + +/** + * @notice Contract that handles metadata related methods. + * @dev Methods assume a deterministic generation of URI based on token IDs. + * Methods also assume that URI uses hex representation of token IDs. + * This contract uses a separate initializable function. + */ +contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { + // URI's default URI prefix + string public baseURI; + string public name; + + constructor() initializer {} + + /** + * @notice Set the initial name and base URI. + * @dev This function should be called once immediately after deployment. + */ + function initialize(string memory _name, string memory _baseURI) public virtual initializer { + name = _name; + baseURI = _baseURI; + } + + /***********************************| + | Metadata Public Functions | + |__________________________________*/ + + /** + * @notice A distinct Uniform Resource Identifier (URI) for a given token. + * @dev URIs are defined in RFC 3986. + * URIs are assumed to be deterministically generated based on token ID + * @return URI string + */ + function uri(uint256 _id) public virtual override view returns (string memory) { + return string(abi.encodePacked(baseURI, _uint2str(_id), ".json")); + } + + + /***********************************| + | Metadata Internal Functions | + |__________________________________*/ + + /** + * @notice Will emit default URI log event for corresponding token _id + * @param _tokenIDs Array of IDs of tokens to log default URI + */ + function _logURIs(uint256[] memory _tokenIDs) internal { + string memory baseURL = baseURI; + string memory tokenURI; + + for (uint256 i = 0; i < _tokenIDs.length; i++) { + tokenURI = string(abi.encodePacked(baseURL, _uint2str(_tokenIDs[i]), ".json")); + emit URI(tokenURI, _tokenIDs[i]); + } + } + + /** + * @notice Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function _setBaseMetadataURI(string memory _newBaseMetadataURI) internal { + baseURI = _newBaseMetadataURI; + } + + /** + * @notice Will update the name of the contract + * @param _newName New contract name + */ + function _setContractName(string memory _newName) internal { + name = _newName; + } + + /** + * @notice Query if a contract implements an interface + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` and + */ + function supportsInterface(bytes4 _interfaceID) public view virtual override returns (bool) { + if (_interfaceID == type(IERC1155Metadata).interfaceId) { + return true; + } + return super.supportsInterface(_interfaceID); + } + + /***********************************| + | Utility Internal Functions | + |__________________________________*/ + + function _uint2str(uint _i) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return '0'; + } + uint j = _i; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } +} diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol new file mode 100644 index 0000000..33d8352 --- /dev/null +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ERC1155Upgradeable.sol"; + +/** + * @dev Multi-Fungible Tokens with minting and burning methods. These methods assume + * a parent contract to be executed as they are `internal` functions. This contract + * uses an upgradeable context. + */ +contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { + + /****************************************| + | Minting Functions | + |_______________________________________*/ + + /** + * @notice Mint _amount of tokens of a given id + * @param _to The address to mint tokens to + * @param _id Token id to mint + * @param _amount The amount to be minted + * @param _data Data to pass if receiver is contract + */ + function _mint(address _to, uint256 _id, uint256 _amount, bytes memory _data) + internal + { + // Add _amount + balances[_to][_id] += _amount; + + // Emit event + emit TransferSingle(_msgSender(), address(0x0), _to, _id, _amount); + + // Calling onReceive method if recipient is contract + _callonERC1155Received(address(0x0), _to, _id, _amount, gasleft(), _data); + } + + /** + * @notice Mint tokens for each ids in _ids + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function _batchMint(address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) + internal + { + require(_ids.length == _amounts.length, "ERC1155MintBurn#batchMint: INVALID_ARRAYS_LENGTH"); + + // Number of mints to execute + uint256 nMint = _ids.length; + + // Executing all minting + for (uint256 i = 0; i < nMint; i++) { + // Update storage balance + balances[_to][_ids[i]] += _amounts[i]; + } + + // Emit batch mint event + emit TransferBatch(_msgSender(), address(0x0), _to, _ids, _amounts); + + // Calling onReceive method if recipient is contract + _callonERC1155BatchReceived(address(0x0), _to, _ids, _amounts, gasleft(), _data); + } + + + /****************************************| + | Burning Functions | + |_______________________________________*/ + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function _burn(address _from, uint256 _id, uint256 _amount) + internal + { + //Substract _amount + balances[_from][_id] -= _amount; + + // Emit event + emit TransferSingle(_msgSender(), _from, address(0x0), _id, _amount); + } + + /** + * @notice Burn tokens of given token id for each (_ids[i], _amounts[i]) pair + * @param _from The address to burn tokens from + * @param _ids Array of token ids to burn + * @param _amounts Array of the amount to be burned + */ + function _batchBurn(address _from, uint256[] memory _ids, uint256[] memory _amounts) + internal + { + // Number of mints to execute + uint256 nBurn = _ids.length; + require(nBurn == _amounts.length, "ERC1155MintBurn#batchBurn: INVALID_ARRAYS_LENGTH"); + + // Executing all minting + for (uint256 i = 0; i < nBurn; i++) { + // Update storage balance + balances[_from][_ids[i]] -= _amounts[i]; + } + + // Emit batch mint event + emit TransferBatch(_msgSender(), _from, address(0x0), _ids, _amounts); + } +} diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol new file mode 100644 index 0000000..d720299 --- /dev/null +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../interfaces/IERC1155TokenReceiver.sol"; +import "../../interfaces/IERC1155.sol"; +import "../../utils/Address.sol"; +import "../../utils/ContextUpgradeable.sol"; +import "../../utils/ERC165.sol"; + +/** + * @dev Implementation of Multi-Token Standard contract. + * This contract uses an upgradeable context. + */ +contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { + using Address for address; + + /***********************************| + | Variables and Events | + |__________________________________*/ + + // onReceive function signatures + bytes4 constant internal ERC1155_RECEIVED_VALUE = 0xf23a6e61; + bytes4 constant internal ERC1155_BATCH_RECEIVED_VALUE = 0xbc197c81; + + // Objects balances + mapping (address => mapping(uint256 => uint256)) internal balances; + + // Operator Functions + mapping (address => mapping(address => bool)) internal operators; + + + /***********************************| + | Public Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) + public override + { + require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155#safeTransferFrom: INVALID_OPERATOR"); + require(_to != address(0),"ERC1155#safeTransferFrom: INVALID_RECIPIENT"); + + _safeTransferFrom(_from, _to, _id, _amount); + _callonERC1155Received(_from, _to, _id, _amount, gasleft(), _data); + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) + public override + { + // Requirements + require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155#safeBatchTransferFrom: INVALID_OPERATOR"); + require(_to != address(0), "ERC1155#safeBatchTransferFrom: INVALID_RECIPIENT"); + + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, gasleft(), _data); + } + + + /***********************************| + | Internal Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + */ + function _safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount) + internal + { + // Update balances + balances[_from][_id] -= _amount; + balances[_to][_id] += _amount; + + // Emit event + emit TransferSingle(_msgSender(), _from, _to, _id, _amount); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) + */ + function _callonERC1155Received(address _from, address _to, uint256 _id, uint256 _amount, uint256 _gasLimit, bytes memory _data) + internal + { + // Check if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155Received{gas: _gasLimit}(_msgSender(), _from, _id, _amount, _data); + require(retval == ERC1155_RECEIVED_VALUE, "ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE"); + } + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + */ + function _safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) + internal + { + require(_ids.length == _amounts.length, "ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); + + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // Executing all transfers + for (uint256 i = 0; i < nTransfer; i++) { + // Update storage balance of previous bin + balances[_from][_ids[i]] -= _amounts[i]; + balances[_to][_ids[i]] += _amounts[i]; + } + + // Emit event + emit TransferBatch(_msgSender(), _from, _to, _ids, _amounts); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) + */ + function _callonERC1155BatchReceived(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, uint256 _gasLimit, bytes memory _data) + internal + { + // Pass data if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155BatchReceived{gas: _gasLimit}(_msgSender(), _from, _ids, _amounts, _data); + require(retval == ERC1155_BATCH_RECEIVED_VALUE, "ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE"); + } + } + + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) + external override + { + // Update operator status + operators[_msgSender()][_operator] = _approved; + emit ApprovalForAll(_msgSender(), _operator, _approved); + } + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return isOperator True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + public override view returns (bool isOperator) + { + return operators[_owner][_operator]; + } + + + /***********************************| + | Balance Functions | + |__________________________________*/ + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + public override view returns (uint256) + { + return balances[_owner][_id]; + } + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders + * @param _ids ID of the Tokens + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) + public override view returns (uint256[] memory) + { + require(_owners.length == _ids.length, "ERC1155#balanceOfBatch: INVALID_ARRAY_LENGTH"); + + // Variables + uint256[] memory batchBalances = new uint256[](_owners.length); + + // Iterate over each owner and token ID + for (uint256 i = 0; i < _owners.length; i++) { + batchBalances[i] = balances[_owners[i]][_ids[i]]; + } + + return batchBalances; + } + + + /***********************************| + | ERC165 Functions | + |__________________________________*/ + + /** + * @notice Query if a contract implements an interface + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` and + */ + function supportsInterface(bytes4 _interfaceID) public override(ERC165, IERC165) virtual view returns (bool) { + if (_interfaceID == type(IERC1155).interfaceId) { + return true; + } + return super.supportsInterface(_interfaceID); + } +} diff --git a/src/contracts/utils/ContextUpgradeable.sol b/src/contracts/utils/ContextUpgradeable.sol new file mode 100644 index 0000000..ac21633 --- /dev/null +++ b/src/contracts/utils/ContextUpgradeable.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; +import "./Initializable.sol"; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract ContextUpgradeable is Initializable { + function __Context_init() internal onlyInitializing { + } + + function __Context_init_unchained() internal onlyInitializing { + } + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/src/contracts/utils/Initializable.sol b/src/contracts/utils/Initializable.sol new file mode 100644 index 0000000..2779160 --- /dev/null +++ b/src/contracts/utils/Initializable.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/utils/Initializable.sol) + +pragma solidity ^0.8.2; + +// Note This uses Address.sol rather than OpenZeppelin's AddressUpgradeable.sol +import "./Address.sol"; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ``` + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } +} diff --git a/src/contracts/utils/StorageSlot.sol b/src/contracts/utils/StorageSlot.sol new file mode 100644 index 0000000..9fe9f9b --- /dev/null +++ b/src/contracts/utils/StorageSlot.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + assembly { + r.slot := slot + } + } +} diff --git a/tests/ERC1155.spec.ts b/tests/ERC1155.spec.ts index 4d47026..32ff784 100644 --- a/tests/ERC1155.spec.ts +++ b/tests/ERC1155.spec.ts @@ -16,547 +16,544 @@ import { ERC1155MetaMintBurnMock, ERC1155ReceiverMock, ERC1155OperatorMock } fro import { web3 } from 'hardhat' const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = createTestWallet(web3, 0) - const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = createTestWallet(web3, 2) - const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = createTestWallet(web3, 4) -describe('ERC1155', () => { - const MAXVAL = BigNumber.from(2) - .pow(256) - .sub(1) // 2**256 - 1 - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - let ownerAddress: string - let receiverAddress: string - let operatorAddress: string - let erc1155Abstract: AbstractContract - let operatorAbstract: AbstractContract - - let erc1155Contract: ERC1155MetaMintBurnMock - let operatorERC1155Contract: ERC1155MetaMintBurnMock - - // load contract abi and deploy to test server - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - - erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') - operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') - }) - - // deploy before each test, to reset state of contract - beforeEach(async () => { - erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock - operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnMock - }) - - describe('Getter functions', () => { - beforeEach(async () => { - await erc1155Contract.mintMock(ownerAddress, 5, 256, []) - await erc1155Contract.mintMock(receiverAddress, 66, 133, []) +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155' + (upgradeable ? 'Upgradeable': ''), () => { + const MAXVAL = BigNumber.from(2) + .pow(256) + .sub(1) // 2**256 - 1 + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let ownerAddress: string + let receiverAddress: string + let operatorAddress: string + let erc1155Abstract: AbstractContract + let operatorAbstract: AbstractContract + + let erc1155Contract: ERC1155MetaMintBurnMock + let operatorERC1155Contract: ERC1155MetaMintBurnMock + + // load contract abi and deploy to test server + before(async () => { + ownerAddress = await ownerWallet.getAddress() + receiverAddress = await receiverWallet.getAddress() + operatorAddress = await operatorWallet.getAddress() + + if (upgradeable) { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnUpgradeableMock') + } else { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') + } + operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') }) - it('balanceOf() should return types balance for queried address', async () => { - const balance5 = await erc1155Contract.balanceOf(ownerAddress, 5) - expect(balance5).to.be.eql(BigNumber.from(256)) - - const balance16 = await erc1155Contract.balanceOf(ownerAddress, 16) - expect(balance16).to.be.eql(BigNumber.from(0)) + // deploy before each test, to reset state of contract + beforeEach(async () => { + if (upgradeable) { + erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnMock + erc1155Contract.initialize(NAME, METADATA_URI) + } else { + erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock + } + operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnMock }) - it('balanceOfBatch() should return types balance for queried addresses', async () => { - const balances = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [5, 66]) - expect(balances[0]).to.be.eql(BigNumber.from(256)) - expect(balances[1]).to.be.eql(BigNumber.from(133)) - - const balancesNull = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [1337, 1337]) - expect(balancesNull[0]).to.be.eql(BigNumber.from(0)) - expect(balancesNull[1]).to.be.eql(BigNumber.from(0)) - }) + describe('Getter functions', () => { + beforeEach(async () => { + await erc1155Contract.mintMock(ownerAddress, 5, 256, []) + await erc1155Contract.mintMock(receiverAddress, 66, 133, []) + }) - it('supportsInterface(0x4e2312e0) on receiver should return true', async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - const returnedValue = await receiverContract.supportsInterface('0x4e2312e0') - await expect(returnedValue).to.be.equal(true) - }) + it('balanceOf() should return types balance for queried address', async () => { + const balance5 = await erc1155Contract.balanceOf(ownerAddress, 5) + expect(balance5).to.be.eql(BigNumber.from(256)) - it('supportsInterface(0x01ffc9a7) on receiver should return true', async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - const returnedValue = await receiverContract.supportsInterface('0x01ffc9a7') - await expect(returnedValue).to.be.equal(true) - }) + const balance16 = await erc1155Contract.balanceOf(ownerAddress, 16) + expect(balance16).to.be.eql(BigNumber.from(0)) + }) - it('supportsInterface(0x4e2312ee) on receiver should return false', async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - const returnedValue = await receiverContract.supportsInterface('0x4e2312ee') - await expect(returnedValue).to.be.equal(false) - }) - }) + it('balanceOfBatch() should return types balance for queried addresses', async () => { + const balances = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [5, 66]) + expect(balances[0]).to.be.eql(BigNumber.from(256)) + expect(balances[1]).to.be.eql(BigNumber.from(133)) - describe('safeTransferFrom() function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock + const balancesNull = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [1337, 1337]) + expect(balancesNull[0]).to.be.eql(BigNumber.from(0)) + expect(balancesNull[1]).to.be.eql(BigNumber.from(0)) + }) - beforeEach(async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - await erc1155Contract.mintMock(ownerAddress, 0, 256, []) - }) + it('supportsInterface(0x4e2312e0) on receiver should return true', async () => { + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + const returnedValue = await receiverContract.supportsInterface('0x4e2312e0') + await expect(returnedValue).to.be.equal(true) + }) - it('should be able to transfer if sufficient balance', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.fulfilled - }) + it('supportsInterface(0x01ffc9a7) on receiver should return true', async () => { + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + const returnedValue = await receiverContract.supportsInterface('0x01ffc9a7') + await expect(returnedValue).to.be.equal(true) + }) - it('should REVERT if insufficient balance', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 257, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + it('supportsInterface(0x4e2312ee) on receiver should return false', async () => { + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + const receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + const returnedValue = await receiverContract.supportsInterface('0x4e2312ee') + await expect(returnedValue).to.be.equal(false) + }) }) - it('should REVERT if sending to 0x0', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, ZERO_ADDRESS, 0, 1, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_RECIPIENT')) - }) + describe('safeTransferFrom() function', () => { + let receiverContract: ERC1155ReceiverMock + let operatorContract: ERC1155OperatorMock - it('should REVERT if operator not approved', async () => { - const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_OPERATOR')) - }) + beforeEach(async () => { + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + await erc1155Contract.mintMock(ownerAddress, 0, 256, []) + }) - it('should be able to transfer via operator if operator is approved', async () => { - // owner first gives operatorWallet address approval permission - await erc1155Contract.setApprovalForAll(operatorAddress, true) + it('should be able to transfer if sufficient balance', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - // operator performs a transfer - const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.fulfilled - }) + it('should REVERT if insufficient balance', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 257, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock(receiverAddress, 0, MAXVAL, []) - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, [], HIGH_GAS_LIMIT) - expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if sending to 0x0', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, ZERO_ADDRESS, 0, 1, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_RECIPIENT')) + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, erc1155Contract.address, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + it('should REVERT if operator not approved', async () => { + const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_OPERATOR')) + }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) + it('should be able to transfer via operator if operator is approved', async () => { + // owner first gives operatorWallet address approval permission + await erc1155Contract.setApprovalForAll(operatorAddress, true) - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) - }) + // operator performs a transfer + const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - await expect(tx).to.be.fulfilled - }) + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock(receiverAddress, 0, MAXVAL, []) + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, [], HIGH_GAS_LIMIT) + expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should pass if data is not null to receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, erc1155Contract.address, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - // NOTE: typechain generates the wrong type for `bytes` type at this time - // see https://github.com/ethereum-ts/TypeChain/issues/123 - // @ts-ignore - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, data) - await expect(tx).to.be.fulfilled - }) + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) - it('should have balances updated before onERC1155Received is called', async () => { - const fromPreBalance = await erc1155Contract.balanceOf(ownerAddress, 0) - const toPreBalance = await erc1155Contract.balanceOf(receiverContract.address, 0) + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) + }) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + it('should pass if data is not null to receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + // NOTE: typechain generates the wrong type for `bytes` type at this time + // see https://github.com/ethereum-ts/TypeChain/issues/123 + // @ts-ignore + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, data) + await expect(tx).to.be.fulfilled + }) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + it('should have balances updated before onERC1155Received is called', async () => { + const fromPreBalance = await erc1155Contract.balanceOf(ownerAddress, 0) + const toPreBalance = await erc1155Contract.balanceOf(receiverContract.address, 0) - expect(args._from).to.be.eql(ownerAddress) - expect(args._to).to.be.eql(receiverContract.address) - expect(args._fromBalance).to.be.eql(fromPreBalance.sub(1)) - expect(args._toBalance).to.be.eql(toPreBalance.add(1)) - }) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) - it('should have TransferSingle event emitted before onERC1155Received is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - const receipt = await tx.wait(1) + await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - expect(firstEventTopic).to.be.equal( - erc1155Contract.interface.getEventTopic( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] - ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics ) - ) - }) - - context('When successful transfer', () => { - let tx: ethers.ContractTransaction - beforeEach(async () => { - tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + expect(args._from).to.be.eql(ownerAddress) + expect(args._to).to.be.eql(receiverContract.address) + expect(args._fromBalance).to.be.eql(fromPreBalance.sub(1)) + expect(args._toBalance).to.be.eql(toPreBalance.add(1)) }) - it('should correctly update balance of sender', async () => { - const balance = await erc1155Contract.balanceOf(ownerAddress, 0) - expect(balance).to.be.eql(BigNumber.from(255)) - }) + it('should have TransferSingle event emitted before onERC1155Received is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + const receipt = await tx.wait(1) - it('should correctly update balance of receiver', async () => { - const balance = await erc1155Contract.balanceOf(receiverAddress, 0) - expect(balance).to.be.eql(BigNumber.from(1)) + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155Contract.interface.getEventTopic( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + ) + ) + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + ) + ) }) - describe('TransferSingle event', async () => { - let filterFromOperatorContract: ethers.EventFilter + context('When successful transfer', () => { + let tx: ethers.ContractTransaction - it('should emit TransferSingle event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferSingle') + beforeEach(async () => { + tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) - - tx = await operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) + it('should correctly update balance of sender', async () => { + const balance = await erc1155Contract.balanceOf(ownerAddress, 0) + expect(balance).to.be.eql(BigNumber.from(255)) }) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferSingle(operatorContract.address, null, null, null, null) - - // Set approval to operator contract - await erc1155Contract.setApprovalForAll(operatorContract.address, true) - - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.safeTransferFrom( - erc1155Contract.address, - ownerAddress, - receiverAddress, - 0, - 1, - [], - HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION - ) - - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + it('should correctly update balance of receiver', async () => { + const balance = await erc1155Contract.balanceOf(receiverAddress, 0) + expect(balance).to.be.eql(BigNumber.from(1)) + }) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) + describe('TransferSingle event', async () => { + let filterFromOperatorContract: ethers.EventFilter + + it('should emit TransferSingle event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferSingle') + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) + + tx = await operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferSingle(operatorContract.address, null, null, null, null) + + // Set approval to operator contract + await erc1155Contract.setApprovalForAll(operatorContract.address, true) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.safeTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + 0, + 1, + [], + HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) }) }) }) - }) - describe('safeBatchTransferFrom() function', () => { - let types: any[], values: any[] - const nTokenTypes = 30 //560 - const nTokensPerType = 10 + describe('safeBatchTransferFrom() function', () => { + let types: any[], values: any[] + const nTokenTypes = 30 //560 + const nTokensPerType = 10 - let receiverContract: ERC1155ReceiverMock + let receiverContract: ERC1155ReceiverMock - beforeEach(async () => { - ;(types = []), (values = []) - - // Minting enough values for transfer for each types - for (let i = 0; i < nTokenTypes; i++) { - types.push(i) - values.push(nTokensPerType) - } - await erc1155Contract.batchMintMock(ownerAddress, types, values, []) + beforeEach(async () => { + ;(types = []), (values = []) - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - }) + // Minting enough values for transfer for each types + for (let i = 0; i < nTokenTypes; i++) { + types.push(i) + values.push(nTokensPerType) + } + await erc1155Contract.batchMintMock(ownerAddress, types, values, []) - it('should be able to transfer tokens if sufficient balances', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - await expect(tx).to.be.fulfilled - }) + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + }) - it('should PASS if arrays are empty', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [], [], []) - await expect(tx).to.be.fulfilled - }) + it('should be able to transfer tokens if sufficient balances', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if insufficient balance', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0], [11], [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should PASS if arrays are empty', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [], [], []) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if single insufficient balance', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 11], [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if insufficient balance', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0], [11], [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should REVERT if operator not approved', async () => { - const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeBatchTransferFrom: INVALID_OPERATOR')) - }) + it('should REVERT if single insufficient balance', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 11], [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should REVERT if length of ids and values are not equal', async () => { - const tx1 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30, 0], [1, 9, 10], []) - await expect(tx1).to.be.rejectedWith(RevertError('ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) + it('should REVERT if operator not approved', async () => { + const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeBatchTransferFrom: INVALID_OPERATOR')) + }) - const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 10, 0], []) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) - }) + it('should REVERT if length of ids and values are not equal', async () => { + const tx1 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30, 0], [1, 9, 10], []) + await expect(tx1).to.be.rejectedWith(RevertError('ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) - it('should REVERT if sending to 0x0', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, ZERO_ADDRESS, types, values, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeBatchTransferFrom: INVALID_RECIPIENT')) - }) + const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 10, 0], []) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) + }) - it('should be able to transfer via operator if operator is approved', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + it('should REVERT if sending to 0x0', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, ZERO_ADDRESS, types, values, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeBatchTransferFrom: INVALID_RECIPIENT')) + }) - const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - await expect(tx).to.be.fulfilled - }) + it('should be able to transfer via operator if operator is approved', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock(receiverAddress, 5, MAXVAL, []) + const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + await expect(tx).to.be.fulfilled + }) - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [5], [1], [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock(receiverAddress, 5, MAXVAL, []) - it('should update balances of sender and receiver', async () => { - await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [5], [1], [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - let balanceFrom: ethers.BigNumber - let balanceTo: ethers.BigNumber + it('should update balances of sender and receiver', async () => { + await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - for (let i = 0; i < types.length; i++) { - balanceFrom = await erc1155Contract.balanceOf(ownerAddress, types[i]) - balanceTo = await erc1155Contract.balanceOf(receiverAddress, types[i]) + let balanceFrom: ethers.BigNumber + let balanceTo: ethers.BigNumber - expect(balanceFrom).to.be.eql(BigNumber.from(0)) - expect(balanceTo).to.be.eql(BigNumber.from(values[i])) - } - }) + for (let i = 0; i < types.length; i++) { + balanceFrom = await erc1155Contract.balanceOf(ownerAddress, types[i]) + balanceTo = await erc1155Contract.balanceOf(receiverAddress, types[i]) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, erc1155Contract.address, types, values, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + expect(balanceFrom).to.be.eql(BigNumber.from(0)) + expect(balanceTo).to.be.eql(BigNumber.from(values[i])) + } + }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE')) - }) + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, erc1155Contract.address, types, values, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.fulfilled - }) + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE')) + }) - it('should pass if data is not null from receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') - - // TODO: remove ts-ignore when contract declaration is fixed - // @ts-ignore - const tx = erc1155Contract.safeBatchTransferFrom( - ownerAddress, - receiverContract.address, - types, - values, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.fulfilled - }) + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.fulfilled + }) - it('should have balances updated before onERC1155BatchReceived is called', async () => { - const fromAddresses = Array(types.length).fill(ownerAddress) - const toAddresses = Array(types.length).fill(receiverContract.address) + it('should pass if data is not null from receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - const fromPreBalances = await erc1155Contract.balanceOfBatch(fromAddresses, types) - const toPreBalances = await erc1155Contract.balanceOfBatch(toAddresses, types) + // TODO: remove ts-ignore when contract declaration is fixed + // @ts-ignore + const tx = erc1155Contract.safeBatchTransferFrom( + ownerAddress, + receiverContract.address, + types, + values, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) + it('should have balances updated before onERC1155BatchReceived is called', async () => { + const fromAddresses = Array(types.length).fill(ownerAddress) + const toAddresses = Array(types.length).fill(receiverContract.address) - await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) + const fromPreBalances = await erc1155Contract.balanceOfBatch(fromAddresses, types) + const toPreBalances = await erc1155Contract.balanceOfBatch(toAddresses, types) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) + await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], HIGH_GAS_LIMIT) - expect(args._from).to.be.eql(ownerAddress) - expect(args._to).to.be.eql(receiverContract.address) - for (let i = 0; i < types.length; i++) { - expect(args._fromBalances[i]).to.be.eql(fromPreBalances[i].sub(values[i])) - expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(values[i])) - } - }) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155Contract.safeBatchTransferFrom( - ownerAddress, - receiverContract.address, - types, - values, - [], - HIGH_GAS_LIMIT - ) - const receipt = await tx.wait(1) - - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] - - expect(firstEventTopic).to.be.equal( - erc1155Contract.interface.getEventTopic( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] - ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics ) - ) - }) - - describe('TransferBatch event', async () => { - let tx: ethers.ContractTransaction - let filterFromOperatorContract: ethers.EventFilter - let operatorContract: ERC1155OperatorMock - beforeEach(async () => { - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + expect(args._from).to.be.eql(ownerAddress) + expect(args._to).to.be.eql(receiverContract.address) + for (let i = 0; i < types.length; i++) { + expect(args._fromBalances[i]).to.be.eql(fromPreBalances[i].sub(values[i])) + expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(values[i])) + } }) - it('should emit 1 TransferBatch events of N transfers', async () => { - const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155Contract.safeBatchTransferFrom( + ownerAddress, + receiverContract.address, + types, + values, + [], + HIGH_GAS_LIMIT + ) const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(types.length) + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155Contract.interface.getEventTopic( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + ) + ) + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + ) + ) }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + describe('TransferBatch event', async () => { + let tx: ethers.ContractTransaction + let filterFromOperatorContract: ethers.EventFilter + let operatorContract: ERC1155OperatorMock - tx = await operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + beforeEach(async () => { + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + }) - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) - }) + it('should emit 1 TransferBatch events of N transfers', async () => { + const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferBatch') - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferBatch(operatorContract.address, null, null, null, null) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(types.length) + }) - // Set approval to operator contract - await erc1155Contract.setApprovalForAll(operatorContract.address, true) + it('should have `msg.sender` as `_operator` field, not _from', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.safeBatchTransferFrom( - erc1155Contract.address, - ownerAddress, - receiverAddress, - types, - values, - [], - HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION - ) + tx = await operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) - }) - }) - }) + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferBatch(operatorContract.address, null, null, null, null) - describe('setApprovalForAll() function', () => { - it('should emit an ApprovalForAll event', async () => { - const tx = await erc1155Contract.setApprovalForAll(operatorAddress, true) - const receipt = await tx.wait(1) + // Set approval to operator contract + await erc1155Contract.setApprovalForAll(operatorContract.address, true) - expect(receipt.events![0].event).to.be.eql('ApprovalForAll') - }) + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.safeBatchTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + types, + values, + [], + HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION + ) - it('should set the operator status to _status argument', async () => { - const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) - await expect(tx).to.be.fulfilled + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) + }) }) - context('When the operator was already an operator', () => { - beforeEach(async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + describe('setApprovalForAll() function', () => { + it('should emit an ApprovalForAll event', async () => { + const tx = await erc1155Contract.setApprovalForAll(operatorAddress, true) + const receipt = await tx.wait(1) + + expect(receipt.events![0].event).to.be.eql('ApprovalForAll') }) - it('should leave the operator status to set to true again', async () => { + it('should set the operator status to _status argument', async () => { const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) await expect(tx).to.be.fulfilled @@ -564,26 +561,40 @@ describe('ERC1155', () => { expect(status).to.be.eql(true) }) - it('should allow the operator status to be set to false', async () => { - const tx = erc1155Contract.setApprovalForAll(operatorAddress, false) - await expect(tx).to.be.fulfilled + context('When the operator was already an operator', () => { + beforeEach(async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) + }) - const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) - expect(status).to.be.eql(false) + it('should leave the operator status to set to true again', async () => { + const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) + + it('should allow the operator status to be set to false', async () => { + const tx = erc1155Contract.setApprovalForAll(operatorAddress, false) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) + expect(status).to.be.eql(false) + }) }) }) - }) - describe('Supports ERC165', () => { - describe('supportsInterface()', () => { - it('should return true for 0x01ffc9a7 (IERC165)', async () => { - const support = await erc1155Contract.supportsInterface('0x01ffc9a7') - expect(support).to.be.eql(true) - }) + describe('Supports ERC165', () => { + describe('supportsInterface()', () => { + it('should return true for 0x01ffc9a7 (IERC165)', async () => { + const support = await erc1155Contract.supportsInterface('0x01ffc9a7') + expect(support).to.be.eql(true) + }) - it('should return true for 0xd9b67a26 (IERC1155)', async () => { - const support = await erc1155Contract.supportsInterface('0xd9b67a26') - expect(support).to.be.eql(true) + it('should return true for 0xd9b67a26 (IERC1155)', async () => { + const support = await erc1155Contract.supportsInterface('0xd9b67a26') + expect(support).to.be.eql(true) + }) }) }) }) diff --git a/tests/ERC1155Metadata.spec.ts b/tests/ERC1155Metadata.spec.ts index f81c2aa..2310b50 100644 --- a/tests/ERC1155Metadata.spec.ts +++ b/tests/ERC1155Metadata.spec.ts @@ -1,128 +1,175 @@ import { ethers } from 'ethers' -import { AbstractContract, assert, expect, RevertError, BigNumber, HIGH_GAS_LIMIT } from './utils' +import { AbstractContract, expect, RevertError, HIGH_GAS_LIMIT } from './utils' import * as utils from './utils' -import { ERC1155MetadataMock } from 'src' +import { ERC1155MetadataMock, ERC1155MetadataUpgradeableMockV2, ProxyUpgradeableDeployerMock, ProxyUpgradeable } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = utils.createTestWallet(web3, 0) +const { wallet: ownerWallet } = utils.createTestWallet(web3, 0) -const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = utils.createTestWallet(web3, 2) +const usingUpgradeable = [false, true] -const { wallet: anyoneWallet, provider: anyoneProvider, signer: anyoneSigner } = utils.createTestWallet(web3, 3) +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155Metadata' + (upgradeable ? 'Upgradeable': ''), () => { -const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = utils.createTestWallet(web3, 4) + let ownerAddress: string -describe('ERC1155Metadata', () => { - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + let erc1155MetadataContract: ERC1155MetadataMock - let ownerAddress: string - let receiverAddress: string - let anyoneAddress: string - let operatorAddress: string + context('When ERC1155MetadataMock contract is deployed', () => { + const BASE_URI = 'https://assets.skyweaver.net/c679a6577c12c47948084dd61a79b9598db17cc5/full-cards/' + const CONTRACT_NAME = 'MyERC1155' + + let abstract: AbstractContract + let factoryContract: ProxyUpgradeableDeployerMock - let erc1155MetadataContract: ERC1155MetadataMock - let anyoneERC1155MetadataContract + before(async () => { + ownerAddress = await ownerWallet.getAddress() - context('When ERC1155MetadataMock contract is deployed', () => { - const BASE_URI = 'https://assets.skyweaver.net/c679a6577c12c47948084dd61a79b9598db17cc5/full-cards/' - const CONTRACT_NAME = 'MyERC1155' - - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - anyoneAddress = await anyoneWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - }) - - beforeEach(async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155MetadataMock') - erc1155MetadataContract = (await abstract.deploy(ownerWallet, [BASE_URI, CONTRACT_NAME])) as ERC1155MetadataMock - anyoneERC1155MetadataContract = (await erc1155MetadataContract.connect(anyoneSigner)) as ERC1155MetadataMock - - await erc1155MetadataContract.setBaseMetadataURI(BASE_URI) - }) + if (upgradeable) { + abstract = await AbstractContract.fromArtifactName('ERC1155MetadataUpgradeableMock') + } else { + abstract = await AbstractContract.fromArtifactName('ERC1155MetadataMock') + } + }) - describe('Getter functions', () => { - it('supportsInterface(0x0e89341c) on receiver should return true', async () => { - const returnedValue = await erc1155MetadataContract.supportsInterface('0x0e89341c') - await expect(returnedValue).to.be.equal(true) + beforeEach(async () => { + if (upgradeable) { + erc1155MetadataContract = (await abstract.deploy(ownerWallet, [])) as ERC1155MetadataMock + + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + + // Create proxy + let tx = factoryContract.createProxy(erc1155MetadataContract.address, ethers.constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(erc1155MetadataContract.address, ethers.constants.HashZero, ownerWallet.address); + erc1155MetadataContract = (await abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetadataMock; + tx = erc1155MetadataContract.initialize(BASE_URI, CONTRACT_NAME) + await expect(tx).to.be.fulfilled + } else { + erc1155MetadataContract = (await abstract.deploy(ownerWallet, [BASE_URI, CONTRACT_NAME])) as ERC1155MetadataMock + } + await erc1155MetadataContract.setBaseMetadataURI(BASE_URI) }) - }) - describe('_updateBaseMetadataURL() function', () => { - it('should ALLOW inheriting contract to call _updateBaseMetadataURL()', async () => { - const tx = erc1155MetadataContract.setBaseMetadataURI('HELLOTEST/') - await expect(tx).to.be.fulfilled + describe('Getter functions', () => { + it('supportsInterface(0x0e89341c) on receiver should return true', async () => { + const returnedValue = await erc1155MetadataContract.supportsInterface('0x0e89341c') + await expect(returnedValue).to.be.equal(true) + }) }) - it('should update baseMetadataURI when successful', async () => { - const URI1 = await erc1155MetadataContract.uri(1928374) - await erc1155MetadataContract.setBaseMetadataURI('HELLOTEST/') - const URI2 = await erc1155MetadataContract.uri(1928374) - expect(URI1).to.be.equal(BASE_URI + '1928374.json') - expect(URI2).to.be.equal('HELLOTEST/1928374.json') + describe('_updateBaseMetadataURL() function', () => { + it('should ALLOW inheriting contract to call _updateBaseMetadataURL()', async () => { + const tx = erc1155MetadataContract.setBaseMetadataURI('HELLOTEST/') + await expect(tx).to.be.fulfilled + }) + + it('should update baseMetadataURI when successful', async () => { + const URI1 = await erc1155MetadataContract.uri(1928374) + await erc1155MetadataContract.setBaseMetadataURI('HELLOTEST/') + const URI2 = await erc1155MetadataContract.uri(1928374) + expect(URI1).to.be.equal(BASE_URI + '1928374.json') + expect(URI2).to.be.equal('HELLOTEST/1928374.json') + }) + + it('Should revert if called directly by non-parent contract', async () => { + const transaction = { + to: erc1155MetadataContract.address, + data: + '0x122f94bf00000000000000000000000000000000000000000000000000000000000000' + + '20000000000000000000000000000000000000000000000000000000000000000a48454c' + + '4c4f544553542f00000000000000000000000000000000000000000000' + } + const tx = ownerWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetadataMock: INVALID_METHOD')) + }) }) - it('Should revert if called directly by non-parent contract', async () => { - const transaction = { - to: erc1155MetadataContract.address, - data: - '0x122f94bf00000000000000000000000000000000000000000000000000000000000000' + - '20000000000000000000000000000000000000000000000000000000000000000a48454c' + - '4c4f544553542f00000000000000000000000000000000000000000000' - } - const tx = ownerWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetadataMock: INVALID_METHOD')) + describe('_logURIs(uint256[]) function', () => { + const ids = [1, 44, 19283091823] + + it('should ALLOW inheriting contract to call _logURIs()', async () => { + const tx = erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT) + await expect(tx).to.be.fulfilled + }) + + it('Should revert if called directly by non-parent contract', async () => { + const transaction = { + to: erc1155MetadataContract.address, + data: + '0x78d76ac2000000000000000000000000000000000000000000000000000000000000002' + + '0000000000000000000000000000000000000000000000000000000000000000300000000000000' + + '0000000000000000000000000000000000000000000000000100000000000000000000000000000' + + '0000000000000000000000000000000002c00000000000000000000000000000000000000000000' + + '0000000000047d5ca16f' + } + const tx = ownerWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetadataMock: INVALID_METHOD')) + }) + + it('should emit N URI events', async () => { + const tx = (await erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT)) as ethers.ContractTransaction + const receipt = await tx.wait(1) + const URIevents = receipt.events!.filter(uri => uri.event === 'URI') + expect(URIevents.length == ids.length) + }) + + it('should emit URI events with correct information', async () => { + const tx = (await erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT)) as ethers.ContractTransaction + const receipt = await tx.wait(1) + receipt + .events!.filter(uri => uri.event === 'URI') + .forEach(ev => { + const args = erc1155MetadataContract.interface.decodeEventLog( + erc1155MetadataContract.interface.events['URI(string,uint256)'], + ev.data, + ev.topics + ) + expect(args._uri).to.be.equal(BASE_URI + args._id + '.json') + }) + }) }) - }) - describe('_logURIs(uint256[]) function', () => { - const ids = [1, 44, 19283091823] + if (upgradeable) { + describe('Upgradeability', () => { - it('should ALLOW inheriting contract to call _logURIs()', async () => { - const tx = erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT) - await expect(tx).to.be.fulfilled - }) + beforeEach(async () => { + // Deploy v2 implementation + abstract = await AbstractContract.fromArtifactName('ERC1155MetadataUpgradeableMockV2') + const v2Implementation = await abstract.deploy(ownerWallet, []) - it('Should revert if called directly by non-parent contract', async () => { - const transaction = { - to: erc1155MetadataContract.address, - data: - '0x78d76ac2000000000000000000000000000000000000000000000000000000000000002' + - '0000000000000000000000000000000000000000000000000000000000000000300000000000000' + - '0000000000000000000000000000000000000000000000000100000000000000000000000000000' + - '0000000000000000000000000000000002c00000000000000000000000000000000000000000000' + - '0000000000047d5ca16f' - } - const tx = ownerWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetadataMock: INVALID_METHOD')) - }) + // Upgrade proxy + const proxyAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeable') + const proxyContract = (await proxyAbstract.connect(ownerWallet, erc1155MetadataContract.address)) as ERC1155MetadataMock + const tx = proxyContract.upgradeTo(v2Implementation.address) + await expect(tx).to.be.fulfilled + }) - it('should emit N URI events', async () => { - const tx = (await erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT)) as ethers.ContractTransaction - const receipt = await tx.wait(1) - const URIevents = receipt.events!.filter(uri => uri.event === 'URI') - expect(receipt.events!.length == ids.length) - }) + it('new functionality is present', async () => { + let uri = await erc1155MetadataContract.uri(0) + expect(uri).not.to.contain('.json') // v2 removes json extension + + // Use new function + const v2Abstract = await AbstractContract.fromArtifactName('ERC1155MetadataUpgradeableMockV2') + const upgradedContract = (await v2Abstract.connect(ownerWallet, erc1155MetadataContract.address)) as ERC1155MetadataUpgradeableMockV2 + let tx = upgradedContract.setIdMapping(0, 1) + await expect(tx).to.be.fulfilled - it('should emit URI events with correct information', async () => { - const tx = (await erc1155MetadataContract.logURIsMock(ids, HIGH_GAS_LIMIT)) as ethers.ContractTransaction - const receipt = await tx.wait(1) - receipt - .events!.filter(uri => uri.event === 'URI') - .forEach(ev => { - const args = erc1155MetadataContract.interface.decodeEventLog( - erc1155MetadataContract.interface.events['URI(string,uint256)'], - ev.data, - ev.topics - ) - expect(args._uri).to.be.equal(BASE_URI + args._id + '.json') + // Use old function + tx = erc1155MetadataContract.setBaseMetadataURI('HELLOTEST/') + await expect(tx).to.be.fulfilled + + uri = await erc1155MetadataContract.uri(0) + expect(uri).to.be.equal('HELLOTEST/1') // New functionality }) - }) + }) + } }) }) }) From be27bd7ef2b73cad52e76f329c67ee4da7b85b5a Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 29 May 2023 10:58:58 +1200 Subject: [PATCH 2/4] Use storage slot for upgradeable contracts --- .../mocks/ERC1155MetadataUpgradeableMock.sol | 2 +- src/contracts/tokens/ERC1155/ERC1155Meta.sol | 7 +- .../ERC1155MetaPackedBalanceUpgradeable.sol | 36 +- ...RC1155MintBurnPackedBalanceUpgradeable.sol | 8 +- .../ERC1155PackedBalanceUpgradeable.sol | 53 +- .../ERC1155MetaUpgradeable.sol | 41 +- .../ERC1155MetadataUpgradeable.sol | 35 +- .../ERC1155MintBurnUpgradeable.sol | 8 +- .../ERC1155Upgradeable/ERC1155Upgradeable.sol | 55 +- src/contracts/utils/ContextUpgradeable.sol | 7 - src/contracts/utils/StorageSlot.sol | 13 + tests/ERC1155.spec.ts | 20 +- tests/ERC1155Meta.spec.ts | 3489 ++++++++-------- tests/ERC1155MetaPackedBalance.spec.ts | 3505 +++++++++-------- tests/ERC1155Metadata.spec.ts | 9 +- tests/ERC1155MintBurn.spec.ts | 722 ++-- tests/ERC1155MintBurnPackedBalance.spec.ts | 914 ++--- tests/ERC1155PackedBalance.spec.ts | 1073 ++--- tests/utils/helpers.ts | 10 +- 19 files changed, 5112 insertions(+), 4895 deletions(-) diff --git a/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol b/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol index e35c1c3..ef79f61 100644 --- a/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol +++ b/src/contracts/mocks/ERC1155MetadataUpgradeableMock.sol @@ -72,6 +72,6 @@ contract ERC1155MetadataUpgradeableMockV2 is ERC1155MetadataUpgradeableMock { } function uri(uint256 _id) public override view returns (string memory) { - return string(abi.encodePacked(baseURI, _uint2str(idMapping[_id]))); // Removes .json extension, swaps ids + return string(abi.encodePacked(baseURI(), _uint2str(idMapping[_id]))); // Removes .json extension, swaps ids } } diff --git a/src/contracts/tokens/ERC1155/ERC1155Meta.sol b/src/contracts/tokens/ERC1155/ERC1155Meta.sol index 4556f67..cea524a 100644 --- a/src/contracts/tokens/ERC1155/ERC1155Meta.sol +++ b/src/contracts/tokens/ERC1155/ERC1155Meta.sol @@ -294,12 +294,13 @@ contract ERC1155Meta is ERC1155, SignatureValidator { // Complete data to pass to signer verifier bytes memory fullData = abi.encodePacked(_encMembers, nonce, signedData); - //Update signature nonce + // Verify if _from is the signer + require(isValidSignature(_signer, hash, fullData, sig), "ERC1155Meta#_signatureValidation: INVALID_SIGNATURE"); + + // Update signature nonce nonces[_signer] = nonce + 1; emit NonceChange(_signer, nonce + 1); - // Verify if _from is the signer - require(isValidSignature(_signer, hash, fullData, sig), "ERC1155Meta#_signatureValidation: INVALID_SIGNATURE"); return signedData; } diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol index f55e13d..17b40a7 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol @@ -6,7 +6,7 @@ import "../../interfaces/IERC20.sol"; import "../../interfaces/IERC1155.sol"; import "../../utils/LibBytes.sol"; import "../../utils/SignatureValidator.sol"; - +import '../../utils/StorageSlot.sol'; /** * @dev ERC-1155 with native metatransaction methods. These additional functions allow users @@ -45,7 +45,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, } // Signature nonce per address - mapping (address => uint256) internal nonces; + bytes32 constant private _NONCES_SLOT_KEY = keccak256("0xsequence.ERC1155MetaPackedBalanceUpgradeable.nonces"); /***********************************| @@ -227,7 +227,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, ); // Update operator status - operators[_owner][_operator] = _approved; + _setOperator(_owner, _operator, _approved); // Emit event emit ApprovalForAll(_owner, _operator, _approved); @@ -284,8 +284,8 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, (sig, signedData) = abi.decode(_sigData, (bytes, bytes)); // Get current nonce and nonce used for signature - uint256 currentNonce = nonces[_signer]; // Lowest valid nonce for signer - uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object + uint256 currentNonce = _getNonce(_signer); // Lowest valid nonce for signer + uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object // Verify if nonce is valid require( @@ -300,8 +300,9 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, bytes memory fullData = abi.encodePacked(_encMembers, nonce, signedData); //Update signature nonce - nonces[_signer] = nonce + 1; - emit NonceChange(_signer, nonce + 1); + nonce++; + _setNonce(_signer, nonce); + emit NonceChange(_signer, nonce); // Verify if _from is the signer require(isValidSignature(_signer, hash, fullData, sig), "ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE"); @@ -315,7 +316,26 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, function getNonce(address _signer) public view returns (uint256 nonce) { - return nonces[_signer]; + return _getNonce(_signer); + } + + /** + * @notice Returns the current nonce associated with a given address + * @param _signer Address to query signature nonce for + */ + function _getNonce(address _signer) + internal view returns (uint256 nonce) + { + return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value; + } + + /** + * @notice Sets the nonce associated with a given address + * @param _signer Address to set signature nonce for + * @param _nonce Nonce value to set + */ + function _setNonce(address _signer, uint256 _nonce) internal { + StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value = _nonce; } diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol index de041f4..12769c6 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol @@ -51,7 +51,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); // Balance for current bin in memory (initialized with first transfer) - uint256 balTo = _viewUpdateBinValue(balances[_to][bin], index, _amounts[0], Operations.Add); + uint256 balTo = _viewUpdateBinValue(_getBalance(_to, bin), index, _amounts[0], Operations.Add); // Number of transfer to execute uint256 nTransfer = _ids.length; @@ -65,8 +65,8 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea // If new bin if (bin != lastBin) { // Update storage balance of previous bin - balances[_to][lastBin] = balTo; - balTo = balances[_to][bin]; + _setBalance(_to, lastBin, balTo); + balTo = _getBalance(_to, bin); // Bin will be the most recent bin lastBin = bin; @@ -77,7 +77,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea } // Update storage of the last bin visited - balances[_to][bin] = balTo; + _setBalance(_to, bin, balTo); } // //Emit event diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol index 19e83b1..04f78e7 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol @@ -6,6 +6,7 @@ import "../../interfaces/IERC1155.sol"; import "../../utils/Address.sol"; import "../../utils/ContextUpgradeable.sol"; import "../../utils/ERC165.sol"; +import '../../utils/StorageSlot.sol'; /** * @dev Implementation of Multi-Token Standard contract. This implementation of the ERC-1155 standard @@ -35,10 +36,10 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 enum Operations { Add, Sub } // Token IDs balances ; balances[address][id] => balance (using array instead of mapping for efficiency) - mapping (address => mapping(uint256 => uint256)) internal balances; + bytes32 constant private _BALANCES_SLOT_KEY = keccak256("0xsequence.ERC1155PackedBalanceUpgradeable.balances"); // Operators - mapping (address => mapping(address => bool)) internal operators; + bytes32 constant private _OPERATORS_SLOT_KEY = keccak256("0xsequence.ERC1155PackedBalanceUpgradeable.operators"); /***********************************| @@ -140,8 +141,8 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); // Balance for current bin in memory (initialized with first transfer) - uint256 balFrom = _viewUpdateBinValue(balances[_from][bin], index, _amounts[0], Operations.Sub); - uint256 balTo = _viewUpdateBinValue(balances[_to][bin], index, _amounts[0], Operations.Add); + uint256 balFrom = _viewUpdateBinValue(_getBalance(_from, bin), index, _amounts[0], Operations.Sub); + uint256 balTo = _viewUpdateBinValue(_getBalance(_to, bin), index, _amounts[0], Operations.Add); // Last bin updated uint256 lastBin = bin; @@ -152,11 +153,11 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 // If new bin if (bin != lastBin) { // Update storage balance of previous bin - balances[_from][lastBin] = balFrom; - balances[_to][lastBin] = balTo; + _setBalance(_from, lastBin, balFrom); + _setBalance(_to, lastBin, balTo); - balFrom = balances[_from][bin]; - balTo = balances[_to][bin]; + balFrom = _getBalance(_from, bin); + balTo = _getBalance(_to, bin); // Bin will be the most recent bin lastBin = bin; @@ -168,8 +169,8 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 } // Update storage of the last bin visited - balances[_from][bin] = balFrom; - balances[_to][bin] = balTo; + _setBalance(_from, bin, balFrom); + _setBalance(_to, bin, balTo); // If transfer to self, just make sure all amounts are valid } else { @@ -209,7 +210,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 external override { // Update operator status - operators[_msgSender()][_operator] = _approved; + _setOperator(_msgSender(), _operator, _approved); emit ApprovalForAll(_msgSender(), _operator, _approved); } @@ -222,7 +223,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 function isApprovedForAll(address _owner, address _operator) public override view returns (bool isOperator) { - return operators[_owner][_operator]; + return _getOperator(_owner, _operator); } @@ -244,7 +245,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 //Get bin and index of _id (bin, index) = getIDBinIndex(_id); - return getValueInBin(balances[_owner][bin], index); + return getValueInBin(_getBalance(_owner, bin), index); } /** @@ -261,7 +262,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 // First values (uint256 bin, uint256 index) = getIDBinIndex(_ids[0]); - uint256 balance_bin = balances[_owners[0]][bin]; + uint256 balance_bin = _getBalance(_owners[0], bin); uint256 last_bin = bin; // Initialization @@ -274,7 +275,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 // SLOAD if bin changed for the same owner or if owner changed if (bin != last_bin || _owners[i-1] != _owners[i]) { - balance_bin = balances[_owners[i]][bin]; + balance_bin = _getBalance(_owners[i], bin); last_bin = bin; } @@ -308,7 +309,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 (bin, index) = getIDBinIndex(_id); // Update balance - balances[_address][bin] = _viewUpdateBinValue(balances[_address][bin], index, _amount, _operation); + _setBalance(_address, bin, _viewUpdateBinValue(_getBalance(_address, bin), index, _amount, _operation)); } /** @@ -379,6 +380,26 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 return (_binValues >> rightShift) & mask; } + /***********************************| + | Storage Functions | + |__________________________________*/ + + function _getBalance(address _owner, uint256 _id) internal view returns (uint256) { + return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value; + } + + function _setBalance(address _owner, uint256 _id, uint256 _balance) internal { + StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value = _balance; + } + + function _getOperator(address _owner, address _operator) internal view returns (bool) { + return StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value; + } + + function _setOperator(address _owner, address _operator, bool _approved) internal { + StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value = _approved; + } + /***********************************| | ERC165 Functions | diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol index b4ff694..8344a63 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol @@ -6,6 +6,7 @@ import "../../interfaces/IERC20.sol"; import "../../interfaces/IERC1155.sol"; import "../../utils/LibBytes.sol"; import "../../utils/SignatureValidator.sol"; +import '../../utils/StorageSlot.sol'; /** * @dev ERC-1155 with native metatransaction methods. These additional functions allow users @@ -41,8 +42,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { } // Signature nonce per address - mapping (address => uint256) internal nonces; - + bytes32 constant private _NONCES_SLOT_KEY = keccak256("0xsequence.ERC1155MetaUpgradeable.nonces"); /***********************************| | Events | @@ -223,7 +223,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { ); // Update operator status - operators[_owner][_operator] = _approved; + _setOperator(_owner, _operator, _approved); // Emit event emit ApprovalForAll(_owner, _operator, _approved); @@ -279,8 +279,8 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { (sig, signedData) = abi.decode(_sigData, (bytes, bytes)); // Get current nonce and nonce used for signature - uint256 currentNonce = nonces[_signer]; // Lowest valid nonce for signer - uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object + uint256 currentNonce = _getNonce(_signer); // Lowest valid nonce for signer + uint256 nonce = uint256(sig.readBytes32(65)); // Nonce passed in the signature object // Verify if nonce is valid require( @@ -294,12 +294,14 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { // Complete data to pass to signer verifier bytes memory fullData = abi.encodePacked(_encMembers, nonce, signedData); - //Update signature nonce - nonces[_signer] = nonce + 1; - emit NonceChange(_signer, nonce + 1); - // Verify if _from is the signer require(isValidSignature(_signer, hash, fullData, sig), "ERC1155Meta#_signatureValidation: INVALID_SIGNATURE"); + + // Update signature nonce + nonce++; + _setNonce(_signer, nonce); + emit NonceChange(_signer, nonce); + return signedData; } @@ -310,7 +312,26 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { function getNonce(address _signer) public view returns (uint256 nonce) { - return nonces[_signer]; + return _getNonce(_signer); + } + + /** + * @notice Returns the current nonce associated with a given address + * @param _signer Address to query signature nonce for + */ + function _getNonce(address _signer) + internal view returns (uint256 nonce) + { + return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value; + } + + /** + * @notice Sets the nonce associated with a given address + * @param _signer Address to set signature nonce for + * @param _nonce Nonce value to set + */ + function _setNonce(address _signer, uint256 _nonce) internal { + StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value = _nonce; } diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol index 6db96ce..6b7579d 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import '../../interfaces/IERC1155Metadata.sol'; import '../../utils/ERC165.sol'; import '../../utils/Initializable.sol'; +import '../../utils/StorageSlot.sol'; /** * @notice Contract that handles metadata related methods. @@ -12,8 +13,8 @@ import '../../utils/Initializable.sol'; */ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { // URI's default URI prefix - string public baseURI; - string public name; + bytes32 private constant _BASEURI_SLOT = keccak256("0xsequence.ERC1155MetadataUpgradeable.baseURI"); + bytes32 private constant _NAME_SLOT = keccak256("0xsequence.ERC1155MetadataUpgradeable.name"); constructor() initializer {} @@ -22,14 +23,22 @@ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { * @dev This function should be called once immediately after deployment. */ function initialize(string memory _name, string memory _baseURI) public virtual initializer { - name = _name; - baseURI = _baseURI; + _setContractName(_name); + _setBaseMetadataURI(_baseURI); } /***********************************| - | Metadata Public Functions | + | Public Functions | |__________________________________*/ + function name() public view virtual returns (string memory) { + return StorageSlot.getStringSlot(_NAME_SLOT).value; + } + + function baseURI() public view virtual returns (string memory) { + return StorageSlot.getStringSlot(_BASEURI_SLOT).value; + } + /** * @notice A distinct Uniform Resource Identifier (URI) for a given token. * @dev URIs are defined in RFC 3986. @@ -37,7 +46,7 @@ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { * @return URI string */ function uri(uint256 _id) public virtual override view returns (string memory) { - return string(abi.encodePacked(baseURI, _uint2str(_id), ".json")); + return string(abi.encodePacked(baseURI(), _uint2str(_id), ".json")); } @@ -50,7 +59,7 @@ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { * @param _tokenIDs Array of IDs of tokens to log default URI */ function _logURIs(uint256[] memory _tokenIDs) internal { - string memory baseURL = baseURI; + string memory baseURL = baseURI(); string memory tokenURI; for (uint256 i = 0; i < _tokenIDs.length; i++) { @@ -61,18 +70,18 @@ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { /** * @notice Will update the base URL of token's URI - * @param _newBaseMetadataURI New base URL of token's URI + * @param _baseURI New base URL of token's URI */ - function _setBaseMetadataURI(string memory _newBaseMetadataURI) internal { - baseURI = _newBaseMetadataURI; + function _setBaseMetadataURI(string memory _baseURI) internal { + StorageSlot.getStringSlot(_BASEURI_SLOT).value = _baseURI; } /** * @notice Will update the name of the contract - * @param _newName New contract name + * @param _name New contract name */ - function _setContractName(string memory _newName) internal { - name = _newName; + function _setContractName(string memory _name) internal { + StorageSlot.getStringSlot(_NAME_SLOT).value = _name; } /** diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol index 33d8352..8d146b3 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol @@ -25,7 +25,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { internal { // Add _amount - balances[_to][_id] += _amount; + _updateBalance(_to, _id, _amount, Operations.Add); // Emit event emit TransferSingle(_msgSender(), address(0x0), _to, _id, _amount); @@ -52,7 +52,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { // Executing all minting for (uint256 i = 0; i < nMint; i++) { // Update storage balance - balances[_to][_ids[i]] += _amounts[i]; + _updateBalance(_to, _ids[i], _amounts[i], Operations.Add); } // Emit batch mint event @@ -77,7 +77,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { internal { //Substract _amount - balances[_from][_id] -= _amount; + _updateBalance(_from, _id, _amount, Operations.Sub); // Emit event emit TransferSingle(_msgSender(), _from, address(0x0), _id, _amount); @@ -99,7 +99,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { // Executing all minting for (uint256 i = 0; i < nBurn; i++) { // Update storage balance - balances[_from][_ids[i]] -= _amounts[i]; + _updateBalance(_from, _ids[i], _amounts[i], Operations.Sub); } // Emit batch mint event diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol index d720299..799b4d1 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol @@ -6,6 +6,7 @@ import "../../interfaces/IERC1155.sol"; import "../../utils/Address.sol"; import "../../utils/ContextUpgradeable.sol"; import "../../utils/ERC165.sol"; +import '../../utils/StorageSlot.sol'; /** * @dev Implementation of Multi-Token Standard contract. @@ -22,11 +23,14 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { bytes4 constant internal ERC1155_RECEIVED_VALUE = 0xf23a6e61; bytes4 constant internal ERC1155_BATCH_RECEIVED_VALUE = 0xbc197c81; + // Math operations + enum Operations { Add, Sub } + // Objects balances - mapping (address => mapping(uint256 => uint256)) internal balances; + bytes32 constant private _BALANCES_SLOT_KEY = keccak256("0xsequence.ERC1155Upgradeable.balances"); // Operator Functions - mapping (address => mapping(address => bool)) internal operators; + bytes32 constant private _OPERATORS_SLOT_KEY = keccak256("0xsequence.ERC1155Upgradeable.operators"); /***********************************| @@ -86,8 +90,8 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { internal { // Update balances - balances[_from][_id] -= _amount; - balances[_to][_id] += _amount; + _updateBalance(_from, _id, _amount, Operations.Sub); + _updateBalance(_to, _id, _amount, Operations.Add); // Emit event emit TransferSingle(_msgSender(), _from, _to, _id, _amount); @@ -124,8 +128,8 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { // Executing all transfers for (uint256 i = 0; i < nTransfer; i++) { // Update storage balance of previous bin - balances[_from][_ids[i]] -= _amounts[i]; - balances[_to][_ids[i]] += _amounts[i]; + _updateBalance(_from, _ids[i], _amounts[i], Operations.Sub); + _updateBalance(_to, _ids[i], _amounts[i], Operations.Add); } // Emit event @@ -159,7 +163,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { external override { // Update operator status - operators[_msgSender()][_operator] = _approved; + _setOperator(_msgSender(), _operator, _approved); emit ApprovalForAll(_msgSender(), _operator, _approved); } @@ -172,7 +176,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { function isApprovedForAll(address _owner, address _operator) public override view returns (bool isOperator) { - return operators[_owner][_operator]; + return _getOperator(_owner, _operator); } @@ -189,7 +193,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { function balanceOf(address _owner, uint256 _id) public override view returns (uint256) { - return balances[_owner][_id]; + return _getBalance(_owner, _id); } /** @@ -208,12 +212,43 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { // Iterate over each owner and token ID for (uint256 i = 0; i < _owners.length; i++) { - batchBalances[i] = balances[_owners[i]][_ids[i]]; + batchBalances[i] = _getBalance(_owners[i], _ids[i]); } return batchBalances; } + /***********************************| + | Storage Functions | + |__________________________________*/ + + function _getBalance(address _owner, uint256 _id) internal view returns (uint256) { + return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value; + } + + function _setBalance(address _owner, uint256 _id, uint256 _balance) internal { + StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value = _balance; + } + + function _updateBalance(address _owner, uint256 _id, uint256 _diff, Operations _operation) internal { + StorageSlot.Uint256Slot storage slot = StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))); + if (_operation == Operations.Add) { + slot.value += _diff; + } else if (_operation == Operations.Sub) { + slot.value -= _diff; + } else { + revert("ERC1155Upgradeable#_updateBalance: INVALID_OPERATION"); + } + } + + function _getOperator(address _owner, address _operator) internal view returns (bool) { + return StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value; + } + + function _setOperator(address _owner, address _operator, bool _approved) internal { + StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value = _approved; + } + /***********************************| | ERC165 Functions | diff --git a/src/contracts/utils/ContextUpgradeable.sol b/src/contracts/utils/ContextUpgradeable.sol index ac21633..1afc9d8 100644 --- a/src/contracts/utils/ContextUpgradeable.sol +++ b/src/contracts/utils/ContextUpgradeable.sol @@ -27,11 +27,4 @@ abstract contract ContextUpgradeable is Initializable { function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } - - /** - * @dev This empty reserved space is put in place to allow future versions to add new - * variables without shifting down storage in the inheritance chain. - * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - */ - uint256[50] private __gap; } diff --git a/src/contracts/utils/StorageSlot.sol b/src/contracts/utils/StorageSlot.sol index 9fe9f9b..11a740f 100644 --- a/src/contracts/utils/StorageSlot.sol +++ b/src/contracts/utils/StorageSlot.sol @@ -22,6 +22,10 @@ library StorageSlot { bytes32 value; } + struct StringSlot { + string value; + } + struct Uint256Slot { uint256 value; } @@ -53,6 +57,15 @@ library StorageSlot { } } + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + assembly { + r.slot := slot + } + } + /** * @dev Returns an `Uint256Slot` with member `value` located at `slot`. */ diff --git a/tests/ERC1155.spec.ts b/tests/ERC1155.spec.ts index 32ff784..88ed8e7 100644 --- a/tests/ERC1155.spec.ts +++ b/tests/ERC1155.spec.ts @@ -10,13 +10,13 @@ import { HIGH_GAS_LIMIT } from './utils' -import { ERC1155MetaMintBurnMock, ERC1155ReceiverMock, ERC1155OperatorMock } from 'src' +import { ERC1155MetaMintBurnMock, ERC1155ReceiverMock, ERC1155OperatorMock, ProxyUpgradeableDeployerMock } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = createTestWallet(web3, 0) -const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = createTestWallet(web3, 2) +const { wallet: ownerWallet, provider: ownerProvider } = createTestWallet(web3, 0) +const { wallet: receiverWallet } = createTestWallet(web3, 2) const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = createTestWallet(web3, 4) const usingUpgradeable = [false, true] @@ -39,6 +39,8 @@ usingUpgradeable.forEach(upgradeable => { let erc1155Contract: ERC1155MetaMintBurnMock let operatorERC1155Contract: ERC1155MetaMintBurnMock + let factoryContract: ProxyUpgradeableDeployerMock + // load contract abi and deploy to test server before(async () => { ownerAddress = await ownerWallet.getAddress() @@ -47,6 +49,9 @@ usingUpgradeable.forEach(upgradeable => { if (upgradeable) { erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock } else { erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') } @@ -57,7 +62,14 @@ usingUpgradeable.forEach(upgradeable => { beforeEach(async () => { if (upgradeable) { erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnMock - erc1155Contract.initialize(NAME, METADATA_URI) + + // Create proxy + let tx = factoryContract.createProxy(erc1155Contract.address, ethers.constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(erc1155Contract.address, ethers.constants.HashZero, ownerWallet.address); + erc1155Contract = (await erc1155Abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnMock; + tx = erc1155Contract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled } else { erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock } diff --git a/tests/ERC1155Meta.spec.ts b/tests/ERC1155Meta.spec.ts index 8be759a..52266b7 100644 --- a/tests/ERC1155Meta.spec.ts +++ b/tests/ERC1155Meta.spec.ts @@ -13,19 +13,15 @@ import { HIGH_GAS_LIMIT } from './utils' -import { BigNumber, utils, ContractTransaction, EventFilter } from 'ethers' - -import { ERC1155MetaMintBurnMock, ERC1271WalletValidationMock, ERC1155ReceiverMock, ERC1155OperatorMock, ERC20Mock } from 'src' - +import { BigNumber, utils, ContractTransaction, EventFilter, constants } from 'ethers' +import { ERC1155MetaMintBurnMock, ERC1271WalletValidationMock, ERC1155ReceiverMock, ERC1155OperatorMock, ERC20Mock, ProxyUpgradeableDeployerMock } from 'src' import { GasReceipt, TransferSignature, ApprovalSignature, BatchTransferSignature } from 'src/typings/tx-types' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = createTestWallet(web3, 1) - +const { wallet: ownerWallet, provider: ownerProvider } = createTestWallet(web3, 1) const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = createTestWallet(web3, 2) - const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = createTestWallet(web3, 4) // Lower polling interval for faster tx send @@ -33,651 +29,439 @@ ownerProvider.pollingInterval = 1000 operatorProvider.pollingInterval = 1000 receiverProvider.pollingInterval = 1000 -describe('ERC1155Meta', () => { - const MAXVAL = BigNumber.from(2) - .pow(256) - .sub(1) // 2**256 - 1 - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const DOMAIN_SEPARATOR_TYPEHASH = '0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749' - - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - // Pass gas since ganache can't figure it out - const TX_PARAM = { gasLimit: 2000000 } - - let ownerAddress: string - let receiverAddress: string - let operatorAddress: string - let erc1155Abstract: AbstractContract - let operatorAbstract: AbstractContract - - let erc1155Contract: ERC1155MetaMintBurnMock - let operatorERC1155Contract: ERC1155MetaMintBurnMock - let receiverERC1155Contract: ERC1155MetaMintBurnMock - - // load contract abi and deploy to test server - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - - erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') - operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') - }) - - // deploy before each test, to reset state of contract - beforeEach(async () => { - erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock - operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnMock - receiverERC1155Contract = (await erc1155Contract.connect(receiverSigner)) as ERC1155MetaMintBurnMock - }) - - describe('metaSafeTransferFrom() (Meta) Function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock - - let transferData: string | null = 'Hello from the other side' - const initBalance = 100 - const amount = 10 - const nonce = BigNumber.from(0) - const id = 66 - - const feeTokenID = 666 - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 // ERC-1155 - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - let transferObj: TransferSignature - let domainHash: string - let gasReceipt: GasReceipt | null - let data: string - - const conditions = [ - [transferData, true, 'Gas receipt & transfer data'], - [null, true, 'Gas receipt w/o transfer data'], - [transferData, false, 'Transfer data w/o gas receipt '], - [null, false, 'No Gas receipt & No transfer data'] - ] - - conditions.forEach(function(condition) { - context(condition[2] as string, () => { - beforeEach(async () => { - // Get conditions - transferData = condition[0] as string | null - isGasReceipt = condition[1] as boolean - - // Deploy contracts - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Transfer Signature Object - transferObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - receiver: receiverAddress, - id: id, - amount: amount, - isGasFee: isGasReceipt, - transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), - nonce: nonce - } - - // Mint tokens - await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Domain hash - domainHash = utils.keccak256( - utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - }) - - it('should REVERT if data is random', async () => { - const dataUint8 = utils.toUtf8Bytes('Breakthroughs! over the river! flips and crucifixions! gone down the flood!') - const data = BigNumber.from(dataUint8).toHexString() - - // Check if data length is more than 69 - expect(utils.arrayify(data).length).to.be.at.least(70) - - const tx = erc1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverContract.address, - id, - amount, - isGasReceipt, - data, - { gasLimit: 100_000 } - ) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - - it('should REVERT if contract address is incorrect', async () => { - // Domain hash - domainHash = utils.keccak256( - utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) - ) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if signer address is incorrect', async () => { - transferObj.signerWallet = operatorWallet - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if receiver address is incorrect', async () => { - transferObj.receiver = ownerAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if token id is incorrect', async () => { - transferObj.id = id + 1 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if token amount is incorrect', async () => { - transferObj.amount = amount + 1 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if transfer data is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.id, - transferObj.amount, - transferObj.nonce - ]) - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - let goodGasAndTransferData - let badGasAndTransferData - - // Correct and incorrect transferData - if (isGasReceipt) { - goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - badGasAndTransferData = utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] - ) - } else { - goodGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [transferData]) - badGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) - } - - // Encode normally the whole thing - sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if nonce is incorrect', async () => { - transferObj.nonce = nonce.add(101) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - // Nonce higher - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - transferObj.nonce = nonce - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - }) - - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.fulfilled - }) - - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155Meta' + (upgradeable ? 'Upgradeable': ''), () => { + const MAXVAL = BigNumber.from(2) + .pow(256) + .sub(1) // 2**256 - 1 + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const DOMAIN_SEPARATOR_TYPEHASH = '0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749' + + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let ownerAddress: string + let receiverAddress: string + let operatorAddress: string + let erc1155Abstract: AbstractContract + let operatorAbstract: AbstractContract + + let erc1155Contract: ERC1155MetaMintBurnMock + let operatorERC1155Contract: ERC1155MetaMintBurnMock + let receiverERC1155Contract: ERC1155MetaMintBurnMock + let factoryContract: ProxyUpgradeableDeployerMock + + // load contract abi and deploy to test server + before(async () => { + ownerAddress = await ownerWallet.getAddress() + receiverAddress = await receiverWallet.getAddress() + operatorAddress = await operatorWallet.getAddress() + + if (upgradeable) { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + } else { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') + } + operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') + }) - let erc1271WalletAddress + const deployERC1155Contract = async () => { + let contract: ERC1155MetaMintBurnMock + if (upgradeable) { + contract = (await erc1155Abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnMock + + // Create proxy + let tx = factoryContract.createProxy(contract.address, constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(contract.address, constants.HashZero, ownerWallet.address); + contract = (await erc1155Abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnMock; + tx = contract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled + } else { + contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock + } + return contract + } + + // deploy before each test, to reset state of contract + beforeEach(async () => { + erc1155Contract = await deployERC1155Contract() + operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnMock + receiverERC1155Contract = (await erc1155Contract.connect(receiverSigner)) as ERC1155MetaMintBurnMock + }) + describe('metaSafeTransferFrom() (Meta) Function', () => { + let receiverContract: ERC1155ReceiverMock + let operatorContract: ERC1155OperatorMock + + let transferData: string | null = 'Hello from the other side' + const initBalance = 100 + const amount = 10 + const nonce = BigNumber.from(0) + const id = 66 + + const feeTokenID = 666 + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) + + const feeType = 0 // ERC-1155 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array + + let transferObj: TransferSignature + let domainHash: string + let gasReceipt: GasReceipt | null + let data: string + + const conditions = [ + [transferData, true, 'Gas receipt & transfer data'], + [null, true, 'Gas receipt w/o transfer data'], + [transferData, false, 'Transfer data w/o gas receipt '], + [null, false, 'No Gas receipt & No transfer data'] + ] + + conditions.forEach(function(condition) { + context(condition[2] as string, () => { beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address - - await erc1155Contract.mintMock(erc1271WalletAddress, id, initBalance, []) - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) - }) + // Get conditions + transferData = condition[0] as string | null + isGasReceipt = condition[1] as boolean - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Deploy contracts + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + feeTokenAddress = erc1155Contract.address - it('should REVERT if token ID is not 66', async () => { - const badID = 77 - await erc1155Contract.mintMock(erc1271WalletAddress, badID, initBalance, []) - transferObj.from = erc1271WalletAddress - transferObj.id = badID - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] + ) - it('should REVERT if amount is more than 100', async () => { - await erc1155Contract.mintMock(erc1271WalletAddress, id, 101, []) - transferObj.from = erc1271WalletAddress - transferObj.amount = 101 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + // Gas Receipt + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.fulfilled - }) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Transfer Signature Object + transferObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + receiver: receiverAddress, + id: id, + amount: amount, + isGasFee: isGasReceipt, + transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), + nonce: nonce + } - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Mint tokens + await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.fulfilled - }) - }) - }) + // Domain hash + domainHash = utils.keccak256( + utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) - describe('When signature is valid', () => { - it('should REVERT if insufficient balance', async () => { - transferObj.amount = initBalance + 1 + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - initBalance + 1, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should REVERT if sending to 0x0', async () => { - transferObj.receiver = ZERO_ADDRESS - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + it('should REVERT if data is random', async () => { + const dataUint8 = utils.toUtf8Bytes('Breakthroughs! over the river! flips and crucifixions! gone down the flood!') + const data = BigNumber.from(dataUint8).toHexString() - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, ZERO_ADDRESS, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#metaSafeTransferFrom: INVALID_RECIPIENT')) - }) + // Check if data length is more than 69 + expect(utils.arrayify(data).length).to.be.at.least(70) - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock(receiverAddress, id, MAXVAL, []) - const tx = operatorERC1155Contract.metaSafeTransferFrom( + const tx = erc1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, id, amount, isGasReceipt, data, - HIGH_GAS_LIMIT + { gasLimit: 100_000 } ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + await expect(tx).to.be.rejectedWith(RevertError()) }) - it('should REVERT when sending to non-receiver contract', async () => { - transferObj.receiver = erc1155Contract.address + it('should REVERT if contract address is incorrect', async () => { + // Domain hash + domainHash = utils.keccak256( + utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) + ) data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - erc1155Contract.address, - id, - amount, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if invalid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address + it('should REVERT if signer address is incorrect', async () => { + transferObj.signerWallet = operatorWallet data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Force invalid response - await receiverContract.setShouldReject(true) - - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverContract.address, - id, - amount, - isGasReceipt, - data, - { gasLimit: 100_000 } - ) - await expect(tx).to.be.rejectedWith(/ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE|out of gas/) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if valid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '02') - - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverContract.address, - id, - amount, - isGasReceipt, - data - ) + it('should REVERT if receiver address is incorrect', async () => { + transferObj.receiver = ownerAddress + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - //await expect(tx).to.be.fulfilled - await expect(tx).to.be.fulfilled + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) - - it('should send gas fee to msg.sender is fee recipient ix 0x0', async () => { - gasReceipt!.feeRecipient = ZERO_ADDRESS + it('should REVERT if token id is incorrect', async () => { + transferObj.id = id + 1 + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + it('should REVERT if token amount is incorrect', async () => { + transferObj.amount = amount + 1 + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const receiverBalance = await erc1155Contract.balanceOf(receiverAddress, feeTokenID) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) - }) + it('should REVERT if transfer data is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] + + const signer = await transferObj.signerWallet.getAddress() + + // Packed encoding of transfer signature message + let sigData = utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.id, + transferObj.amount, + transferObj.nonce + ]) + + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + let goodGasAndTransferData + let badGasAndTransferData + + // Correct and incorrect transferData + if (isGasReceipt) { + goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + badGasAndTransferData = utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] + ) + } else { + goodGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [transferData]) + badGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) + } - it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { - await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - const operatorBalance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) + // Encode normally the whole thing + sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) - }) + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - it('should REVERT if gasReceipt is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] + // PASS BAD DATA + data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - const signer = await transferObj.signerWallet.getAddress() + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - // Packed encoding of transfer signature message - let sigData = utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.id, - transferObj.amount, - transferObj.nonce - ]) + it('should REVERT if nonce is incorrect', async () => { + transferObj.nonce = nonce.add(101) + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Form bad gas receipt - const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } + // Nonce higher + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + // Correct nonce + transferObj.nonce = nonce + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - // Correct and incorrect transferData - const goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - const badGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [badGasReceipt, transferData]) + // Nonce lower + const tx2 = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) + }) - // Encode normally the whole thing - sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.fulfilled + }) - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract - // PASS BAD DATA - data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) + let erc1271WalletAddress - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.mintMock(erc1271WalletAddress, id, initBalance, []) + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) }) - it('should PASS if another approved ERC-1155 is used for fee', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) - - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + it('should REVERT if token ID is not 66', async () => { + const badID = 77 + await erc1155Contract.mintMock(erc1271WalletAddress, badID, initBalance, []) + transferObj.from = erc1271WalletAddress + transferObj.id = badID + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + it('should REVERT if amount is more than 100', async () => { + await erc1155Contract.mintMock(erc1271WalletAddress, id, 101, []) + transferObj.from = erc1271WalletAddress + transferObj.amount = 101 + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.fulfilled + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) }) - it('should REVERT if NOT approved ERC-1155 is used for fee', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) + }) + }) - // Data to pass in transfer method + describe('When signature is valid', () => { + it('should REVERT if insufficient balance', async () => { + transferObj.amount = initBalance + 1 data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, receiverAddress, id, - amount, + initBalance + 1, isGasReceipt, data, - { gasLimit: 2000000 } + HIGH_GAS_LIMIT ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_OPERATOR')) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should REVERT if another ERC-1155 is used for fee without sufficient balance', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, 100, []) - await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) - - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method + it('should REVERT if sending to 0x0', async () => { + transferObj.receiver = ZERO_ADDRESS data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, ZERO_ADDRESS, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#metaSafeTransferFrom: INVALID_RECIPIENT')) + }) + + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock(receiverAddress, id, MAXVAL, []) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, receiverAddress, @@ -690,231 +474,259 @@ describe('ERC1155Meta', () => { await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should PASS if approved ERC20 is used for fee', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - - const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method + it('should REVERT when sending to non-receiver contract', async () => { + transferObj.receiver = erc1155Contract.address data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + erc1155Contract.address, id, amount, isGasReceipt, - data, - { gasLimit: 2000000 } + data ) - await expect(tx).to.be.fulfilled + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) }) - it('should REVERT if NOT approved ERC20 is used for fee', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - - const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method + it.skip('should REVERT if invalid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Force invalid response + await receiverContract.setShouldReject(true) + const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, id, amount, isGasReceipt, data, - HIGH_GAS_LIMIT + { gasLimit: 100_000 } ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + //FIXME Wrong error returned + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) }) - it('should REVERT if approved ERC20 balance is insufficient', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, 100) - await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - - const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + it('should PASS if valid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '02') const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, id, amount, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + + await expect(tx).to.be.fulfilled }) - it('should REVERT if FeeTokenType is not supported', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) - let feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 2]) + it('should send gas fee to msg.sender is fee recipient ix 0x0', async () => { + gasReceipt!.feeRecipient = ZERO_ADDRESS - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const receiverBalance = await erc1155Contract.balanceOf(receiverAddress, feeTokenID) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_transferGasFee: UNSUPPORTED_TOKEN')) + expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + }) - feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 3]) + it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { + await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + const operatorBalance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) + }) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + it('should REVERT if gasReceipt is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const signer = await transferObj.signerWallet.getAddress() - const tx2 = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - { gasLimit: 2000000 } - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_transferGasFee: UNSUPPORTED_TOKEN')) - }) + // Packed encoding of transfer signature message + let sigData = utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.id, + transferObj.amount, + transferObj.nonce + ]) - it('should REVERT if gas receipt is passed, but not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + // Form bad gas receipt + const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - transferObj.isGasFee = false - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - transferObj.isGasFee = false - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.fulfilled - }) + // Correct and incorrect transferData + const goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + const badGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [badGasReceipt, transferData]) + + // Encode normally the whole thing + sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce + + // PASS BAD DATA + data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - describe('When receiver is a contract', () => { - it('should REVERT if gas used in onERC1155Received exceeds limit', async () => { - const lowGasLimit = 1000 - gasReceipt!.gasLimitCallback = lowGasLimit - transferObj.receiver = receiverContract.address + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + it('should PASS if another approved ERC-1155 is used for fee', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) + + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, data, { gasLimit: 2000000 } ) - await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + await expect(tx).to.be.fulfilled }) - it('should PASS if gas used in onERC1155Received does not exceed limit', async () => { - const okGasLimit = 9700 - gasReceipt!.gasLimitCallback = okGasLimit - transferObj.receiver = receiverContract.address + it('should REVERT if NOT approved ERC-1155 is used for fee', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, data, { gasLimit: 2000000 } ) - await expect(tx).to.be.fulfilled + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#safeTransferFrom: INVALID_OPERATOR')) + }) + + it('should REVERT if another ERC-1155 is used for fee without sufficient balance', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, 100, []) + await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) + + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should PASS if gasLimitCallback is higher than gas sent in transaction', async () => { - const highGasLimit = 30000000 - gasReceipt!.gasLimitCallback = highGasLimit - transferObj.receiver = receiverContract.address + it('should PASS if approved ERC20 is used for fee', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) + await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) + + const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, @@ -923,508 +735,480 @@ describe('ERC1155Meta', () => { ) await expect(tx).to.be.fulfilled }) - }) - }) - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if NOT approved ERC20 is used for fee', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.fulfilled - }) + const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - transferObj.isGasFee = true - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - context('When successful transfer', () => { - let tx: ContractTransaction + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - beforeEach(async () => { - tx = await operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data - ) - }) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should correctly update balance of sender', async () => { - const balance = await erc1155Contract.balanceOf(ownerAddress, id) - expect(balance).to.be.eql(BigNumber.from(initBalance - amount)) - }) + it('should REVERT if approved ERC20 balance is insufficient', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, 100) + await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - it('should correctly update balance of receiver', async () => { - const balance = await erc1155Contract.balanceOf(receiverAddress, id) - expect(balance).to.be.eql(BigNumber.from(amount)) - }) + const feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 } - }) - it('should update gas token balance of sender', async () => { - const senderBalance = await erc1155Contract.balanceOf(ownerAddress, feeTokenID) - expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - it('should update gas token balance of executor', async () => { - const balance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) - expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - }) - describe('TransferSingle event', async () => { - let filterFromOperatorContract: EventFilter + it('should REVERT if FeeTokenType is not supported', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) + await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - it('should emit TransferSingle event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferSingle') - }) + let feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 2]) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferSingle( - operatorContract.address, - null, - null, - null, - null + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_transferGasFee: UNSUPPORTED_TOKEN')) + + feeTokenDataERC20 = utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 3]) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - // Increment nonce because it's the second transfer - transferObj.nonce = nonce.add(1) + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.metaSafeTransferFrom( - erc1155Contract.address, + const tx2 = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, receiverAddress, id, amount, isGasReceipt, data, - { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + { gasLimit: 2000000 } ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_transferGasFee: UNSUPPORTED_TOKEN')) + }) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + transferObj.isGasFee = false + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + transferObj.isGasFee = false + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.fulfilled + }) + + describe('When receiver is a contract', () => { + it('should REVERT if gas used in onERC1155Received exceeds limit', async () => { + const lowGasLimit = 1000 + gasReceipt!.gasLimitCallback = lowGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + }) + + it('should PASS if gas used in onERC1155Received does not exceed limit', async () => { + const okGasLimit = 9700 + gasReceipt!.gasLimitCallback = okGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) + + it('should PASS if gasLimitCallback is higher than gas sent in transaction', async () => { + const highGasLimit = 30000000 + gasReceipt!.gasLimitCallback = highGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) + }) + }) + + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) + + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.fulfilled + }) + + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + transferObj.isGasFee = true + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError()) }) + }) + + context('When successful transfer', () => { + let tx: ContractTransaction - it('should emit NonceChange event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') + beforeEach(async () => { + tx = await operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data + ) }) - it('should have `_signer` as `signer` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + it('should correctly update balance of sender', async () => { + const balance = await erc1155Contract.balanceOf(ownerAddress, id) + expect(balance).to.be.eql(BigNumber.from(initBalance - amount)) + }) - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) + it('should correctly update balance of receiver', async () => { + const balance = await erc1155Contract.balanceOf(receiverAddress, id) + expect(balance).to.be.eql(BigNumber.from(amount)) }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) + + it('should update gas token balance of sender', async () => { + const senderBalance = await erc1155Contract.balanceOf(ownerAddress, feeTokenID) + expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + }) + + it('should update gas token balance of executor', async () => { + const balance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) + expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + }) + }) - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) + describe('TransferSingle event', async () => { + let filterFromOperatorContract: EventFilter + + it('should emit TransferSingle event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferSingle') + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferSingle( + operatorContract.address, + null, + null, + null, + null + ) + + // Increment nonce because it's the second transfer + transferObj.nonce = nonce.add(1) + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.metaSafeTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) + + it('should emit NonceChange event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) + + it('should have `_signer` as `signer` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) + }) + + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) + }) }) }) }) }) }) }) - }) - describe('metaSafeBatchTransferFrom() Function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock - const METATRANSFER_IDENTIFIER = '0xebc71fa5' - - let transferData: string | null = 'Hello from the other side' - const initBalance = 100 - const amount = 10 - const nonce = BigNumber.from(0) - - // Parameters for balances - let ids: any[], amounts: any[] - const nTokenTypes = 33 - - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 - const feeTokenID = 666 - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - let transferObj: BatchTransferSignature - let gasReceipt: GasReceipt | null - let domainHash: string - let data: string - - const conditions = [ - [transferData, true, 'Gas receipt & transfer data'], - [null, true, 'Gas receipt w/o transfer data'], - [transferData, false, 'Transfer data w/o gas receipt '], - [null, false, 'No Gas receipt & No transfer data'] - ] - - conditions.forEach(function(condition) { - context(condition[2] as string, () => { - beforeEach(async () => { - // Get conditions - transferData = (await condition[0]) as string | null - isGasReceipt = (await condition[1]) as boolean - - // Deploy contracts - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - - // Mint tokens - ;(ids = []), (amounts = []) - - // Minting enough amounts for transfer for each types - for (let i = 0; i < nTokenTypes; i++) { - await erc1155Contract.mintMock(ownerAddress, i, initBalance, []) - ids.push(i) - amounts.push(amount) - } - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 160000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Transfer Signature Object - transferObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - receiver: receiverAddress, - ids: ids.slice(0), - amounts: amounts.slice(0), - isGasFee: isGasReceipt, - transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), - nonce: nonce - } - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Domain hash - domainHash = utils.keccak256( - utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - - // Data to pass in transfer method - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - }) + describe('metaSafeBatchTransferFrom() Function', () => { + let receiverContract: ERC1155ReceiverMock - it('should REVERT if contract address is incorrect', async () => { - domainHash = utils.keccak256( - utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) - ) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + let transferData: string | null = 'Hello from the other side' + const initBalance = 100 + const amount = 10 + const nonce = BigNumber.from(0) - it('should REVERT if signer address is incorrect', async () => { - transferObj.signerWallet = operatorWallet - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + // Parameters for balances + let ids: any[], amounts: any[] + const nTokenTypes = 33 - it('should REVERT if receiver address is incorrect', async () => { - transferObj.receiver = ownerAddress - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) - it('should REVERT if token id is incorrect', async () => { - transferObj.ids[0] = 6 - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + const feeType = 0 + const feeTokenID = 666 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array - it('should REVERT if token amount is incorrect', async () => { - transferObj.amounts[0] = amount + 1 - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + let transferObj: BatchTransferSignature + let gasReceipt: GasReceipt | null + let domainHash: string + let data: string - it('should REVERT if transfer data is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.ids, - transferObj.amounts, - transferObj.nonce - ]) - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - let goodGasAndTransferData - let badGasAndTransferData - - // Correct and incorrect transferData - if (isGasReceipt) { - goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - badGasAndTransferData = utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] - ) - } else { - goodGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [transferData]) - badGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) - } - - // Encode normally the whole thing - sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + const conditions = [ + [transferData, true, 'Gas receipt & transfer data'], + [null, true, 'Gas receipt w/o transfer data'], + [transferData, false, 'Transfer data w/o gas receipt '], + [null, false, 'No Gas receipt & No transfer data'] + ] - it('should REVERT if nonce is incorrect', async () => { - transferObj.nonce = nonce.add(101) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - transferObj.nonce = nonce - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSafeBatchTransferFrom(ownerAddress, receiverAddress, ids, amounts, isGasReceipt, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - }) + conditions.forEach(function(condition) { + context(condition[2] as string, () => { + beforeEach(async () => { + // Get conditions + transferData = (await condition[0]) as string | null + isGasReceipt = (await condition[1]) as boolean + + // Deploy contracts + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + + // Mint tokens + ids = [] + amounts = [] + + // Minting enough amounts for transfer for each types + for (let i = 0; i < nTokenTypes; i++) { + await erc1155Contract.mintMock(ownerAddress, i, initBalance, []) + ids.push(i) + amounts.push(amount) + } - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.fulfilled - }) + feeTokenAddress = erc1155Contract.address - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] + ) - let erc1271WalletAddress + // Gas Receipt + gasReceipt = { + gasLimitCallback: 160000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address - - await erc1155Contract.batchMintMock(erc1271WalletAddress, ids, amounts, []) - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Transfer Signature Object + transferObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + receiver: receiverAddress, + ids: ids.slice(0), + amounts: amounts.slice(0), + isGasFee: isGasReceipt, + transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), + nonce: nonce + } - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - }) + // Domain hash + domainHash = utils.keccak256( + utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Data to pass in transfer method + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + }, 60000) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + it('should REVERT if contract address is incorrect', async () => { + domainHash = utils.keccak256( + utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) + ) + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.fulfilled - }) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - }) - describe('When signature is valid', () => { - it('should REVERT if insufficient balance', async () => { - transferObj.amounts[0] = initBalance + 1 - amounts[0] = initBalance + 1 + it('should REVERT if signer address is incorrect', async () => { + transferObj.signerWallet = operatorWallet data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( @@ -1433,780 +1217,977 @@ describe('ERC1155Meta', () => { ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if sending to 0x0', async () => { - transferObj.receiver = ZERO_ADDRESS + it('should REVERT if receiver address is incorrect', async () => { + transferObj.receiver = ownerAddress data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - ZERO_ADDRESS, + receiverAddress, ids, amounts, isGasReceipt, data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#metaSafeBatchTransferFrom: INVALID_RECIPIENT')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if transfer leads to overflow', async () => { - await operatorERC1155Contract.mintMock(receiverAddress, ids[0], MAXVAL, []) + it('should REVERT if token id is incorrect', async () => { + transferObj.ids[0] = 6 + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT when sending to non-receiver contract', async () => { - transferObj.receiver = erc1155Contract.address + it('should REVERT if token amount is incorrect', async () => { + transferObj.amounts[0] = amount + 1 data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - erc1155Contract.address, + receiverAddress, ids, amounts, isGasReceipt, data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if invalid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + it('should REVERT if transfer data is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] + + const signer = await transferObj.signerWallet.getAddress() + + // Packed encoding of transfer signature message + let sigData = utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.ids, + transferObj.amounts, + transferObj.nonce + ]) + + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + let goodGasAndTransferData + let badGasAndTransferData + + // Correct and incorrect transferData + if (isGasReceipt) { + goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + badGasAndTransferData = utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] + ) + } else { + goodGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [transferData]) + badGasAndTransferData = utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) + } + + // Encode normally the whole thing + sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - // Force invalid response - await receiverContract.setShouldReject(true) + // PASS BAD DATA + data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - { gasLimit: 200_000 } + data ) - await expect(tx).to.be.rejectedWith(/ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE|out of gas/) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if valid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address + it('should REVERT if nonce is incorrect', async () => { + transferObj.nonce = nonce.add(101) data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - { gasLimit: 2000000 } + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) + + // Correct nonce + transferObj.nonce = nonce + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSafeBatchTransferFrom(ownerAddress, receiverAddress, ids, amounts, isGasReceipt, data) + + // Nonce lower + const tx2 = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) + }) + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) await expect(tx).to.be.fulfilled }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract + + let erc1271WalletAddress + + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.batchMintMock(erc1271WalletAddress, ids, amounts, []) + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) }) - it('should send gas fee to tx.origin is fee recipient ix 0x0', async () => { - gasReceipt!.feeRecipient = ZERO_ADDRESS + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + }) + + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.fulfilled + }) + }) + }) + + describe('When signature is valid', () => { + it('should REVERT if insufficient balance', async () => { + transferObj.amounts[0] = initBalance + 1 + amounts[0] = initBalance + 1 data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - await receiverERC1155Contract.metaSafeBatchTransferFrom( + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data + data, + HIGH_GAS_LIMIT ) - - const receiverBalance = await operatorERC1155Contract.balanceOf(receiverAddress, feeTokenID) - - expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { - await receiverERC1155Contract.metaSafeBatchTransferFrom( + it('should REVERT if sending to 0x0', async () => { + transferObj.receiver = ZERO_ADDRESS + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + ZERO_ADDRESS, ids, amounts, isGasReceipt, data ) - const operatorBalance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) - - expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#metaSafeBatchTransferFrom: INVALID_RECIPIENT')) }) - it('should REVERT if gasReceipt is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.ids, - transferObj.amounts, - transferObj.nonce - ]) - - // Form bad gas receipt - const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - - // Correct and incorrect transferData - const goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - const badGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [badGasReceipt, transferData]) - - // Encode normally the whole thing - sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - + it('should REVERT if transfer leads to overflow', async () => { + await operatorERC1155Contract.mintMock(receiverAddress, ids[0], MAXVAL, []) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data + data, + HIGH_GAS_LIMIT ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should REVERT if gas receipt is passed, but not claimed', async () => { + it('should REVERT when sending to non-receiver contract', async () => { + transferObj.receiver = erc1155Contract.address + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + erc1155Contract.address, ids, amounts, - false, + isGasReceipt, data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) }) - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - transferObj.isGasFee = false + it.skip('should REVERT if invalid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + // Force invalid response + await receiverContract.setShouldReject(true) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, ids, amounts, - true, - data + isGasReceipt, + data, + { gasLimit: 200_000 } ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + //FIXME Wrong error returned + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) }) - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - transferObj.isGasFee = false + it('should PASS if valid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, ids, amounts, - false, - data + isGasReceipt, + data, + { gasLimit: 2000000 } ) + await expect(tx).to.be.fulfilled }) - describe('When receiver is a contract', () => { - it('should REVERT if gas used in onERC1155BatchReceived exceeds limit', async () => { - const lowGasLimit = 1000 - gasReceipt!.gasLimitCallback = lowGasLimit - transferObj.receiver = receiverContract.address - - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverContract.address, - ids, - amounts, - isGasReceipt, - data, - { gasLimit: 2_000_000 } - ) - await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } }) - it('should PASS if gas used in onERC1155BatchReceived does not exceed limit', async () => { - const okGasLimit = 160000 - gasReceipt!.gasLimitCallback = okGasLimit - transferObj.receiver = receiverContract.address + it('should send gas fee to tx.origin is fee recipient ix 0x0', async () => { + gasReceipt!.feeRecipient = ZERO_ADDRESS data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + await receiverERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - { gasLimit: 2000000 } + data ) - await expect(tx).to.be.fulfilled - }) - it('should PASS if gasLimit is higher than gas sent in transaction', async () => { - const highGasLimit = 3000000 - gasReceipt!.gasLimitCallback = highGasLimit - transferObj.receiver = receiverContract.address + const receiverBalance = await operatorERC1155Contract.balanceOf(receiverAddress, feeTokenID) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + }) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { + await receiverERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - { gasLimit: 2000000 } + data ) - await expect(tx).to.be.fulfilled - }) - }) - }) - - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) - - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - false, - data - ) - await expect(tx).to.be.fulfilled - }) - - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - true, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - transferObj.isGasFee = true - data = await encodeMetaBatchTransferFromData(transferObj, domainHash) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - true, - data - ) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - }) - - context('When successful transfer', () => { - let tx: ContractTransaction + const operatorBalance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) - beforeEach(async () => { - tx = await operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - }) + expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) + }) - it('should correctly update balance of sender and receiver', async () => { - let balanceFrom: BigNumber - let balanceTo: BigNumber + it('should REVERT if gasReceipt is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] - for (let i = 0; i < ids.length; i++) { - balanceFrom = await operatorERC1155Contract.balanceOf(ownerAddress, ids[i]) - balanceTo = await operatorERC1155Contract.balanceOf(receiverAddress, ids[i]) + const signer = await transferObj.signerWallet.getAddress() - expect(balanceFrom).to.be.eql(BigNumber.from(initBalance - amounts[i])) - expect(balanceTo).to.be.eql(BigNumber.from(amounts[i])) - } - }) + // Packed encoding of transfer signature message + let sigData = utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.ids, + transferObj.amounts, + transferObj.nonce + ]) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } + // Form bad gas receipt + const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } + + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + + // Correct and incorrect transferData + const goodGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + const badGasAndTransferData = utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [badGasReceipt, transferData]) + + // Encode normally the whole thing + sigData = utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce + + // PASS BAD DATA + data = utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should update gas token balance of sender', async () => { - const senderBalance = await operatorERC1155Contract.balanceOf(ownerAddress, feeTokenID) - expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should update gas token balance of executor', async () => { - const balance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) - expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + transferObj.isGasFee = false + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - }) - describe('TransferBatch event', async () => { - let filterFromOperatorContract: EventFilter - let operatorContract: ERC1155OperatorMock + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + transferObj.isGasFee = false + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - beforeEach(async () => { - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.fulfilled }) - it('should emit 1 TransferBatch events of N transfers', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![1] - expect(ev.event).to.be.eql('TransferBatch') + describe('When receiver is a contract', () => { + it('should REVERT if gas used in onERC1155BatchReceived exceeds limit', async () => { + const lowGasLimit = 1000 + gasReceipt!.gasLimitCallback = lowGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + { gasLimit: 2_000_000 } + ) + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + }) + + it('should PASS if gas used in onERC1155BatchReceived does not exceed limit', async () => { + const okGasLimit = 160000 + gasReceipt!.gasLimitCallback = okGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) + + it('should PASS if gasLimit is higher than gas sent in transaction', async () => { + const highGasLimit = 3000000 + gasReceipt!.gasLimitCallback = highGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + { gasLimit: 2000000 } + ) + await expect(tx).to.be.fulfilled + }) + }) + }) - const args = ev.args! as any - expect(args._ids.length).to.be.eql(ids.length) + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[1]) { + this.test!.parent!.pending = true + this.skip() + } }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.fulfilled + }) - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferBatch( - operatorContract.address, - null, - null, - null, - null + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + transferObj.isGasFee = true + data = await encodeMetaBatchTransferFromData(transferObj, domainHash) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data ) + await expect(tx).to.be.rejectedWith(RevertError()) + }) + }) - //Increment nonce because it's the second transfer - transferObj.nonce = nonce.add(1) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + context('When successful transfer', () => { + let tx: ContractTransaction - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.metaSafeBatchTransferFrom( - erc1155Contract.address, + beforeEach(async () => { + tx = await operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data, - { gasLimit: 2000000 } // INCORRECT GAS ESTIMATION - ) - - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics + data ) - - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) }) - it('should emit NonceChange event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') - }) + it('should correctly update balance of sender and receiver', async () => { + let balanceFrom: BigNumber + let balanceTo: BigNumber - it('should have `_signer` as `signer` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + for (let i = 0; i < ids.length; i++) { + balanceFrom = await operatorERC1155Contract.balanceOf(ownerAddress, ids[i]) + balanceTo = await operatorERC1155Contract.balanceOf(receiverAddress, ids[i]) - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) + expect(balanceFrom).to.be.eql(BigNumber.from(initBalance - amounts[i])) + expect(balanceTo).to.be.eql(BigNumber.from(amounts[i])) + } }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) + + it('should update gas token balance of sender', async () => { + const senderBalance = await operatorERC1155Contract.balanceOf(ownerAddress, feeTokenID) + expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + }) + + it('should update gas token balance of executor', async () => { + const balance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) + expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + }) + }) - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) + describe('TransferBatch event', async () => { + let filterFromOperatorContract: EventFilter + let operatorContract: ERC1155OperatorMock + + beforeEach(async () => { + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + }) + + it('should emit 1 TransferBatch events of N transfers', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![1] + expect(ev.event).to.be.eql('TransferBatch') + + const args = ev.args! as any + expect(args._ids.length).to.be.eql(ids.length) + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferBatch( + operatorContract.address, + null, + null, + null, + null + ) + + //Increment nonce because it's the second transfer + transferObj.nonce = nonce.add(1) + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.metaSafeBatchTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data, + { gasLimit: 2000000 } // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) + + it('should emit NonceChange event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) + + it('should have `_signer` as `signer` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) + }) + + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) + }) }) }) }) }) }) }) - }) - describe('metaSetApprovalForAll() function', () => { - const initBalance = 100 - let isGasReimbursed = true - const approved = true - const nonce = BigNumber.from(0) - const id = 66 - - let approvalObj: ApprovalSignature - let gasReceipt: GasReceipt | null - let domainHash: string - let data: string - - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 - const feeTokenID = 666 - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - const conditions = [ - [true, 'Gas receipt'], - [false, 'No Gas receipt'] - ] - - conditions.forEach(function(condition) { - context(condition[1] as string, () => { - beforeEach(async () => { - isGasReceipt = condition[0] as boolean - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 125000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - isGasReimbursed = isGasReceipt ? true : false - - // Approval Signture Object - approvalObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - operator: operatorAddress, - approved: approved, - isGasFee: isGasReceipt, - nonce: nonce - } - - // Mint tokens - await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Domain hash - domainHash = utils.keccak256( - utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - // Data to pass in approval method - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - }) + describe('metaSetApprovalForAll() function', () => { + const initBalance = 100 + let isGasReimbursed = true + const approved = true + const nonce = BigNumber.from(0) + const id = 66 + + let approvalObj: ApprovalSignature + let gasReceipt: GasReceipt | null + let domainHash: string + let data: string + + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) + + const feeType = 0 + const feeTokenID = 666 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array + + const conditions = [ + [true, 'Gas receipt'], + [false, 'No Gas receipt'] + ] + + conditions.forEach(function(condition) { + context(condition[1] as string, () => { + beforeEach(async () => { + isGasReceipt = condition[0] as boolean - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.fulfilled - }) + feeTokenAddress = erc1155Contract.address - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract + feeTokenDataERC1155 = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] + ) - let erc1271WalletAddress + // Gas Receipt + gasReceipt = { + gasLimitCallback: 125000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + isGasReimbursed = isGasReceipt ? true : false + + // Approval Signture Object + approvalObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + operator: operatorAddress, + approved: approved, + isGasFee: isGasReceipt, + nonce: nonce + } - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + // Mint tokens + await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) + + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + + // Domain hash + domainHash = utils.keccak256( + utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) + // Data to pass in approval method + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) }) - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - approvalObj.owner = erc1271WalletAddress - approvalObj.signerWallet = receiverWallet + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.fulfilled + }) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract + + let erc1271WalletAddress + + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + }) + + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + approvalObj.owner = erc1271WalletAddress + approvalObj.signerWallet = receiverWallet + + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + }) + + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + approvalObj.owner = erc1271WalletAddress + approvalObj.signerWallet = receiverWallet + + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should PASS if signature is valid', async () => { + approvalObj.owner = erc1271WalletAddress + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.fulfilled + }) }) }) - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - approvalObj.owner = erc1271WalletAddress - approvalObj.signerWallet = receiverWallet + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[0]) { + this.test!.parent!.pending = true + this.skip() + } + }) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if signature is valid', async () => { - approvalObj.owner = erc1271WalletAddress - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx).to.be.fulfilled + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + approvalObj.isGasFee = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - }) - }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[0]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + approvalObj.isGasFee = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - it('should REVERT if gas receipt is passed, but not claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) + await expect(tx).to.be.fulfilled + }) }) - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - approvalObj.isGasFee = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[0]) { + this.test!.parent!.pending = true + this.skip() + } + }) - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - approvalObj.isGasFee = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) + await expect(tx).to.be.fulfilled + }) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.fulfilled - }) - }) + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[0]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + approvalObj.isGasFee = true + data = await encodeMetaApprovalData(approvalObj, domainHash) - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.fulfilled + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError()) + }) }) - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + it('should REVERT if contract address is incorrect', async () => { + domainHash = utils.keccak256(utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverAddress])) + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - approvalObj.isGasFee = true - data = await encodeMetaApprovalData(approvalObj, domainHash) + it('should REVERT if operator address is incorrect', async () => { + approvalObj.operator = receiverAddress + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) - await expect(tx).to.be.rejectedWith(RevertError()) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) }) - }) - - it('should REVERT if contract address is incorrect', async () => { - domainHash = utils.keccak256(utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverAddress])) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) - it('should REVERT if operator address is incorrect', async () => { - approvalObj.operator = receiverAddress - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + it('should REVERT if approved value is incorrect', async () => { + approvalObj.approved = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) + }) - it('should REVERT if approved value is incorrect', async () => { - approvalObj.approved = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + it('should REVERT if nonce is incorrect', async () => { + approvalObj.nonce = nonce.add(101) + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_SIGNATURE')) - }) + // Nonce higher + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - it('should REVERT if nonce is incorrect', async () => { - approvalObj.nonce = nonce.add(101) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - - // Nonce higher - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - approvalObj.nonce = nonce - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) - }) + // Correct nonce + approvalObj.nonce = nonce + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - it('should emit an ApprovalForAll event', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - - expect(receipt.events![1].event).to.be.eql('ApprovalForAll') - }) + // Nonce lower + const tx2 = operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155Meta#_signatureValidation: INVALID_NONCE')) + }) - it('should set the operator status to _status argument', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.fulfilled + it('should emit an ApprovalForAll event', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + const receipt = await tx.wait(1) - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) - }) + expect(receipt.events![1].event).to.be.eql('ApprovalForAll') + }) - it('should emit NonceChange event', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') - }) + it('should set the operator status to _status argument', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.fulfilled - it('should have `_signer` as `signer` in NonceChange', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - const ev = receipt.events![0] - - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) - }) + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - const ev = receipt.events![0] - - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) - }) + it('should emit NonceChange event', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) - context('When the operator was already an operator', () => { - beforeEach(async () => { + it('should have `_signer` as `signer` in NonceChange', async () => { const tx = await operatorERC1155Contract.metaSetApprovalForAll( ownerAddress, operatorAddress, @@ -2214,36 +2195,68 @@ describe('ERC1155Meta', () => { isGasReimbursed, data ) + const receipt = await tx.wait(1) + const ev = receipt.events![0] - // Update nonce of approval signature object for subsequent tests - approvalObj.nonce = nonce.add(1) + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) }) - it('should leave the operator status to set to true again', async () => { - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSetApprovalForAll( + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( ownerAddress, operatorAddress, approved, isGasReimbursed, data ) - await expect(tx).to.be.fulfilled + const receipt = await tx.wait(1) + const ev = receipt.events![0] - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) }) - it('should allow the operator status to be set to false', async () => { - approvalObj.approved = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + context('When the operator was already an operator', () => { + beforeEach(async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, false, isGasReimbursed, data) - await expect(tx).to.be.fulfilled + // Update nonce of approval signature object for subsequent tests + approvalObj.nonce = nonce.add(1) + }) - const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) - expect(status).to.be.eql(false) + it('should leave the operator status to set to true again', async () => { + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) + + it('should allow the operator status to be set to false', async () => { + approvalObj.approved = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, false, isGasReimbursed, data) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) + expect(status).to.be.eql(false) + }) }) }) }) diff --git a/tests/ERC1155MetaPackedBalance.spec.ts b/tests/ERC1155MetaPackedBalance.spec.ts index a85ffdd..196021f 100644 --- a/tests/ERC1155MetaPackedBalance.spec.ts +++ b/tests/ERC1155MetaPackedBalance.spec.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers' +import { constants, ethers } from 'ethers' import { AbstractContract, @@ -22,7 +22,8 @@ import { ERC1271WalletValidationMock, ERC1155ReceiverMock, ERC1155OperatorMock, - ERC20Mock + ERC20Mock, + ProxyUpgradeableDeployerMock } from 'src' import { GasReceipt, TransferSignature, ApprovalSignature, BatchTransferSignature } from 'src/typings/tx-types' @@ -30,7 +31,7 @@ import { GasReceipt, TransferSignature, ApprovalSignature, BatchTransferSignatur // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = createTestWallet(web3, 0) +const { wallet: ownerWallet, provider: ownerProvider } = createTestWallet(web3, 0) const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = createTestWallet(web3, 2) @@ -41,882 +42,701 @@ ownerProvider.pollingInterval = 1000 operatorProvider.pollingInterval = 1000 receiverProvider.pollingInterval = 1000 -describe('ERC1155MetaPackedBalance', () => { - const MAXVAL = BigNumber.from(2) - .pow(32) - .sub(1) // 2**32 - 1 - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const DOMAIN_SEPARATOR_TYPEHASH = '0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749' - - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - let ownerAddress: string - let receiverAddress: string - let operatorAddress: string - let erc1155Abstract: AbstractContract - let operatorAbstract: AbstractContract - - let erc1155Contract: ERC1155MetaMintBurnPackedBalanceMock - let operatorERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock - let receiverERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock - - // load contract abi and deploy to test server - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - - erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') - operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') - }) - - // deploy before each test, to reset state of contract - beforeEach(async () => { - erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock - operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnPackedBalanceMock - receiverERC1155Contract = (await erc1155Contract.connect(receiverSigner)) as ERC1155MetaMintBurnPackedBalanceMock - }) - - describe('metaSafeTransferFrom() (Meta) Function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock - - let transferData: string | null = 'Hello from the other side' - const initBalance = 100 - const amount = 10 - const nonce = BigNumber.from(0) - const id = 66 - - const feeTokenID = 666 - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 //ERC-11555 - let feeToken: BigNumber - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - let transferObj: TransferSignature - let domainHash: string - let gasReceipt: GasReceipt | null - let data: string - - const conditions = [ - [transferData, true, 'Gas receipt & transfer data'], - [null, true, 'Gas receipt w/o transfer data'], - [transferData, false, 'Transfer data w/o gas receipt '], - [null, false, 'No Gas receipt & No transfer data'] - ] - - conditions.forEach(function(condition) { - context(condition[2] as string, () => { - beforeEach(async () => { - // Get conditions - transferData = condition[0] as string | null - isGasReceipt = condition[1] as boolean - - // Deploy contracts - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 150000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Transfer Signature Object - transferObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - receiver: receiverAddress, - id: id, - amount: amount, - isGasFee: isGasReceipt, - transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), - nonce: nonce - } - - // Mint tokens - await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Get domain hash - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - }) - - it("should REVERT if data is 'random", async () => { - const dataUint8 = utils.toUtf8Bytes('Breakthroughs! over the river! flips and crucifixions! gone down the flood!') - const data = BigNumber.from(dataUint8).toHexString() - - // Check if data lelngth is more than 69 - expect(ethers.utils.arrayify(data).length).to.be.at.least(70) - - const tx = erc1155Contract.metaSafeTransferFrom(ownerAddress, receiverContract.address, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - - it('should REVERT if contract address is incorrect', async () => { - // Domain hash - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) - ) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if signer address is incorrect', async () => { - transferObj.signerWallet = operatorWallet - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if receiver address is incorrect', async () => { - transferObj.receiver = ownerAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155MetaPackedBalance' + (upgradeable ? 'Upgradeable': ''), () => { + const MAXVAL = BigNumber.from(2) + .pow(32) + .sub(1) // 2**32 - 1 + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const DOMAIN_SEPARATOR_TYPEHASH = '0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749' + + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let ownerAddress: string + let receiverAddress: string + let operatorAddress: string + let erc1155Abstract: AbstractContract + let operatorAbstract: AbstractContract + + let erc1155Contract: ERC1155MetaMintBurnPackedBalanceMock + let operatorERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock + let receiverERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock + let factoryContract: ProxyUpgradeableDeployerMock + + // load contract abi and deploy to test server + before(async () => { + ownerAddress = await ownerWallet.getAddress() + receiverAddress = await receiverWallet.getAddress() + operatorAddress = await operatorWallet.getAddress() + + if (upgradeable) { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + } else { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') + } + operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') + }) - it('should REVERT if token id is incorrect', async () => { - transferObj.id = id + 1 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const deployERC1155Contract = async () => { + let contract: ERC1155MetaMintBurnPackedBalanceMock + if (upgradeable) { + contract = (await erc1155Abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnPackedBalanceMock + + // Create proxy + let tx = factoryContract.createProxy(contract.address, constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(contract.address, constants.HashZero, ownerWallet.address); + contract = (await erc1155Abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnPackedBalanceMock; + tx = contract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled + } else { + contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock + } + return contract + } + + // deploy before each test, to reset state of contract + beforeEach(async () => { + erc1155Contract = await deployERC1155Contract() + operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnPackedBalanceMock + receiverERC1155Contract = (await erc1155Contract.connect(receiverSigner)) as ERC1155MetaMintBurnPackedBalanceMock + }) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + describe('metaSafeTransferFrom() (Meta) Function', () => { + let receiverContract: ERC1155ReceiverMock + let operatorContract: ERC1155OperatorMock + + let transferData: string | null = 'Hello from the other side' + const initBalance = 100 + const amount = 10 + const nonce = BigNumber.from(0) + const id = 66 + + const feeTokenID = 666 + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) + + const feeType = 0 //ERC-11555 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array + + let transferObj: TransferSignature + let domainHash: string + let gasReceipt: GasReceipt | null + let data: string + + const conditions = [ + [transferData, true, 'Gas receipt & transfer data'], + [null, true, 'Gas receipt w/o transfer data'], + [transferData, false, 'Transfer data w/o gas receipt '], + [null, false, 'No Gas receipt & No transfer data'] + ] + + conditions.forEach(function(condition) { + context(condition[2] as string, () => { + beforeEach(async () => { + // Get conditions + transferData = condition[0] as string | null + isGasReceipt = condition[1] as boolean - it('should REVERT if token amount is incorrect', async () => { - transferObj.amount = amount + 1 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Deploy contracts + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + feeTokenAddress = erc1155Contract.address - it('should REVERT if transfer data is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = ethers.utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.id, - transferObj.amount, - transferObj.nonce - ]) - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - let goodGasAndTransferData - let badGasAndTransferData - - // Correct and incorrect transferData - if (isGasReceipt) { - goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] ) - } else { - goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [transferData]) - badGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) - } - - // Encode normally the whole thing - sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if nonce is incorrect', async () => { - transferObj.nonce = nonce.add(101) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - - // Nonce higher - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - transferObj.nonce = nonce - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - }) - - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.fulfilled - }) - - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract - let erc1271WalletAddress - - beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address - - await erc1155Contract.mintMock(erc1271WalletAddress, id, initBalance, []) - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) - }) + // Gas Receipt + gasReceipt = { + gasLimitCallback: 150000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Transfer Signature Object + transferObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + receiver: receiverAddress, + id: id, + amount: amount, + isGasFee: isGasReceipt, + transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), + nonce: nonce + } - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + // Mint tokens + await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - it('should REVERT if token ID is not 66', async () => { - const badID = 77 - await erc1155Contract.mintMock(erc1271WalletAddress, badID, initBalance, []) - transferObj.from = erc1271WalletAddress - transferObj.id = badID - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - it('should REVERT if amount is more than 100', async () => { - await erc1155Contract.mintMock(erc1271WalletAddress, id, 101, []) - transferObj.from = erc1271WalletAddress - transferObj.amount = 101 - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + // Get domain hash + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.fulfilled - }) + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) }) - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + it("should REVERT if data is 'random", async () => { + const dataUint8 = utils.toUtf8Bytes('Breakthroughs! over the river! flips and crucifixions! gone down the flood!') + const data = BigNumber.from(dataUint8).toHexString() - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + // Check if data lelngth is more than 69 + expect(ethers.utils.arrayify(data).length).to.be.at.least(70) - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeTransferFrom( - erc1271WalletAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.fulfilled - }) + const tx = erc1155Contract.metaSafeTransferFrom(ownerAddress, receiverContract.address, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError()) }) - }) - describe('When signature is valid', () => { - it('should REVERT if insufficient balance', async () => { - transferObj.amount = initBalance + 1 + it('should REVERT if contract address is incorrect', async () => { + // Domain hash + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) + ) data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - initBalance + 1, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if sending to 0x0', async () => { - transferObj.receiver = ZERO_ADDRESS + it('should REVERT if signer address is incorrect', async () => { + transferObj.signerWallet = operatorWallet data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, ZERO_ADDRESS, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#metaSafeTransferFrom: INVALID_RECIPIENT')) - }) - - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock(receiverAddress, id, MAXVAL, []) const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT when sending to non-receiver contract', async () => { - transferObj.receiver = erc1155Contract.address + it('should REVERT if receiver address is incorrect', async () => { + transferObj.receiver = ownerAddress data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - erc1155Contract.address, - id, - amount, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if invalid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - if (gasReceipt) { - gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit - } + it('should REVERT if token id is incorrect', async () => { + transferObj.id = id + 1 data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Force invalid response - await receiverContract.setShouldReject(true) - - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverContract.address, - id, - amount, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE') - ) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if valid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - if (gasReceipt) { - gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit - } + it('should REVERT if token amount is incorrect', async () => { + transferObj.amount = amount + 1 data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverContract.address, - id, - amount, - isGasReceipt, - data - ) - - //await expect(tx).to.be.fulfilled - await expect(tx).to.be.fulfilled + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if transfer data is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] + + const signer = await transferObj.signerWallet.getAddress() + + // Packed encoding of transfer signature message + let sigData = ethers.utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.id, + transferObj.amount, + transferObj.nonce + ]) + + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + let goodGasAndTransferData + let badGasAndTransferData + + // Correct and incorrect transferData + if (isGasReceipt) { + goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] + ) + } else { + goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [transferData]) + badGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) + } - it('should send gas fee to msg.sender is fee recipient ix 0x0', async () => { - gasReceipt!.feeRecipient = ZERO_ADDRESS + // Encode normally the whole thing + sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + // PASS BAD DATA + data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - const receiverBalance = await erc1155Contract.balanceOf(receiverAddress, feeTokenID) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) - }) + it('should REVERT if nonce is incorrect', async () => { + transferObj.nonce = nonce.add(101) + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { - await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - const operatorBalance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) + // Nonce higher + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) - }) + // Correct nonce + transferObj.nonce = nonce + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - it('should REVERT if gasReceipt is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] + // Nonce lower + const tx2 = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) + }) - const signer = await transferObj.signerWallet.getAddress() + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.fulfilled + }) - // Packed encoding of transfer signature message - let sigData = ethers.utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.id, - transferObj.amount, - transferObj.nonce - ]) + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract - // Form bad gas receipt - const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } + let erc1271WalletAddress - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.mintMock(erc1271WalletAddress, id, initBalance, []) + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + }) - // Correct and incorrect transferData - const goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, transferData] - ) - const badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [badGasReceipt, transferData] - ) + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet - // Encode normally the whole thing - sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce + it('should REVERT if token ID is not 66', async () => { + const badID = 77 + await erc1155Contract.mintMock(erc1271WalletAddress, badID, initBalance, []) + transferObj.from = erc1271WalletAddress + transferObj.id = badID + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - // PASS BAD DATA - data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) + it('should REVERT if amount is more than 100', async () => { + await erc1155Contract.mintMock(erc1271WalletAddress, id, 101, []) + transferObj.from = erc1271WalletAddress + transferObj.amount = 101 + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) }) - it('should PASS if another approved ERC-1155 is used for fee', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnPackedBalanceMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeTransferFrom( + erc1271WalletAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) + }) + }) - // Data to pass in transfer method + describe('When signature is valid', () => { + it('should REVERT if insufficient balance', async () => { + transferObj.amount = initBalance + 1 data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, receiverAddress, id, - amount, + initBalance + 1, isGasReceipt, data, HIGH_GAS_LIMIT ) - await expect(tx).to.be.fulfilled + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should REVERT if NOT approved ERC-1155 is used for fee', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnPackedBalanceMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) + it('should REVERT if sending to 0x0', async () => { + transferObj.receiver = ZERO_ADDRESS + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, ZERO_ADDRESS, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#metaSafeTransferFrom: INVALID_RECIPIENT')) + }) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock(receiverAddress, id, MAXVAL, []) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - // Data to pass in transfer method + it('should REVERT when sending to non-receiver contract', async () => { + transferObj.receiver = erc1155Contract.address data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + erc1155Contract.address, id, amount, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) }) - it('should REVERT if another ERC-1155 is used for fee without sufficient balance', async () => { - const erc1155Contract2 = (await erc1155Abstract.deploy(ownerWallet, [ - NAME, - METADATA_URI - ])) as ERC1155MetaMintBurnPackedBalanceMock - await erc1155Contract2.mintMock(ownerAddress, feeTokenID, 100, []) - await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [erc1155Contract2.address, feeTokenID, 0] - ) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 + it('should REVERT if invalid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address + if (gasReceipt) { + gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Force invalid response + await receiverContract.setShouldReject(true) + const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, id, amount, isGasReceipt, - data, - HIGH_GAS_LIMIT + data + ) + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE') ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should PASS if approved ERC20 is used for fee', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - - const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 + it('should PASS if valid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address + if (gasReceipt) { + gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, id, amount, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) + + //await expect(tx).to.be.fulfilled await expect(tx).to.be.fulfilled }) - it('should REVERT if NOT approved ERC20 is used for fee', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) - const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) + it('should send gas fee to msg.sender is fee recipient ix 0x0', async () => { + gasReceipt!.feeRecipient = ZERO_ADDRESS - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const receiverBalance = await erc1155Contract.balanceOf(receiverAddress, feeTokenID) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + }) - it('should REVERT if approved ERC20 balance is insufficient', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, 100) + it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { + await receiverERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, isGasReceipt, data) + const operatorBalance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) - const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) + expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) + }) - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + it('should REVERT if gasReceipt is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256', 'uint256', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + const signer = await transferObj.signerWallet.getAddress() - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Packed encoding of transfer signature message + let sigData = ethers.utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.id, + transferObj.amount, + transferObj.nonce + ]) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + // Form bad gas receipt + const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } - it('should REVERT if FeeTokenType is not supported', async () => { - const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') - const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock - await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - let feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 2]) + // Correct and incorrect transferData + const goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, transferData] + ) + const badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [badGasReceipt, transferData] + ) - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + // Encode normally the whole thing + sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // PASS BAD DATA + data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - const tx = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_transferGasFee: UNSUPPORTED_TOKEN')) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 3]) + it('should PASS if another approved ERC-1155 is used for fee', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 1000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC20 - } + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - // Data to pass in transfer method - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - const tx2 = operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_transferGasFee: UNSUPPORTED_TOKEN')) - }) + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - it('should REVERT if gas receipt is passed, but not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - transferObj.isGasFee = false - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + it('should REVERT if NOT approved ERC-1155 is used for fee', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - transferObj.isGasFee = false - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.fulfilled - }) + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - describe('When receiver is a contract', () => { - it('should REVERT if gas used in onERC1155Received exceeds limit', async () => { - const lowGasLimit = 1000 - gasReceipt!.gasLimitCallback = lowGasLimit - transferObj.receiver = receiverContract.address + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, data, HIGH_GAS_LIMIT ) - await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR')) }) - it('should PASS if gas used in onERC1155Received does not exceed limit', async () => { - const okGasLimit = 12000 - gasReceipt!.gasLimitCallback = okGasLimit - transferObj.receiver = receiverContract.address + it('should REVERT if another ERC-1155 is used for fee without sufficient balance', async () => { + const erc1155Contract2 = await deployERC1155Contract() + await erc1155Contract2.mintMock(ownerAddress, feeTokenID, 100, []) + await erc1155Contract2.setApprovalForAll(operatorERC1155Contract.address, true) + + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [erc1155Contract2.address, feeTokenID, 0] + ) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, data, HIGH_GAS_LIMIT ) - await expect(tx).to.be.fulfilled + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - it('should PASS if gasLimit is higher than gas sent in transaction', async () => { - const highGasLimit = 3000000 - gasReceipt!.gasLimitCallback = highGasLimit - transferObj.receiver = receiverContract.address + it('should PASS if approved ERC20 is used for fee', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) + await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) + + const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, id, amount, isGasReceipt, @@ -925,508 +745,481 @@ describe('ERC1155MetaPackedBalance', () => { ) await expect(tx).to.be.fulfilled }) - }) - }) - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if NOT approved ERC20 is used for fee', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) - await expect(tx).to.be.fulfilled - }) + const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - transferObj.isGasFee = true - data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - context('When successful transfer', () => { - let tx: ethers.ContractTransaction + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - beforeEach(async () => { - tx = await operatorERC1155Contract.metaSafeTransferFrom( - ownerAddress, - receiverAddress, - id, - amount, - isGasReceipt, - data - ) - }) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should correctly update balance of sender', async () => { - const balance = await erc1155Contract.balanceOf(ownerAddress, id) - expect(balance).to.be.eql(BigNumber.from(initBalance - amount)) - }) + it('should REVERT if approved ERC20 balance is insufficient', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, 100) - it('should correctly update balance of receiver', async () => { - const balance = await erc1155Contract.balanceOf(receiverAddress, id) - expect(balance).to.be.eql(BigNumber.from(amount)) - }) + const feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 1]) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 } - }) - it('should update gas token balance of sender', async () => { - const senderBalance = await erc1155Contract.balanceOf(ownerAddress, feeTokenID) - expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - it('should update gas token balance of executor', async () => { - const balance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) - expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) }) - }) - describe('TransferSingle event', async () => { - let filterFromOperatorContract: ethers.EventFilter + it('should REVERT if FeeTokenType is not supported', async () => { + const erc20Abstract = await AbstractContract.fromArtifactName('ERC20Mock') + const erc20Contract = (await erc20Abstract.deploy(ownerWallet)) as ERC20Mock + await erc20Contract.mockMint(ownerAddress, feeTokenInitBalance) + await erc20Contract.approve(operatorERC1155Contract.address, feeTokenInitBalance) - it('should emit TransferSingle event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferSingle') - }) + let feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 2]) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Data to pass in transfer method + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferSingle( - operatorContract.address, - null, - null, - null, - null + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_transferGasFee: UNSUPPORTED_TOKEN')) + + feeTokenDataERC20 = ethers.utils.defaultAbiCoder.encode(['address', 'uint8'], [erc20Contract.address, 3]) + + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 1000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC20 + } + + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null - // Increment nonce because it's the second transfer - transferObj.nonce = nonce.add(1) + // Data to pass in transfer method data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.metaSafeTransferFrom( - erc1155Contract.address, + const tx2 = operatorERC1155Contract.metaSafeTransferFrom( ownerAddress, receiverAddress, id, amount, isGasReceipt, data, - { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + HIGH_GAS_LIMIT ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_transferGasFee: UNSUPPORTED_TOKEN')) + }) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + transferObj.isGasFee = false + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should emit NonceChange event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + transferObj.isGasFee = false + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.fulfilled }) - it('should have `_signer` as `signer` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + describe('When receiver is a contract', () => { + it('should REVERT if gas used in onERC1155Received exceeds limit', async () => { + const lowGasLimit = 1000 + gasReceipt!.gasLimitCallback = lowGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + }) + + it('should PASS if gas used in onERC1155Received does not exceed limit', async () => { + const okGasLimit = 12000 + gasReceipt!.gasLimitCallback = okGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) + + it('should PASS if gasLimit is higher than gas sent in transaction', async () => { + const highGasLimit = 3000000 + gasReceipt!.gasLimitCallback = highGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverContract.address, + id, + amount, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) + }) + }) - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[1]) { + this.test!.parent!.pending = true + this.skip() + } }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, false, data) + await expect(tx).to.be.fulfilled + }) - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - }) - }) - }) - }) - }) - }) - describe('metaSafeBatchTransferFrom() (Meta) Function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock - - let transferData: string | null = 'Hello from the other side' - const initBalance = 100 - const amount = 10 - const nonce = BigNumber.from(0) - - // Parameters for balances - let ids: any[], amounts: any[] - const nTokenTypes = 33 - - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 - let feeToken: BigNumber - const feeTokenID = 666 - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - let transferObj: BatchTransferSignature - let gasReceipt: GasReceipt | null - let domainHash: string - let data: string - - const conditions = [ - [transferData, true, 'Gas receipt & transfer data'], - [null, true, 'Gas receipt w/o transfer data'], - [transferData, false, 'Transfer data w/o gas receipt '], - [null, false, 'No Gas receipt & No transfer data'] - ] - - conditions.forEach(function(condition) { - context(condition[2] as string, () => { - beforeEach(async () => { - // Get conditions - transferData = (await condition[0]) as string | null - isGasReceipt = (await condition[1]) as boolean - - // Deploy contracts - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - - // Mint tokens - ;(ids = []), (amounts = []) - - // Minting enough amounts for transfer for each types - for (let i = 0; i < nTokenTypes; i++) { - await erc1155Contract.mintMock(ownerAddress, i, initBalance, []) - ids.push(i) - amounts.push(amount) - } - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - - // Transfer Signature Object - transferObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - receiver: receiverAddress, - ids: ids.slice(0), - amounts: amounts.slice(0), - isGasFee: isGasReceipt, - transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), - nonce: nonce - } - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Domain hash - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - - // Data to pass in transfer method - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - }) + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + transferObj.isGasFee = true + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeTransferFrom(ownerAddress, receiverAddress, id, amount, true, data) + await expect(tx).to.be.rejectedWith(RevertError()) + }) + }) - it('should REVERT if contract address is incorrect', async () => { - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) - ) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + context('When successful transfer', () => { + let tx: ethers.ContractTransaction - it('should REVERT if signer address is incorrect', async () => { - transferObj.signerWallet = operatorWallet - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + beforeEach(async () => { + tx = await operatorERC1155Contract.metaSafeTransferFrom( + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data + ) + }) - it('should REVERT if receiver address is incorrect', async () => { - transferObj.receiver = ownerAddress - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + it('should correctly update balance of sender', async () => { + const balance = await erc1155Contract.balanceOf(ownerAddress, id) + expect(balance).to.be.eql(BigNumber.from(initBalance - amount)) + }) - it('should REVERT if token id is incorrect', async () => { - transferObj.ids[0] = 6 - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + it('should correctly update balance of receiver', async () => { + const balance = await erc1155Contract.balanceOf(receiverAddress, id) + expect(balance).to.be.eql(BigNumber.from(amount)) + }) - it('should REVERT if token amount is incorrect', async () => { - transferObj.amounts[0] = amount + 1 - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) + + it('should update gas token balance of sender', async () => { + const senderBalance = await erc1155Contract.balanceOf(ownerAddress, feeTokenID) + expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + }) + + it('should update gas token balance of executor', async () => { + const balance = await erc1155Contract.balanceOf(operatorAddress, feeTokenID) + expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + }) + }) - it('should REVERT if transfer data is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = ethers.utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.ids, - transferObj.amounts, - transferObj.nonce - ]) - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - let goodGasAndTransferData - let badGasAndTransferData - - // Correct and incorrect transferData - if (isGasReceipt) { - goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) - badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] - ) - } else { - goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [transferData]) - badGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) - } - - // Encode normally the whole thing - sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + describe('TransferSingle event', async () => { + let filterFromOperatorContract: ethers.EventFilter + + it('should emit TransferSingle event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferSingle') + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferSingle( + operatorContract.address, + null, + null, + null, + null + ) + + // Increment nonce because it's the second transfer + transferObj.nonce = nonce.add(1) + data = await encodeMetaTransferFromData(transferObj, domainHash, gasReceipt) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.metaSafeTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + id, + amount, + isGasReceipt, + data, + { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) + + it('should emit NonceChange event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) + + it('should have `_signer` as `signer` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) + }) + + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) + }) + }) + }) + }) }) + }) + }) - it('should REVERT if nonce is incorrect', async () => { - transferObj.nonce = nonce.add(101) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - transferObj.nonce = nonce - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSafeBatchTransferFrom(ownerAddress, receiverAddress, ids, amounts, isGasReceipt, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - }) + describe('metaSafeBatchTransferFrom() (Meta) Function', () => { + let receiverContract: ERC1155ReceiverMock + let operatorContract: ERC1155OperatorMock + + let transferData: string | null = 'Hello from the other side' + const initBalance = 100 + const amount = 10 + const nonce = BigNumber.from(0) + + // Parameters for balances + let ids: any[], amounts: any[] + const nTokenTypes = 33 + + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) + + const feeType = 0 + let feeToken: BigNumber + const feeTokenID = 666 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array + + let transferObj: BatchTransferSignature + let gasReceipt: GasReceipt | null + let domainHash: string + let data: string + + const conditions = [ + [transferData, true, 'Gas receipt & transfer data'], + [null, true, 'Gas receipt w/o transfer data'], + [transferData, false, 'Transfer data w/o gas receipt '], + [null, false, 'No Gas receipt & No transfer data'] + ] + + conditions.forEach(function(condition) { + context(condition[2] as string, () => { + beforeEach(async () => { + // Get conditions + transferData = (await condition[0]) as string | null + isGasReceipt = (await condition[1]) as boolean + + // Deploy contracts + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + + // Mint tokens + ;(ids = []), (amounts = []) + + // Minting enough amounts for transfer for each types + for (let i = 0; i < nTokenTypes; i++) { + await erc1155Contract.mintMock(ownerAddress, i, initBalance, []) + ids.push(i) + amounts.push(amount) + } - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.fulfilled - }) + feeTokenAddress = erc1155Contract.address - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] + ) - let erc1271WalletAddress + // Gas Receipt + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address - - await erc1155Contract.batchMintMock(erc1271WalletAddress, ids, amounts, []) - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) - }) + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + + // Transfer Signature Object + transferObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + receiver: receiverAddress, + ids: ids.slice(0), + amounts: amounts.slice(0), + isGasFee: isGasReceipt, + transferData: transferData === null ? null : utils.toUtf8Bytes(transferData), + nonce: nonce + } - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - }) + // Domain hash + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - transferObj.from = erc1271WalletAddress - transferObj.signerWallet = receiverWallet + // Data to pass in transfer method + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + }) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + it('should REVERT if contract address is incorrect', async () => { + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverContract.address]) + ) + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - it('should PASS if signature is valid', async () => { - transferObj.from = erc1271WalletAddress - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - erc1271WalletAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - await expect(tx).to.be.fulfilled - }) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - }) - describe('When signature is valid', () => { - it('should REVERT if insufficient balance', async () => { - transferObj.amounts[0] = initBalance + 1 - amounts[0] = initBalance + 1 + it('should REVERT if signer address is incorrect', async () => { + transferObj.signerWallet = operatorWallet data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( @@ -1435,31 +1228,30 @@ describe('ERC1155MetaPackedBalance', () => { ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if sending to 0x0', async () => { - transferObj.receiver = ZERO_ADDRESS + it('should REVERT if receiver address is incorrect', async () => { + transferObj.receiver = ownerAddress data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - ZERO_ADDRESS, + receiverAddress, ids, amounts, isGasReceipt, data ) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155MetaPackedBalance#metaSafeBatchTransferFrom: INVALID_RECIPIENT') - ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if transfer leads to overflow', async () => { - await operatorERC1155Contract.mintMock(receiverAddress, ids[0], MAXVAL, []) + it('should REVERT if token id is incorrect', async () => { + transferObj.ids[0] = 6 + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, @@ -1468,152 +1260,226 @@ describe('ERC1155MetaPackedBalance', () => { isGasReceipt, data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT when sending to non-receiver contract', async () => { - transferObj.receiver = erc1155Contract.address + it('should REVERT if token amount is incorrect', async () => { + transferObj.amounts[0] = amount + 1 data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - erc1155Contract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if invalid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - if (gasReceipt) { - gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit + it('should REVERT if transfer data is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] + + const signer = await transferObj.signerWallet.getAddress() + + // Packed encoding of transfer signature message + let sigData = ethers.utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.ids, + transferObj.amounts, + transferObj.nonce + ]) + + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData + let goodGasAndTransferData + let badGasAndTransferData + + // Correct and incorrect transferData + if (isGasReceipt) { + goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode([GasReceiptType, 'bytes'], [gasReceipt, transferData]) + badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, utils.toUtf8Bytes('Goodbyebyebye')] + ) + } else { + goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [transferData]) + badGasAndTransferData = ethers.utils.defaultAbiCoder.encode(['bytes'], [utils.toUtf8Bytes('Goodbyebyebye')]) } - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - // Force invalid response - await receiverContract.setShouldReject(true) + // Encode normally the whole thing + sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) + + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce + + // PASS BAD DATA + data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') + data ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if valid response from receiver contract', async () => { - transferObj.receiver = receiverContract.address - if (gasReceipt) { - gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit - } + it('should REVERT if nonce is incorrect', async () => { + transferObj.nonce = nonce.add(101) data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) + + // Correct nonce + transferObj.nonce = nonce + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSafeBatchTransferFrom(ownerAddress, receiverAddress, ids, amounts, isGasReceipt, data) + + // Nonce lower + const tx2 = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) + }) + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) await expect(tx).to.be.fulfilled }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract + + let erc1271WalletAddress + + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.batchMintMock(erc1271WalletAddress, ids, amounts, []) + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + }) + + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) }) - it('should send gas fee to msg.sender if fee recipient ix 0x0', async () => { - gasReceipt!.feeRecipient = ZERO_ADDRESS + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + transferObj.from = erc1271WalletAddress + transferObj.signerWallet = receiverWallet + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should PASS if signature is valid', async () => { + transferObj.from = erc1271WalletAddress + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + erc1271WalletAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.fulfilled + }) + }) + }) + describe('When signature is valid', () => { + it('should REVERT if insufficient balance', async () => { + transferObj.amounts[0] = initBalance + 1 + amounts[0] = initBalance + 1 data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - await receiverERC1155Contract.metaSafeBatchTransferFrom( + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data + data, + HIGH_GAS_LIMIT ) - - const receiverBalance = await operatorERC1155Contract.balanceOf(receiverAddress, feeTokenID) - - expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) }) - it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { - await receiverERC1155Contract.metaSafeBatchTransferFrom( + it('should REVERT if sending to 0x0', async () => { + transferObj.receiver = ZERO_ADDRESS + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + ZERO_ADDRESS, ids, amounts, isGasReceipt, data ) - const operatorBalance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) - - expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) - }) - - it('should REVERT if gasReceipt is incorrect', async () => { - const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] - const txDataTypes = ['bytes', 'bytes'] - - const signer = await transferObj.signerWallet.getAddress() - - // Packed encoding of transfer signature message - let sigData = ethers.utils.solidityPack(sigArgTypes, [ - transferObj.contractAddress, - signer, - transferObj.receiver, - transferObj.ids, - transferObj.amounts, - transferObj.nonce - ]) - - // Form bad gas receipt - const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } - - const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - - // Correct and incorrect transferData - const goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [gasReceipt, transferData] - ) - const badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( - [GasReceiptType, 'bytes'], - [badGasReceipt, transferData] + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155MetaPackedBalance#metaSafeBatchTransferFrom: INVALID_RECIPIENT') ) + }) - // Encode normally the whole thing - sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - - // Get signature - const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) - const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) - const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - - // PASS BAD DATA - data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - + it('should REVERT if transfer leads to overflow', async () => { + await operatorERC1155Contract.mintMock(receiverAddress, ids[0], MAXVAL, []) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, @@ -1622,613 +1488,735 @@ describe('ERC1155MetaPackedBalance', () => { isGasReceipt, data ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) }) - it('should REVERT if gas receipt is passed, but not claimed', async () => { + it('should REVERT when sending to non-receiver contract', async () => { + transferObj.receiver = erc1155Contract.address + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + erc1155Contract.address, ids, amounts, - false, - data + isGasReceipt, + data, + HIGH_GAS_LIMIT ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) }) - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - transferObj.isGasFee = false + it('should REVERT if invalid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address + if (gasReceipt) { + gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit + } data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + // Force invalid response + await receiverContract.setShouldReject(true) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, ids, amounts, - true, - data + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - transferObj.isGasFee = false + it('should PASS if valid response from receiver contract', async () => { + transferObj.receiver = receiverContract.address + if (gasReceipt) { + gasReceipt.gasLimitCallback = HIGH_GAS_LIMIT.gasLimit + } data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverAddress, + receiverContract.address, ids, amounts, - false, - data + isGasReceipt, + data, + HIGH_GAS_LIMIT ) + await expect(tx).to.be.fulfilled }) - describe('When receiver is a contract', () => { - it('should REVERT if gas used in onERC1155BatchReceived exceeds limit', async () => { - const lowGasLimit = 1000 - gasReceipt!.gasLimitCallback = lowGasLimit - transferObj.receiver = receiverContract.address - - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverContract.address, - ids, - amounts, - isGasReceipt, - data, - HIGH_GAS_LIMIT - ) - await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } }) - it('should PASS if gas used in onERC1155BatchReceived does not exceed limit', async () => { - const okGasLimit = 160000 - gasReceipt!.gasLimitCallback = okGasLimit - transferObj.receiver = receiverContract.address + it('should send gas fee to msg.sender if fee recipient ix 0x0', async () => { + gasReceipt!.feeRecipient = ZERO_ADDRESS data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + await receiverERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.fulfilled - }) - it('should PASS if gasLimit is higher than gas sent in transaction', async () => { - const highGasLimit = 3000000 - gasReceipt!.gasLimitCallback = highGasLimit - transferObj.receiver = receiverContract.address + const receiverBalance = await operatorERC1155Contract.balanceOf(receiverAddress, feeTokenID) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + expect(gasReceipt!.gasFee).to.be.eql(receiverBalance.toNumber()) + }) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + it('should send gas fee to specified fee recipient (if not 0x0), not tx.origin', async () => { + await receiverERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, - receiverContract.address, + receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT + data ) - await expect(tx).to.be.fulfilled + const operatorBalance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) + + expect(gasReceipt!.gasFee).to.be.eql(operatorBalance.toNumber()) }) - }) - }) - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[1]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if gasReceipt is incorrect', async () => { + const sigArgTypes = ['address', 'address', 'address', 'uint256[]', 'uint256[]', 'uint256'] + const txDataTypes = ['bytes', 'bytes'] - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - false, - data - ) - await expect(tx).to.be.fulfilled - }) + const signer = await transferObj.signerWallet.getAddress() - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - true, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + // Packed encoding of transfer signature message + let sigData = ethers.utils.solidityPack(sigArgTypes, [ + transferObj.contractAddress, + signer, + transferObj.receiver, + transferObj.ids, + transferObj.amounts, + transferObj.nonce + ]) - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - transferObj.isGasFee = true - data = await encodeMetaBatchTransferFromData(transferObj, domainHash) - const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - true, - data - ) - await expect(tx).to.be.rejectedWith(RevertError()) - }) - }) + // Form bad gas receipt + const badGasReceipt = { ...gasReceipt, gasPrice: 109284123 } - context('When successful transfer', () => { - let tx: ethers.ContractTransaction + const transferData = transferObj.transferData == null ? utils.toUtf8Bytes('') : transferObj.transferData - beforeEach(async () => { - tx = await operatorERC1155Contract.metaSafeBatchTransferFrom( - ownerAddress, - receiverAddress, - ids, - amounts, - isGasReceipt, - data - ) - }) + // Correct and incorrect transferData + const goodGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [gasReceipt, transferData] + ) + const badGasAndTransferData = ethers.utils.defaultAbiCoder.encode( + [GasReceiptType, 'bytes'], + [badGasReceipt, transferData] + ) - it('should correctly update balance of sender and receiver', async () => { - let balanceFrom: BigNumber - let balanceTo: BigNumber + // Encode normally the whole thing + sigData = ethers.utils.solidityPack(['bytes', 'bytes'], [sigData, goodGasAndTransferData]) - for (let i = 0; i < ids.length; i++) { - balanceFrom = await operatorERC1155Contract.balanceOf(ownerAddress, ids[i]) - balanceTo = await operatorERC1155Contract.balanceOf(receiverAddress, ids[i]) + // Get signature + const sig = (await ethSign(transferObj.signerWallet, sigData)).slice(0, -2) + const paddedNonce = ethers.utils.solidityPack(['uint256'], [transferObj.nonce]) + const ethsig_nonce = sig + paddedNonce.slice(2) + '02' // encode packed the nonce - expect(balanceFrom).to.be.eql(BigNumber.from(initBalance - amounts[i])) - expect(balanceTo).to.be.eql(BigNumber.from(amounts[i])) - } - }) + // PASS BAD DATA + data = ethers.utils.defaultAbiCoder.encode(txDataTypes, [ethsig_nonce, badGasAndTransferData]) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[1]) { - this.test!.parent!.pending = true - this.skip() - } + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should update gas token balance of sender', async () => { - const senderBalance = await operatorERC1155Contract.balanceOf(ownerAddress, feeTokenID) - expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should update gas token balance of executor', async () => { - const balance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) - expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + transferObj.isGasFee = false + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - }) - describe('TransferBatch event', async () => { - let filterFromOperatorContract: ethers.EventFilter - let operatorContract: ERC1155OperatorMock + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + transferObj.isGasFee = false + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) - beforeEach(async () => { - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.fulfilled }) - it('should emit 1 TransferBatch events of N transfers', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![1] - expect(ev.event).to.be.eql('TransferBatch') + describe('When receiver is a contract', () => { + it('should REVERT if gas used in onERC1155BatchReceived exceeds limit', async () => { + const lowGasLimit = 1000 + gasReceipt!.gasLimitCallback = lowGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.rejectedWith(RevertOutOfGasError()) + }) + + it('should PASS if gas used in onERC1155BatchReceived does not exceed limit', async () => { + const okGasLimit = 160000 + gasReceipt!.gasLimitCallback = okGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) + + it('should PASS if gasLimit is higher than gas sent in transaction', async () => { + const highGasLimit = 3000000 + gasReceipt!.gasLimitCallback = highGasLimit + transferObj.receiver = receiverContract.address + + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverContract.address, + ids, + amounts, + isGasReceipt, + data, + HIGH_GAS_LIMIT + ) + await expect(tx).to.be.fulfilled + }) + }) + }) - const args = ev.args! as any - expect(args._ids.length).to.be.eql(ids.length) + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[1]) { + this.test!.parent!.pending = true + this.skip() + } }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + false, + data + ) + await expect(tx).to.be.fulfilled + }) - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferBatch( - operatorContract.address, - null, - null, - null, - null + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + transferObj.isGasFee = true + data = await encodeMetaBatchTransferFromData(transferObj, domainHash) + const tx = operatorERC1155Contract.metaSafeBatchTransferFrom( + ownerAddress, + receiverAddress, + ids, + amounts, + true, + data ) + await expect(tx).to.be.rejectedWith(RevertError()) + }) + }) - //Increment nonce because it's the second transfer - transferObj.nonce = nonce.add(1) - data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + context('When successful transfer', () => { + let tx: ethers.ContractTransaction - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.metaSafeBatchTransferFrom( - erc1155Contract.address, + beforeEach(async () => { + tx = await operatorERC1155Contract.metaSafeBatchTransferFrom( ownerAddress, receiverAddress, ids, amounts, isGasReceipt, - data, - HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION - ) - - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics + data ) - - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) }) - it('should emit NonceChange event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') - }) + it('should correctly update balance of sender and receiver', async () => { + let balanceFrom: BigNumber + let balanceTo: BigNumber - it('should have `_signer` as `signer` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + for (let i = 0; i < ids.length; i++) { + balanceFrom = await operatorERC1155Contract.balanceOf(ownerAddress, ids[i]) + balanceTo = await operatorERC1155Contract.balanceOf(receiverAddress, ids[i]) - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) + expect(balanceFrom).to.be.eql(BigNumber.from(initBalance - amounts[i])) + expect(balanceTo).to.be.eql(BigNumber.from(amounts[i])) + } }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events![0] + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[1]) { + this.test!.parent!.pending = true + this.skip() + } + }) + + it('should update gas token balance of sender', async () => { + const senderBalance = await operatorERC1155Contract.balanceOf(ownerAddress, feeTokenID) + expect(senderBalance).to.be.eql(feeTokenInitBalance.sub(gasReceipt!.gasFee)) + }) + + it('should update gas token balance of executor', async () => { + const balance = await operatorERC1155Contract.balanceOf(operatorAddress, feeTokenID) + expect(gasReceipt!.gasFee).to.be.eql(balance.toNumber()) + }) + }) - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) + describe('TransferBatch event', async () => { + let filterFromOperatorContract: ethers.EventFilter + let operatorContract: ERC1155OperatorMock + + beforeEach(async () => { + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + }) + + it('should emit 1 TransferBatch events of N transfers', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![1] + expect(ev.event).to.be.eql('TransferBatch') + + const args = ev.args! as any + expect(args._ids.length).to.be.eql(ids.length) + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferBatch( + operatorContract.address, + null, + null, + null, + null + ) + + //Increment nonce because it's the second transfer + transferObj.nonce = nonce.add(1) + data = await encodeMetaBatchTransferFromData(transferObj, domainHash, gasReceipt) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.metaSafeBatchTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + ids, + amounts, + isGasReceipt, + data, + HIGH_GAS_LIMIT // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) + + it('should emit NonceChange event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) + + it('should have `_signer` as `signer` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) + }) + + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events![0] + + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) + }) }) }) }) }) }) }) - }) - describe('metaSetApprovalForAll() function', () => { - const initBalance = 100 - let isGasReimbursed = true - const approved = true - const nonce = BigNumber.from(0) - const id = 66 - - let approvalObj: ApprovalSignature - let gasReceipt: GasReceipt | null - let domainHash: string - let data: string - - let isGasReceipt: boolean = true - const feeTokenInitBalance = BigNumber.from(100000000) - - const feeType = 0 - const feeTokenID = 666 - let feeTokenAddress: string - let feeTokenDataERC1155: string | Uint8Array - - const conditions = [ - [true, 'Gas receipt'], - [false, 'No Gas receipt'] - ] - - conditions.forEach(function(condition) { - context(condition[1] as string, () => { - beforeEach(async () => { - isGasReceipt = condition[0] as boolean - - feeTokenAddress = erc1155Contract.address - - feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint8'], - [feeTokenAddress, feeTokenID, feeType] - ) - - // Gas Receipt - gasReceipt = { - gasLimitCallback: 130000, - gasFee: 30000, - feeRecipient: operatorAddress, - feeTokenData: feeTokenDataERC1155 - } - - // Check if gas receipt is included - gasReceipt = isGasReceipt ? gasReceipt : null - isGasReimbursed = isGasReceipt ? true : false - - // Approval Signture Object - approvalObj = { - contractAddress: erc1155Contract.address, - signerWallet: ownerWallet, - operator: operatorAddress, - approved: approved, - isGasFee: isGasReceipt, - nonce: nonce - } - - // Mint tokens - await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - - // Mint tokens used to pay for gas - await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) - - // Domain hash - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) - ) - - // Data to pass in approval method - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - }) + describe('metaSetApprovalForAll() function', () => { + const initBalance = 100 + let isGasReimbursed = true + const approved = true + const nonce = BigNumber.from(0) + const id = 66 + + let approvalObj: ApprovalSignature + let gasReceipt: GasReceipt | null + let domainHash: string + let data: string + + let isGasReceipt: boolean = true + const feeTokenInitBalance = BigNumber.from(100000000) + + const feeType = 0 + const feeTokenID = 666 + let feeTokenAddress: string + let feeTokenDataERC1155: string | Uint8Array + + const conditions = [ + [true, 'Gas receipt'], + [false, 'No Gas receipt'] + ] + + conditions.forEach(function(condition) { + context(condition[1] as string, () => { + beforeEach(async () => { + isGasReceipt = condition[0] as boolean - it('should PASS if signature is valid', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.fulfilled - }) + feeTokenAddress = erc1155Contract.address + + feeTokenDataERC1155 = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint8'], + [feeTokenAddress, feeTokenID, feeType] + ) - describe('ERC-1271 Receiver', () => { - let erc1271WalletValidationMockContract: ERC1271WalletValidationMock - let ERC1271WalletValidationMockAbstract: AbstractContract + // Gas Receipt + gasReceipt = { + gasLimitCallback: 130000, + gasFee: 30000, + feeRecipient: operatorAddress, + feeTokenData: feeTokenDataERC1155 + } - let erc1271WalletAddress + // Check if gas receipt is included + gasReceipt = isGasReceipt ? gasReceipt : null + isGasReimbursed = isGasReceipt ? true : false + + // Approval Signture Object + approvalObj = { + contractAddress: erc1155Contract.address, + signerWallet: ownerWallet, + operator: operatorAddress, + approved: approved, + isGasFee: isGasReceipt, + nonce: nonce + } - beforeEach(async () => { - ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') - erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ - domainHash - ])) as ERC1271WalletValidationMock - erc1271WalletAddress = erc1271WalletValidationMockContract.address + // Mint tokens + await erc1155Contract.mintMock(ownerAddress, id, initBalance, []) - await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + // Mint tokens used to pay for gas + await erc1155Contract.mintMock(ownerAddress, feeTokenID, feeTokenInitBalance, []) + + // Domain hash + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, erc1155Contract.address]) + ) + + // Data to pass in approval method + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) }) - describe(`EIP-1271 (bytes) signatures (03)`, () => { - it('should return REVERT if signature is invalid', async () => { - approvalObj.owner = erc1271WalletAddress - approvalObj.signerWallet = receiverWallet + it('should PASS if signature is valid', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.fulfilled + }) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '03') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + describe('ERC-1271 Receiver', () => { + let erc1271WalletValidationMockContract: ERC1271WalletValidationMock + let ERC1271WalletValidationMockAbstract: AbstractContract + + let erc1271WalletAddress + + beforeEach(async () => { + ERC1271WalletValidationMockAbstract = await AbstractContract.fromArtifactName('ERC1271WalletValidationMock') + erc1271WalletValidationMockContract = (await ERC1271WalletValidationMockAbstract.deploy(ownerWallet, [ + domainHash + ])) as ERC1271WalletValidationMock + erc1271WalletAddress = erc1271WalletValidationMockContract.address + + await erc1155Contract.mintMock(erc1271WalletAddress, feeTokenID, feeTokenInitBalance, []) + }) + + describe(`EIP-1271 (bytes) signatures (03)`, () => { + it('should return REVERT if signature is invalid', async () => { + approvalObj.owner = erc1271WalletAddress + approvalObj.signerWallet = receiverWallet + + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '03') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) + }) + + describe(`EIP-1271 (bytes32) signatures (04)`, () => { + it('should return REVERT if signature is invalid', async () => { + approvalObj.owner = erc1271WalletAddress + approvalObj.signerWallet = receiverWallet + + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) + + it('should PASS if signature is valid', async () => { + approvalObj.owner = erc1271WalletAddress + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') + const tx = operatorERC1155Contract.metaSetApprovalForAll( + erc1271WalletAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.fulfilled + }) }) }) - describe(`EIP-1271 (bytes32) signatures (04)`, () => { - it('should return REVERT if signature is invalid', async () => { - approvalObj.owner = erc1271WalletAddress - approvalObj.signerWallet = receiverWallet + describe('When gas is reimbursed', () => { + before(async function() { + if (!condition[0]) { + this.test!.parent!.pending = true + this.skip() + } + }) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) + it('should REVERT if gas receipt is passed, but not claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should PASS if signature is valid', async () => { - approvalObj.owner = erc1271WalletAddress - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt, '04') - const tx = operatorERC1155Contract.metaSetApprovalForAll( - erc1271WalletAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx).to.be.fulfilled + it('should REVERT if gas receipt is passed but isGasFee is false', async () => { + approvalObj.isGasFee = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - }) - }) - describe('When gas is reimbursed', () => { - before(async function() { - if (!condition[0]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { + approvalObj.isGasFee = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - it('should REVERT if gas receipt is passed, but not claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) + await expect(tx).to.be.fulfilled + }) }) - it('should REVERT if gas receipt is passed but isGasFee is false', async () => { - approvalObj.isGasFee = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + describe('When gas is NOT reimbursed', () => { + before(async function() { + if (condition[0]) { + this.test!.parent!.pending = true + this.skip() + } + }) - it('should PASS if gas receipt is passed with isGasFee to false and not claimed', async () => { - approvalObj.isGasFee = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + it('should PASS if gas receipt is not passed and not claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) + await expect(tx).to.be.fulfilled + }) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.fulfilled - }) - }) + it('should REVER if gas receipt is not passed and claimed', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - describe('When gas is NOT reimbursed', () => { - before(async function() { - if (condition[0]) { - this.test!.parent!.pending = true - this.skip() - } - }) + it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { + approvalObj.isGasFee = true + data = await encodeMetaApprovalData(approvalObj, domainHash) - it('should PASS if gas receipt is not passed and not claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, false, data) - await expect(tx).to.be.fulfilled + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + await expect(tx).to.be.rejectedWith(RevertError()) + }) }) - it('should REVER if gas receipt is not passed and claimed', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) + it('should REVERT if contract address is incorrect', async () => { + domainHash = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverAddress]) + ) + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - it('should REVERT if gas receipt is not passed but isGasFee is set to true and is claimed', async () => { - approvalObj.isGasFee = true - data = await encodeMetaApprovalData(approvalObj, domainHash) + it('should REVERT if operator address is incorrect', async () => { + approvalObj.operator = receiverAddress + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, true, data) - await expect(tx).to.be.rejectedWith(RevertError()) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) }) - }) - - it('should REVERT if contract address is incorrect', async () => { - domainHash = ethers.utils.keccak256( - ethers.utils.solidityPack(['bytes32', 'uint256'], [DOMAIN_SEPARATOR_TYPEHASH, receiverAddress]) - ) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if operator address is incorrect', async () => { - approvalObj.operator = receiverAddress - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) - - it('should REVERT if approved value is incorrect', async () => { - approvalObj.approved = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + it('should REVERT if approved value is incorrect', async () => { + approvalObj.approved = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) - }) + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_SIGNATURE')) + }) - it('should REVERT if nonce is incorrect', async () => { - approvalObj.nonce = nonce.add(101) - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - - // Nonce higher - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - - // Correct nonce - approvalObj.nonce = nonce - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - await operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - - // Nonce lower - const tx2 = operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - }) + it('should REVERT if nonce is incorrect', async () => { + approvalObj.nonce = nonce.add(101) + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) - it('should emit an ApprovalForAll event', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - - expect(receipt.events![1].event).to.be.eql('ApprovalForAll') - }) + // Nonce higher + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) - it('should set the operator status to _status argument', async () => { - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - await expect(tx).to.be.fulfilled + // Correct nonce + approvalObj.nonce = nonce + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + await operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) - }) + // Nonce lower + const tx2 = operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155MetaPackedBalance#_signatureValidation: INVALID_NONCE')) + }) - it('should emit NonceChange event', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('NonceChange') - }) + it('should emit an ApprovalForAll event', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + const receipt = await tx.wait(1) - it('should have `_signer` as `signer` in NonceChange', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) + expect(receipt.events![1].event).to.be.eql('ApprovalForAll') + }) - const receipt = await tx.wait(1) - const ev = receipt.events![0] + it('should set the operator status to _status argument', async () => { + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, approved, isGasReimbursed, data) + await expect(tx).to.be.fulfilled - const args = ev.args! as any - expect(args.signer).to.be.eql(ownerWallet.address) - }) + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) - it('should have `nonce` as `nonce + 1` in NonceChange', async () => { - const tx = await operatorERC1155Contract.metaSetApprovalForAll( - ownerAddress, - operatorAddress, - approved, - isGasReimbursed, - data - ) - const receipt = await tx.wait(1) - const ev = receipt.events![0] - - const args = ev.args! as any - expect(args.newNonce).to.be.eql(nonce.add(1)) - }) + it('should emit NonceChange event', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + const receipt = await tx.wait(1) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('NonceChange') + }) - context('When the operator was already an operator', () => { - beforeEach(async () => { + it('should have `_signer` as `signer` in NonceChange', async () => { const tx = await operatorERC1155Contract.metaSetApprovalForAll( ownerAddress, operatorAddress, @@ -2237,35 +2225,68 @@ describe('ERC1155MetaPackedBalance', () => { data ) - // Update nonce of approval signature object for subsequent tests - approvalObj.nonce = nonce.add(1) - }) + const receipt = await tx.wait(1) + const ev = receipt.events![0] - it('should leave the operator status to set to true again', async () => { - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + const args = ev.args! as any + expect(args.signer).to.be.eql(ownerWallet.address) + }) - const tx = operatorERC1155Contract.metaSetApprovalForAll( + it('should have `nonce` as `nonce + 1` in NonceChange', async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( ownerAddress, operatorAddress, approved, isGasReimbursed, data ) - await expect(tx).to.be.fulfilled + const receipt = await tx.wait(1) + const ev = receipt.events![0] - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) + const args = ev.args! as any + expect(args.newNonce).to.be.eql(nonce.add(1)) }) - it('should allow the operator status to be set to false', async () => { - approvalObj.approved = false - data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + context('When the operator was already an operator', () => { + beforeEach(async () => { + const tx = await operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) - const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, false, isGasReimbursed, data) - await expect(tx).to.be.fulfilled + // Update nonce of approval signature object for subsequent tests + approvalObj.nonce = nonce.add(1) + }) - const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) - expect(status).to.be.eql(false) + it('should leave the operator status to set to true again', async () => { + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll( + ownerAddress, + operatorAddress, + approved, + isGasReimbursed, + data + ) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) + + it('should allow the operator status to be set to false', async () => { + approvalObj.approved = false + data = await encodeMetaApprovalData(approvalObj, domainHash, gasReceipt) + + const tx = operatorERC1155Contract.metaSetApprovalForAll(ownerAddress, operatorAddress, false, isGasReimbursed, data) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) + expect(status).to.be.eql(false) + }) }) }) }) diff --git a/tests/ERC1155Metadata.spec.ts b/tests/ERC1155Metadata.spec.ts index 2310b50..3771332 100644 --- a/tests/ERC1155Metadata.spec.ts +++ b/tests/ERC1155Metadata.spec.ts @@ -3,7 +3,7 @@ import { ethers } from 'ethers' import { AbstractContract, expect, RevertError, HIGH_GAS_LIMIT } from './utils' import * as utils from './utils' -import { ERC1155MetadataMock, ERC1155MetadataUpgradeableMockV2, ProxyUpgradeableDeployerMock, ProxyUpgradeable } from 'src' +import { ERC1155MetadataMock, ERC1155MetadataUpgradeableMockV2, ProxyUpgradeableDeployerMock } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' @@ -31,6 +31,9 @@ usingUpgradeable.forEach(upgradeable => { if (upgradeable) { abstract = await AbstractContract.fromArtifactName('ERC1155MetadataUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock } else { abstract = await AbstractContract.fromArtifactName('ERC1155MetadataMock') } @@ -40,10 +43,6 @@ usingUpgradeable.forEach(upgradeable => { if (upgradeable) { erc1155MetadataContract = (await abstract.deploy(ownerWallet, [])) as ERC1155MetadataMock - // Create factory - const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') - factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock - // Create proxy let tx = factoryContract.createProxy(erc1155MetadataContract.address, ethers.constants.HashZero, ownerWallet.address); await expect(tx).to.be.fulfilled diff --git a/tests/ERC1155MintBurn.spec.ts b/tests/ERC1155MintBurn.spec.ts index bb70143..e457d3f 100644 --- a/tests/ERC1155MintBurn.spec.ts +++ b/tests/ERC1155MintBurn.spec.ts @@ -1,450 +1,464 @@ -import { ethers } from 'ethers' +import { constants, ethers } from 'ethers' import { AbstractContract, expect, RevertError, BigNumber, RevertUnsafeMathError, HIGH_GAS_LIMIT } from './utils' import * as utils from './utils' -import { ERC1155MetaMintBurnMock, ERC1155ReceiverMock } from 'src' +import { ERC1155MetaMintBurnMock, ERC1155ReceiverMock, ProxyUpgradeableDeployerMock } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = utils.createTestWallet(web3, 1) - -const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = utils.createTestWallet(web3, 2) - -const { wallet: anyoneWallet, provider: anyoneProvider, signer: anyoneSigner } = utils.createTestWallet(web3, 3) - -const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = utils.createTestWallet(web3, 4) - -describe('ERC1155MintBurn', () => { - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - let ownerAddress: string - let receiverAddress: string - let anyoneAddress: string - let operatorAddress: string - - let erc1155MintBurnContract: ERC1155MetaMintBurnMock - let anyoneERC1155MintBurnContract: ERC1155MetaMintBurnMock - let receiverContract: ERC1155ReceiverMock - - context('When ERC1155MintBurn contract is deployed', () => { - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - anyoneAddress = await anyoneWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - }) - - beforeEach(async () => { - const abstractReceiver = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstractReceiver.deploy(ownerWallet)) as ERC1155ReceiverMock - - const abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') - erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock - anyoneERC1155MintBurnContract = (await erc1155MintBurnContract.connect(anyoneSigner)) as ERC1155MetaMintBurnMock - }) - - describe('_mint() function', () => { - const tokenID = 666 - const amount = 11 - - it('should ALLOW inheriting contract to call _mint()', async () => { - const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - await expect(tx).to.be.fulfilled +const { wallet: ownerWallet, provider: ownerProvider } = utils.createTestWallet(web3, 1) +const { wallet: receiverWallet } = utils.createTestWallet(web3, 2) +const { wallet: anyoneWallet, signer: anyoneSigner } = utils.createTestWallet(web3, 3) + +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155MintBurn' + (upgradeable ? 'Upgradeable': ''), () => { + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let receiverAddress: string + + let abstract: AbstractContract + let erc1155MintBurnContract: ERC1155MetaMintBurnMock + let anyoneERC1155MintBurnContract: ERC1155MetaMintBurnMock + let receiverContract: ERC1155ReceiverMock + let factoryContract: ProxyUpgradeableDeployerMock + + context('When ERC1155MintBurn contract is deployed', () => { + before(async () => { + receiverAddress = await receiverWallet.getAddress() + if (upgradeable) { + abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + } else { + abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnMock') + } }) - it('should NOT allow anyone to call _mint()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x7776afa0000000000000000000000000b87213121fb89cbd8b877cb1bb3ff84dd2869cfa' + - '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + - '00000000000000000000000000000000000000000000000b' + beforeEach(async () => { + const abstractReceiver = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstractReceiver.deploy(ownerWallet)) as ERC1155ReceiverMock + + if (upgradeable) { + erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnMock + // Create proxy + let tx = factoryContract.createProxy(erc1155MintBurnContract.address, constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(erc1155MintBurnContract.address, constants.HashZero, ownerWallet.address); + erc1155MintBurnContract = (await abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnMock; + tx = erc1155MintBurnContract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled + } else { + erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnMock } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + anyoneERC1155MintBurnContract = (await erc1155MintBurnContract.connect(anyoneSigner)) as ERC1155MetaMintBurnMock }) - it('should increase the balance of receiver by the right amount', async () => { - const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + describe('_mint() function', () => { + const tokenID = 666 + const amount = 11 - const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + it('should ALLOW inheriting contract to call _mint()', async () => { + const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + await expect(tx).to.be.fulfilled + }) - expect(recipientBalanceB).to.be.eql(recipientBalanceA.add(amount)) - }) + it('should NOT allow anyone to call _mint()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x7776afa0000000000000000000000000b87213121fb89cbd8b877cb1bb3ff84dd2869cfa' + + '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + + '00000000000000000000000000000000000000000000000b' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should REVERT if added amount leads to overflow', async () => { - const val = BigNumber.from(2) - .pow(256) - .sub(1) - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, val, []) - const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, 1, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should increase the balance of receiver by the right amount', async () => { + const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155MintBurnContract.mintMock(erc1155MintBurnContract.address, tokenID, amount, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) + expect(recipientBalanceB).to.be.eql(recipientBalanceA.add(amount)) + }) - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) - }) + it('should REVERT if added amount leads to overflow', async () => { + const val = BigNumber.from(2) + .pow(256) + .sub(1) + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, val, []) + const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, 1, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - await expect(tx).to.be.fulfilled - }) + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155MintBurnContract.mintMock(erc1155MintBurnContract.address, tokenID, amount, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should pass if data is not null to receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) - // NOTE: typechain generates the wrong type for `bytes` type at this time - // see https://github.com/ethereum-ts/TypeChain/issues/123 - // @ts-ignore - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, data) - await expect(tx).to.be.fulfilled - }) + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) + }) + + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + await expect(tx).to.be.fulfilled + }) - it('should have balances updated before onERC1155Received is called', async () => { - const toPreBalance = await erc1155MintBurnContract.balanceOf(receiverContract.address, tokenID) + it('should pass if data is not null to receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) + // NOTE: typechain generates the wrong type for `bytes` type at this time + // see https://github.com/ethereum-ts/TypeChain/issues/123 + // @ts-ignore + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, data) + await expect(tx).to.be.fulfilled + }) - await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + it('should have balances updated before onERC1155Received is called', async () => { + const toPreBalance = await erc1155MintBurnContract.balanceOf(receiverContract.address, tokenID) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - expect(args._from).to.be.eql(ZERO_ADDRESS) - expect(args._to).to.be.eql(receiverContract.address) - expect(args._toBalance).to.be.eql(toPreBalance.add(amount)) - }) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - it('should have TransferSingle event emitted before onERC1155Received is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - const receipt = await tx.wait(1) + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(receiverContract.address) + expect(args._toBalance).to.be.eql(toPreBalance.add(amount)) + }) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + it('should have TransferSingle event emitted before onERC1155Received is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + const receipt = await tx.wait(1) - expect(firstEventTopic).to.be.equal( - erc1155MintBurnContract.interface.getEventTopic( - erc1155MintBurnContract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155MintBurnContract.interface.getEventTopic( + erc1155MintBurnContract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + ) ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + ) ) - ) - }) + }) - it('should emit a Transfer event', async () => { - const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - const receipt = await tx.wait(1) + it('should emit a Transfer event', async () => { + const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferSingle') - }) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferSingle') + }) - it('should have 0x0 as `from` argument in Transfer event', async () => { - const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - const receipt = await tx.wait(1) + it('should have 0x0 as `from` argument in Transfer event', async () => { + const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + const receipt = await tx.wait(1) - // TODO: this form can be improved eventually as ethers improves its api - // or we write a wrapper function to parse the tx - const ev = receipt.events![0] - const args = ev.args! as any + // TODO: this form can be improved eventually as ethers improves its api + // or we write a wrapper function to parse the tx + const ev = receipt.events![0] + const args = ev.args! as any - expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._from).to.be.eql(ZERO_ADDRESS) + }) }) - }) - - describe('_batchMint() function', () => { - const Ntypes = 32 - const amountToMint = 10 - const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) - const amountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToMint) - it('should ALLOW inheriting contract to call _batchMint()', async () => { - const req = erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - await expect(req).to.be.fulfilled - }) + describe('_batchMint() function', () => { + const Ntypes = 32 + const amountToMint = 10 + const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) + const amountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToMint) - it('should PASS if arrays are empty', async () => { - const tx = erc1155MintBurnContract.batchMintMock(receiverAddress, [], [], []) - await expect(tx).to.be.fulfilled - }) + it('should ALLOW inheriting contract to call _batchMint()', async () => { + const req = erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + await expect(req).to.be.fulfilled + }) - it('should NOT allow anyone to call _batchMint()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x2589aeae00000000000000000000000035ef07393b57464e93deb59175ff72e6499450cf' + - '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + - '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + - '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + - '00000000000000010000000000000000000000000000000000000000000000000000000000000002' + - '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + - '00000000000000000000000000000000000000000000000a00000000000000000000000000000000' + - '0000000000000000000000000000000a' - } + it('should PASS if arrays are empty', async () => { + const tx = erc1155MintBurnContract.batchMintMock(receiverAddress, [], [], []) + await expect(tx).to.be.fulfilled + }) - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + it('should NOT allow anyone to call _batchMint()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x2589aeae00000000000000000000000035ef07393b57464e93deb59175ff72e6499450cf' + + '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + + '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + + '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + + '00000000000000010000000000000000000000000000000000000000000000000000000000000002' + + '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + + '00000000000000000000000000000000000000000000000a00000000000000000000000000000000' + + '0000000000000000000000000000000a' + } + + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should increase the balances of receiver by the right amounts', async () => { - await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + it('should increase the balances of receiver by the right amounts', async () => { + await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - for (let i = 0; i < typesArray.length; i++) { - const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) - expect(balanceTo).to.be.eql(BigNumber.from(amountArray[i])) - } - }) + for (let i = 0; i < typesArray.length; i++) { + const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) + expect(balanceTo).to.be.eql(BigNumber.from(amountArray[i])) + } + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155MintBurnContract.batchMintMock(erc1155MintBurnContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155MintBurnContract.batchMintMock(erc1155MintBurnContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) }) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE')) }) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE')) - }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - it('should pass if data is not null from receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should pass if data is not null from receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - // TODO: remove ts-ignore when contract declaration is fixed - // @ts-ignore - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, data, { - gasLimit: 2000000 + // TODO: remove ts-ignore when contract declaration is fixed + // @ts-ignore + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, data, { + gasLimit: 2000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - - it('should have balances updated before onERC1155BatchReceived is called', async () => { - const toAddresses = Array(typesArray.length).fill(receiverContract.address) - const toPreBalances = await erc1155MintBurnContract.balanceOfBatch(toAddresses, typesArray) + it('should have balances updated before onERC1155BatchReceived is called', async () => { + const toAddresses = Array(typesArray.length).fill(receiverContract.address) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) + const toPreBalances = await erc1155MintBurnContract.balanceOfBatch(toAddresses, typesArray) - await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { gasLimit: 2000000 }) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { gasLimit: 2000000 }) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - expect(args._from).to.be.eql(ZERO_ADDRESS) - expect(args._to).to.be.eql(receiverContract.address) - for (let i = 0; i < typesArray.length; i++) { - expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(amountArray[i])) - } - }) + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) - it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(receiverContract.address) + for (let i = 0; i < typesArray.length; i++) { + expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(amountArray[i])) + } }) - const receipt = await tx.wait(1) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + const receipt = await tx.wait(1) + + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] - expect(firstEventTopic).to.be.equal( - erc1155MintBurnContract.interface.getEventTopic( - erc1155MintBurnContract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + expect(firstEventTopic).to.be.equal( + erc1155MintBurnContract.interface.getEventTopic( + erc1155MintBurnContract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + ) ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + ) ) - ) - }) + }) - it('should emit 1 Transfer events of N transfers', async () => { - const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - const receipt = await tx.wait() - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferBatch') + it('should emit 1 Transfer events of N transfers', async () => { + const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + const receipt = await tx.wait() + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(typesArray.length) - }) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(typesArray.length) + }) - it('should have 0x0 as `from` argument in Transfer events', async () => { - const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - const receipt = await tx.wait() - const args = receipt.events![0].args! as any - expect(args._from).to.be.eql(ZERO_ADDRESS) + it('should have 0x0 as `from` argument in Transfer events', async () => { + const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + const receipt = await tx.wait() + const args = receipt.events![0].args! as any + expect(args._from).to.be.eql(ZERO_ADDRESS) + }) }) - }) - describe('_burn() function', () => { - const tokenID = 666 - const initBalance = 100 - const amountToBurn = 10 + describe('_burn() function', () => { + const tokenID = 666 + const initBalance = 100 + const amountToBurn = 10 - beforeEach(async () => { - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, initBalance, []) - }) + beforeEach(async () => { + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, initBalance, []) + }) - it('should ALLOW inheriting contract to call _burn()', async () => { - const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - await expect(tx).to.be.fulfilled - }) + it('should ALLOW inheriting contract to call _burn()', async () => { + const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + await expect(tx).to.be.fulfilled + }) - it('should NOT allow anyone to call _burn()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x464a5ffb00000000000000000000000008970fed061e7747cd9a38d680a601510cb659fb' + - '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + - '00000000000000000000000000000000000000000000000a' - } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + it('should NOT allow anyone to call _burn()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x464a5ffb00000000000000000000000008970fed061e7747cd9a38d680a601510cb659fb' + + '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + + '00000000000000000000000000000000000000000000000a' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should decrease the balance of receiver by the right amount', async () => { - const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + it('should decrease the balance of receiver by the right amount', async () => { + const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - expect(recipientBalanceB).to.be.eql(recipientBalanceA.sub(amountToBurn)) - }) + expect(recipientBalanceB).to.be.eql(recipientBalanceA.sub(amountToBurn)) + }) - it('should REVERT if amount is hgher than balance', async () => { - const invalidVal = initBalance + 1 - const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, invalidVal, HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if amount is hgher than balance', async () => { + const invalidVal = initBalance + 1 + const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, invalidVal, HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should emit a Transfer event', async () => { - const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const receipt = await tx.wait(1) + it('should emit a Transfer event', async () => { + const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferSingle') - }) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferSingle') + }) - it('should have 0x0 as `to` argument in Transfer event', async () => { - const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const receipt = await tx.wait(1) + it('should have 0x0 as `to` argument in Transfer event', async () => { + const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + const receipt = await tx.wait(1) - // TODO: this form can be improved eventually as ethers improves its api - // or we write a wrapper function to parse the tx - const ev = receipt.events![0] - const args = ev.args! as any + // TODO: this form can be improved eventually as ethers improves its api + // or we write a wrapper function to parse the tx + const ev = receipt.events![0] + const args = ev.args! as any - expect(args._to).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(ZERO_ADDRESS) + }) }) - }) - describe('batchBurn() function', () => { - const Ntypes = 32 - const initBalance = 100 - const amountToBurn = 30 - const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) - const burnAmountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToBurn) - const initBalanceArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, initBalance) + describe('batchBurn() function', () => { + const Ntypes = 32 + const initBalance = 100 + const amountToBurn = 30 + const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) + const burnAmountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToBurn) + const initBalanceArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, initBalance) - beforeEach(async () => { - await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, initBalanceArray, []) - }) + beforeEach(async () => { + await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, initBalanceArray, []) + }) - it('should ALLOW inheriting contract to call _batchBurn()', async () => { - const req = erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const tx = (await expect(req).to.be.fulfilled) as ethers.ContractTransaction - // const receipt = await tx.wait() - // console.log('Batch mint :' + receipt.gasUsed) - }) + it('should ALLOW inheriting contract to call _batchBurn()', async () => { + const req = erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const tx = (await expect(req).to.be.fulfilled) as ethers.ContractTransaction + // const receipt = await tx.wait() + // console.log('Batch mint :' + receipt.gasUsed) + }) - // Should call mock's fallback function - it('should NOT allow anyone to call _batchBurn()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0xb389c3bb000000000000000000000000dc04977a2078c8ffdf086d618d1f961b6c546222' + - '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + - '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + - '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + - '00000000000000010000000000000000000000000000000000000000000000000000000000000003' + - '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + - '00000000000000000000000000000000000000000000001e00000000000000000000000000000000' + - '0000000000000000000000000000001e' - } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) - }) + // Should call mock's fallback function + it('should NOT allow anyone to call _batchBurn()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0xb389c3bb000000000000000000000000dc04977a2078c8ffdf086d618d1f961b6c546222' + + '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + + '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + + '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + + '00000000000000010000000000000000000000000000000000000000000000000000000000000003' + + '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + + '00000000000000000000000000000000000000000000001e00000000000000000000000000000000' + + '0000000000000000000000000000001e' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnMock: INVALID_METHOD')) + }) - it('should decrease the balances of receiver by the right amounts', async () => { - await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + it('should decrease the balances of receiver by the right amounts', async () => { + await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - for (let i = 0; i < typesArray.length; i++) { - const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) - expect(balanceTo).to.be.eql(BigNumber.from(initBalance - burnAmountArray[i])) - } - }) + for (let i = 0; i < typesArray.length; i++) { + const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) + expect(balanceTo).to.be.eql(BigNumber.from(initBalance - burnAmountArray[i])) + } + }) - it('should emit 1 Transfer events of N transfers', async () => { - const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const receipt = await tx.wait() - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferBatch') + it('should emit 1 Transfer events of N transfers', async () => { + const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const receipt = await tx.wait() + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(typesArray.length) - }) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(typesArray.length) + }) - it('should have 0x0 as `to` argument in Transfer events', async () => { - const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const receipt = await tx.wait() - const args = receipt.events![0].args! as any - expect(args._to).to.be.eql(ZERO_ADDRESS) + it('should have 0x0 as `to` argument in Transfer events', async () => { + const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const receipt = await tx.wait() + const args = receipt.events![0].args! as any + expect(args._to).to.be.eql(ZERO_ADDRESS) + }) }) }) }) diff --git a/tests/ERC1155MintBurnPackedBalance.spec.ts b/tests/ERC1155MintBurnPackedBalance.spec.ts index b8bec9f..045d1c4 100644 --- a/tests/ERC1155MintBurnPackedBalance.spec.ts +++ b/tests/ERC1155MintBurnPackedBalance.spec.ts @@ -1,554 +1,574 @@ -import { ethers } from 'ethers' +import { constants, ethers } from 'ethers' import { AbstractContract, RevertError, expect, BigNumber, HIGH_GAS_LIMIT, RevertUnsafeMathError } from './utils' import * as utils from './utils' -import { ERC1155MetaMintBurnPackedBalanceMock, ERC1155ReceiverMock } from 'src' +import { ERC1155MetaMintBurnPackedBalanceMock, ERC1155ReceiverMock, ProxyUpgradeableDeployerMock } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = utils.createTestWallet(web3, 1) - -const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = utils.createTestWallet(web3, 2) - -const { wallet: anyoneWallet, provider: anyoneProvider, signer: anyoneSigner } = utils.createTestWallet(web3, 3) - -const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = utils.createTestWallet(web3, 4) - -describe('ERC1155MintBurnPackedBalance', () => { - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - let ownerAddress: string - let receiverAddress: string - let anyoneAddress: string - let operatorAddress: string - - let erc1155MintBurnContract: ERC1155MetaMintBurnPackedBalanceMock - let anyoneERC1155MintBurnContract: ERC1155MetaMintBurnPackedBalanceMock - let receiverContract: ERC1155ReceiverMock - - context('When ERC1155MintBurn contract is deployed', () => { - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - anyoneAddress = await anyoneWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - }) - - beforeEach(async () => { - const abstractReceiver = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstractReceiver.deploy(ownerWallet)) as ERC1155ReceiverMock - - const abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') - erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock - anyoneERC1155MintBurnContract = (await erc1155MintBurnContract.connect( - anyoneSigner - )) as ERC1155MetaMintBurnPackedBalanceMock - }) - - describe('_mint() function', () => { - const tokenID = 666 - const amount = 11 - - it('should ALLOW inheriting contract to call mint()', async () => { - const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - await expect(tx).to.be.fulfilled +const { wallet: ownerWallet, provider: ownerProvider } = utils.createTestWallet(web3, 1) +const { wallet: receiverWallet } = utils.createTestWallet(web3, 2) +const { wallet: anyoneWallet } = utils.createTestWallet(web3, 3) +const { wallet: operatorWallet } = utils.createTestWallet(web3, 4) + + +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155MintBurnPackedBalance' + (upgradeable ? 'Upgradeable': ''), () => { + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let ownerAddress: string + let receiverAddress: string + let anyoneAddress: string + let operatorAddress: string + + let abstract: AbstractContract + let erc1155MintBurnContract: ERC1155MetaMintBurnPackedBalanceMock + let receiverContract: ERC1155ReceiverMock + let factoryContract: ProxyUpgradeableDeployerMock + + context('When ERC1155MintBurn contract is deployed', () => { + before(async () => { + ownerAddress = await ownerWallet.getAddress() + receiverAddress = await receiverWallet.getAddress() + anyoneAddress = await anyoneWallet.getAddress() + operatorAddress = await operatorWallet.getAddress() + + if (upgradeable) { + abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + } else { + abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') + } }) - it('should NOT allow anyone to call _mint()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x7776afa0000000000000000000000000b87213121fb89cbd8b877cb1bb3ff84dd2869cfa' + - '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + - '00000000000000000000000000000000000000000000000b' + beforeEach(async () => { + const abstractReceiver = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstractReceiver.deploy(ownerWallet)) as ERC1155ReceiverMock + + if (upgradeable) { + erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnPackedBalanceMock + + // Create proxy + let tx = factoryContract.createProxy(erc1155MintBurnContract.address, constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(erc1155MintBurnContract.address, constants.HashZero, ownerWallet.address); + erc1155MintBurnContract = (await abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnPackedBalanceMock; + tx = erc1155MintBurnContract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled + } else { + erc1155MintBurnContract = (await abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) }) - it('should increase the balance of receiver by the right amount', async () => { - const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + describe('_mint() function', () => { + const tokenID = 666 + const amount = 11 - const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + it('should ALLOW inheriting contract to call mint()', async () => { + const tx = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + await expect(tx).to.be.fulfilled + }) - expect(recipientBalanceB).to.be.eql(recipientBalanceA.add(amount)) - }) + it('should NOT allow anyone to call _mint()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x7776afa0000000000000000000000000b87213121fb89cbd8b877cb1bb3ff84dd2869cfa' + + '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + + '00000000000000000000000000000000000000000000000b' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should REVERT if amount is larger than limit (overflow 1)', async () => { - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - const maxVal0 = BigNumber.from(2) - .pow(32) - .sub(amount) - const tx0 = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, maxVal0, []) - await expect(tx0).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + it('should increase the balance of receiver by the right amount', async () => { + const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - it('should REVERT if amount is larger than limit (invalid amount by 1)', async () => { - const maxVal = BigNumber.from(2).pow(32) - const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - it('should REVERT if amount is larger than limit (invalid amount min overflow)', async () => { - const maxVal = BigNumber.from(2).pow(32) - // Set balance to max acceptable value - await erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal.sub(1), []) - const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) - await expect(balance).to.be.eql(maxVal.sub(1)) - - // Value that overflows solidity, but result is < maxVal - // Minimum overflow - const maxVal2 = BigNumber.from(2) - .pow(256) - .sub(maxVal.sub(1)) - const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal2, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + expect(recipientBalanceB).to.be.eql(recipientBalanceA.add(amount)) + }) - it('should REVERT if amount is larger than limit (invalid amount max overflow)', async () => { - // Maximum overflow - const maxVal = BigNumber.from(2) - .pow(256) - .sub(1) - const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal, [], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + it('should REVERT if amount is larger than limit (overflow 1)', async () => { + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + const maxVal0 = BigNumber.from(2) + .pow(32) + .sub(amount) + const tx0 = erc1155MintBurnContract.mintMock(receiverAddress, tokenID, maxVal0, []) + await expect(tx0).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155MintBurnContract.mintMock(erc1155MintBurnContract.address, tokenID, amount, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) + it('should REVERT if amount is larger than limit (invalid amount by 1)', async () => { + const maxVal = BigNumber.from(2).pow(32) + const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) + it('should REVERT if amount is larger than limit (invalid amount min overflow)', async () => { + const maxVal = BigNumber.from(2).pow(32) + // Set balance to max acceptable value + await erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal.sub(1), []) + const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) + await expect(balance).to.be.eql(maxVal.sub(1)) + + // Value that overflows solidity, but result is < maxVal + // Minimum overflow + const maxVal2 = BigNumber.from(2) + .pow(256) + .sub(maxVal.sub(1)) + const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal2, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE') - ) - }) + it('should REVERT if amount is larger than limit (invalid amount max overflow)', async () => { + // Maximum overflow + const maxVal = BigNumber.from(2) + .pow(256) + .sub(1) + const tx = erc1155MintBurnContract.mintMock(anyoneWallet.address, 0, maxVal, [], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - await expect(tx).to.be.fulfilled - }) + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155MintBurnContract.mintMock(erc1155MintBurnContract.address, tokenID, amount, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should pass if data is not null to receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) - // NOTE: typechain generates the wrong type for `bytes` type at this time - // see https://github.com/ethereum-ts/TypeChain/issues/123 - // @ts-ignore - const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, data) - await expect(tx).to.be.fulfilled - }) + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE') + ) + }) - it('should have balances updated before onERC1155Received is called', async () => { - const toPreBalance = await erc1155MintBurnContract.balanceOf(receiverContract.address, tokenID) + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + await expect(tx).to.be.fulfilled + }) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) + it('should pass if data is not null to receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + // NOTE: typechain generates the wrong type for `bytes` type at this time + // see https://github.com/ethereum-ts/TypeChain/issues/123 + // @ts-ignore + const tx = erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, data) + await expect(tx).to.be.fulfilled + }) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + it('should have balances updated before onERC1155Received is called', async () => { + const toPreBalance = await erc1155MintBurnContract.balanceOf(receiverContract.address, tokenID) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) - expect(args._from).to.be.eql(ZERO_ADDRESS) - expect(args._to).to.be.eql(receiverContract.address) - expect(args._toBalance).to.be.eql(toPreBalance.add(amount)) - }) + await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - it('should have TransferSingle event emitted before onERC1155Received is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) - const receipt = await tx.wait(1) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(receiverContract.address) + expect(args._toBalance).to.be.eql(toPreBalance.add(amount)) + }) + + it('should have TransferSingle event emitted before onERC1155Received is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155MintBurnContract.mintMock(receiverContract.address, tokenID, amount, []) + const receipt = await tx.wait(1) - expect(firstEventTopic).to.be.equal( - erc1155MintBurnContract.interface.getEventTopic( - erc1155MintBurnContract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155MintBurnContract.interface.getEventTopic( + erc1155MintBurnContract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + ) ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + ) ) - ) - }) + }) - it('should emit a Transfer event', async () => { - const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - const receipt = await tx.wait(1) + it('should emit a Transfer event', async () => { + const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferSingle') - }) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferSingle') + }) - it('should have 0x0 as `from` argument in Transfer event', async () => { - const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) - const receipt = await tx.wait(1) + it('should have 0x0 as `from` argument in Transfer event', async () => { + const tx = await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, amount, []) + const receipt = await tx.wait(1) - // TODO: this form can be improved eventually as ethers improves its api - // or we write a wrapper function to parse the tx - const ev = receipt.events![0] - const args = ev.args! as any + // TODO: this form can be improved eventually as ethers improves its api + // or we write a wrapper function to parse the tx + const ev = receipt.events![0] + const args = ev.args! as any - expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._from).to.be.eql(ZERO_ADDRESS) + }) }) - }) - describe('_batchMint() function', () => { - const Ntypes = 123 - const amountToMint = 10 - const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) - const amountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToMint) + describe('_batchMint() function', () => { + const Ntypes = 123 + const amountToMint = 10 + const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) + const amountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToMint) - it('should ALLOW inheriting contract to call _batchMint()', async () => { - const req = erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - ;(await expect(req).to.be.fulfilled) as ethers.ContractTransaction - }) - - it('should PASS if arrays are empty', async () => { - const tx = erc1155MintBurnContract.batchMintMock(receiverAddress, [], [], []) - await expect(tx).to.be.fulfilled - }) + it('should ALLOW inheriting contract to call _batchMint()', async () => { + const req = erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + ;(await expect(req).to.be.fulfilled) as ethers.ContractTransaction + }) - it('should NOT allow anyone to call _batchMint()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x2589aeae00000000000000000000000035ef07393b57464e93deb59175ff72e6499450cf' + - '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + - '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + - '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + - '00000000000000010000000000000000000000000000000000000000000000000000000000000002' + - '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + - '00000000000000000000000000000000000000000000000a00000000000000000000000000000000' + - '0000000000000000000000000000000a' - } + it('should PASS if arrays are empty', async () => { + const tx = erc1155MintBurnContract.batchMintMock(receiverAddress, [], [], []) + await expect(tx).to.be.fulfilled + }) - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) + it('should NOT allow anyone to call _batchMint()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x2589aeae00000000000000000000000035ef07393b57464e93deb59175ff72e6499450cf' + + '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + + '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + + '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + + '00000000000000010000000000000000000000000000000000000000000000000000000000000002' + + '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + + '00000000000000000000000000000000000000000000000a00000000000000000000000000000000' + + '0000000000000000000000000000000a' + } + + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should increase the balances of receiver by the right amounts', async () => { - await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + it('should increase the balances of receiver by the right amounts', async () => { + await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - for (let i = 0; i < typesArray.length; i++) { - const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) - expect(balanceTo).to.be.eql(BigNumber.from(amountArray[i])) - } - }) + for (let i = 0; i < typesArray.length; i++) { + const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) + expect(balanceTo).to.be.eql(BigNumber.from(amountArray[i])) + } + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155MintBurnContract.batchMintMock(erc1155MintBurnContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155MintBurnContract.batchMintMock(erc1155MintBurnContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) }) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') + ) }) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') - ) - }) - it('should REVERT if amount is larger than limit (overflow 1)', async () => { - await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, [], { gasLimit: 2000000 }) - // Overflow by 1 - const maxVal0 = BigNumber.from(2) - .pow(32) - .sub(amountToMint) - const tx0 = erc1155MintBurnContract.batchMintMock( - receiverAddress, - [typesArray[0], typesArray[1]], - [maxVal0, amountArray[1]], - [] - ) - await expect(tx0).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + it('should REVERT if amount is larger than limit (overflow 1)', async () => { + await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, [], { gasLimit: 2000000 }) + // Overflow by 1 + const maxVal0 = BigNumber.from(2) + .pow(32) + .sub(amountToMint) + const tx0 = erc1155MintBurnContract.batchMintMock( + receiverAddress, + [typesArray[0], typesArray[1]], + [maxVal0, amountArray[1]], + [] + ) + await expect(tx0).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should REVERT if amount is larger than limit (invalid amount 1)', async () => { - const maxVal = BigNumber.from(2).pow(32) - const tx2 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0, 1], [maxVal, 1], []) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + it('should REVERT if amount is larger than limit (invalid amount 1)', async () => { + const maxVal = BigNumber.from(2).pow(32) + const tx2 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0, 1], [maxVal, 1], []) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should REVERT if amount is larger than limit (invalid amount min overflow)', async () => { - const maxVal = BigNumber.from(2).pow(32) - // Set balance to max acceptable value - await erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal.sub(1)], []) - const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) - await expect(balance).to.be.eql(maxVal.sub(1)) - - // Value that overflows solidity, but result is < maxVal - const maxVal2 = BigNumber.from(2) - .pow(256) - .sub(maxVal.sub(1)) - const tx3 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal2], [], HIGH_GAS_LIMIT) - await expect(tx3).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if amount is larger than limit (invalid amount min overflow)', async () => { + const maxVal = BigNumber.from(2).pow(32) + // Set balance to max acceptable value + await erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal.sub(1)], []) + const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) + await expect(balance).to.be.eql(maxVal.sub(1)) + + // Value that overflows solidity, but result is < maxVal + const maxVal2 = BigNumber.from(2) + .pow(256) + .sub(maxVal.sub(1)) + const tx3 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal2], [], HIGH_GAS_LIMIT) + await expect(tx3).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should REVERT if amount is larger than limit (invalid amount max overflow)', async () => { - const maxVal = BigNumber.from(2).pow(32) - // Set balance to max acceptable value - await erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal.sub(1)], []) - const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) - - await expect(balance).to.be.eql(maxVal.sub(1)) - const maxVal3 = BigNumber.from(2) - .pow(256) - .sub(1) - const tx4 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal3], [], HIGH_GAS_LIMIT) - await expect(tx4).to.be.rejectedWith(RevertUnsafeMathError()) - }) + it('should REVERT if amount is larger than limit (invalid amount max overflow)', async () => { + const maxVal = BigNumber.from(2).pow(32) + // Set balance to max acceptable value + await erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal.sub(1)], []) + const balance = await erc1155MintBurnContract.balanceOf(anyoneWallet.address, 0) + + await expect(balance).to.be.eql(maxVal.sub(1)) + const maxVal3 = BigNumber.from(2) + .pow(256) + .sub(1) + const tx4 = erc1155MintBurnContract.batchMintMock(anyoneWallet.address, [0], [maxVal3], [], HIGH_GAS_LIMIT) + await expect(tx4).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 6000000 + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 6000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - it('should pass if data is not null from receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should pass if data is not null from receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - // TODO: remove ts-ignore when contract declaration is fixed - // @ts-ignore - const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, data, { - gasLimit: 2000000 + // TODO: remove ts-ignore when contract declaration is fixed + // @ts-ignore + const tx = erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, data, { + gasLimit: 2000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - it('should have balances updated before onERC1155BatchReceived is called', async () => { - const toAddresses = Array(typesArray.length).fill(receiverContract.address) + it('should have balances updated before onERC1155BatchReceived is called', async () => { + const toAddresses = Array(typesArray.length).fill(receiverContract.address) - const toPreBalances = await erc1155MintBurnContract.balanceOfBatch(toAddresses, typesArray) + const toPreBalances = await erc1155MintBurnContract.balanceOfBatch(toAddresses, typesArray) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) - await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { gasLimit: 2000000 }) + await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { gasLimit: 2000000 }) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) - - expect(args._from).to.be.eql(ZERO_ADDRESS) - expect(args._to).to.be.eql(receiverContract.address) - for (let i = 0; i < typesArray.length; i++) { - expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(amountArray[i])) - } - }) + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) - it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { - gasLimit: 2000000 + expect(args._from).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(receiverContract.address) + for (let i = 0; i < typesArray.length; i++) { + expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(amountArray[i])) + } }) - const receipt = await tx.wait(1) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155MintBurnContract.batchMintMock(receiverContract.address, typesArray, amountArray, [], { + gasLimit: 2000000 + }) + const receipt = await tx.wait(1) - expect(firstEventTopic).to.be.equal( - erc1155MintBurnContract.interface.getEventTopic( - erc1155MintBurnContract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155MintBurnContract.interface.getEventTopic( + erc1155MintBurnContract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + ) ) - ) - expect(secondEventTopic).to.be.equal( - erc1155MintBurnContract.interface.getEventTopic( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + expect(secondEventTopic).to.be.equal( + erc1155MintBurnContract.interface.getEventTopic( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + ) ) - ) - }) + }) - it('should emit 1 Transfer events of N transfers', async () => { - const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - const receipt = await tx.wait() - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferBatch') + it('should emit 1 Transfer events of N transfers', async () => { + const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + const receipt = await tx.wait() + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(typesArray.length) - }) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(typesArray.length) + }) - it('should have 0x0 as `from` argument in Transfer events', async () => { - const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) - const receipt = await tx.wait() - const args = receipt.events![0].args! as any - expect(args._from).to.be.eql(ZERO_ADDRESS) + it('should have 0x0 as `from` argument in Transfer events', async () => { + const tx = await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, amountArray, []) + const receipt = await tx.wait() + const args = receipt.events![0].args! as any + expect(args._from).to.be.eql(ZERO_ADDRESS) + }) }) - }) - describe('_burn() function', () => { - const tokenID = 666 - const initBalance = 100 - const amountToBurn = 10 + describe('_burn() function', () => { + const tokenID = 666 + const initBalance = 100 + const amountToBurn = 10 - beforeEach(async () => { - await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, initBalance, []) - }) + beforeEach(async () => { + await erc1155MintBurnContract.mintMock(receiverAddress, tokenID, initBalance, []) + }) - it('should ALLOW inheriting contract to call _burn()', async () => { - const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - await expect(tx).to.be.fulfilled - }) + it('should ALLOW inheriting contract to call _burn()', async () => { + const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + await expect(tx).to.be.fulfilled + }) - it('should NOT allow anyone to call _burn()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0x464a5ffb00000000000000000000000008970fed061e7747cd9a38d680a601510cb659fb' + - '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + - '00000000000000000000000000000000000000000000000a' - } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) + it('should NOT allow anyone to call _burn()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0x464a5ffb00000000000000000000000008970fed061e7747cd9a38d680a601510cb659fb' + + '000000000000000000000000000000000000000000000000000000000000029a0000000000000000' + + '00000000000000000000000000000000000000000000000a' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should decrease the balance of receiver by the right amount', async () => { - const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + it('should decrease the balance of receiver by the right amount', async () => { + const recipientBalanceA = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - expect(recipientBalanceB).to.be.eql(recipientBalanceA.sub(amountToBurn)) - }) + const recipientBalanceB = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + expect(recipientBalanceB).to.be.eql(recipientBalanceA.sub(amountToBurn)) + }) - it('should REVERT if amount is higher than balance', async () => { - // Sanity check - const balance = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) - expect(balance).to.be.eql(BigNumber.from(initBalance)) + it('should REVERT if amount is higher than balance', async () => { + // Sanity check + const balance = await erc1155MintBurnContract.balanceOf(receiverAddress, tokenID) + expect(balance).to.be.eql(BigNumber.from(initBalance)) - // Invalid amount to burn that would cause underflow - const invalidVal = initBalance + 1 + // Invalid amount to burn that would cause underflow + const invalidVal = initBalance + 1 - const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, invalidVal, HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) - }) + const tx = erc1155MintBurnContract.burnMock(receiverAddress, tokenID, invalidVal, HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertUnsafeMathError()) + }) - it('should emit a Transfer event', async () => { - const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const receipt = await tx.wait(1) + it('should emit a Transfer event', async () => { + const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + const receipt = await tx.wait(1) - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferSingle') - }) + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferSingle') + }) - it('should have 0x0 as `to` argument in Transfer event', async () => { - const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) - const receipt = await tx.wait(1) + it('should have 0x0 as `to` argument in Transfer event', async () => { + const tx = await erc1155MintBurnContract.burnMock(receiverAddress, tokenID, amountToBurn) + const receipt = await tx.wait(1) - // TODO: this form can be improved eventually as ethers improves its api - // or we write a wrapper function to parse the tx - const ev = receipt.events![0] - const args = ev.args! as any + // TODO: this form can be improved eventually as ethers improves its api + // or we write a wrapper function to parse the tx + const ev = receipt.events![0] + const args = ev.args! as any - expect(args._to).to.be.eql(ZERO_ADDRESS) + expect(args._to).to.be.eql(ZERO_ADDRESS) + }) }) - }) - describe('_batchBurn() function', () => { - const Ntypes = 32 - const initBalance = 100 - const amountToBurn = 30 - const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) - const burnAmountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToBurn) - const initBalanceArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, initBalance) + describe('_batchBurn() function', () => { + const Ntypes = 32 + const initBalance = 100 + const amountToBurn = 30 + const typesArray = Array.apply(null, { length: Ntypes }).map(Number.call, Number) + const burnAmountArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, amountToBurn) + const initBalanceArray = Array.apply(null, Array(Ntypes)).map(Number.prototype.valueOf, initBalance) - beforeEach(async () => { - await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, initBalanceArray, []) - }) + beforeEach(async () => { + await erc1155MintBurnContract.batchMintMock(receiverAddress, typesArray, initBalanceArray, []) + }) - it('should ALLOW inheriting contract to call _batchBurn()', async () => { - const req = erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const tx = (await expect(req).to.be.fulfilled) as ethers.ContractTransaction - // const receipt = await tx.wait() - // console.log('Batch mint :' + receipt.gasUsed) - }) + it('should ALLOW inheriting contract to call _batchBurn()', async () => { + const req = erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const tx = (await expect(req).to.be.fulfilled) as ethers.ContractTransaction + // const receipt = await tx.wait() + // console.log('Batch mint :' + receipt.gasUsed) + }) - // Should call mock's fallback function - it('should NOT allow anyone to call _batchBurn()', async () => { - const transaction = { - to: erc1155MintBurnContract.address, - data: - '0xb389c3bb000000000000000000000000dc04977a2078c8ffdf086d618d1f961b6c546222' + - '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + - '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + - '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + - '00000000000000010000000000000000000000000000000000000000000000000000000000000003' + - '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + - '00000000000000000000000000000000000000000000001e00000000000000000000000000000000' + - '0000000000000000000000000000001e' - } - const tx = anyoneWallet.sendTransaction(transaction) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) + // Should call mock's fallback function + it('should NOT allow anyone to call _batchBurn()', async () => { + const transaction = { + to: erc1155MintBurnContract.address, + data: + '0xb389c3bb000000000000000000000000dc04977a2078c8ffdf086d618d1f961b6c546222' + + '00000000000000000000000000000000000000000000000000000000000000600000000000000000' + + '0000000000000000000000000000000000000000000000c000000000000000000000000000000000' + + '00000000000000000000000000000002000000000000000000000000000000000000000000000000' + + '00000000000000010000000000000000000000000000000000000000000000000000000000000003' + + '00000000000000000000000000000000000000000000000000000000000000020000000000000000' + + '00000000000000000000000000000000000000000000001e00000000000000000000000000000000' + + '0000000000000000000000000000001e' + } + const tx = anyoneWallet.sendTransaction(transaction) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should decrease the balances of receiver by the right amounts', async () => { - await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + it('should decrease the balances of receiver by the right amounts', async () => { + await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - for (let i = 0; i < typesArray.length; i++) { - const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) - expect(balanceTo).to.be.eql(BigNumber.from(initBalance - burnAmountArray[i])) - } - }) + for (let i = 0; i < typesArray.length; i++) { + const balanceTo = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[i]) + expect(balanceTo).to.be.eql(BigNumber.from(initBalance - burnAmountArray[i])) + } + }) - it('should REVERT if amount is higher than balance', async () => { - // Sanity check - const balance = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[0]) - expect(balance).to.be.eql(BigNumber.from(initBalance)) + it('should REVERT if amount is higher than balance', async () => { + // Sanity check + const balance = await erc1155MintBurnContract.balanceOf(receiverAddress, typesArray[0]) + expect(balance).to.be.eql(BigNumber.from(initBalance)) - // Invalid amount to burn that would cause underflow - const invalidVal = initBalance + 1 + // Invalid amount to burn that would cause underflow + const invalidVal = initBalance + 1 - const tx = erc1155MintBurnContract.batchBurnMock(receiverAddress, [typesArray[0]], [invalidVal], HIGH_GAS_LIMIT) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) - }) + const tx = erc1155MintBurnContract.batchBurnMock(receiverAddress, [typesArray[0]], [invalidVal], HIGH_GAS_LIMIT) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) + }) - it('should emit 1 Transfer events of N transfers', async () => { - const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const receipt = await tx.wait() - const ev = receipt.events![0] - expect(ev.event).to.be.eql('TransferBatch') + it('should emit 1 Transfer events of N transfers', async () => { + const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const receipt = await tx.wait() + const ev = receipt.events![0] + expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(typesArray.length) - }) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(typesArray.length) + }) - it('should have 0x0 as `to` argument in Transfer events', async () => { - const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) - const receipt = await tx.wait() - const args = receipt.events![0].args! as any - expect(args._to).to.be.eql(ZERO_ADDRESS) + it('should have 0x0 as `to` argument in Transfer events', async () => { + const tx = await erc1155MintBurnContract.batchBurnMock(receiverAddress, typesArray, burnAmountArray) + const receipt = await tx.wait() + const args = receipt.events![0].args! as any + expect(args._to).to.be.eql(ZERO_ADDRESS) + }) }) }) }) diff --git a/tests/ERC1155PackedBalance.spec.ts b/tests/ERC1155PackedBalance.spec.ts index 0d944a0..b4af4c7 100644 --- a/tests/ERC1155PackedBalance.spec.ts +++ b/tests/ERC1155PackedBalance.spec.ts @@ -1,643 +1,652 @@ -import { ethers } from 'ethers' +import { constants, ethers } from 'ethers' import { AbstractContract, expect, BigNumber, RevertError } from './utils' import * as utils from './utils' -import { ERC1155MetaMintBurnPackedBalanceMock, ERC1155ReceiverMock, ERC1155OperatorMock } from 'src' +import { ERC1155MetaMintBurnPackedBalanceMock, ERC1155ReceiverMock, ERC1155OperatorMock, ProxyUpgradeableDeployerMock } from 'src' // init test wallets from package.json mnemonic import { web3 } from 'hardhat' -const { wallet: ownerWallet, provider: ownerProvider, signer: ownerSigner } = utils.createTestWallet(web3, 1) - -const { wallet: receiverWallet, provider: receiverProvider, signer: receiverSigner } = utils.createTestWallet(web3, 2) - +const { wallet: ownerWallet, provider: ownerProvider } = utils.createTestWallet(web3, 1) +const { wallet: receiverWallet } = utils.createTestWallet(web3, 2) const { wallet: operatorWallet, provider: operatorProvider, signer: operatorSigner } = utils.createTestWallet(web3, 4) -describe('ERC1155PackedBalance', () => { - const LARGEVAL = BigNumber.from(2) - .pow(256) - .sub(2) // 2**256 - 2 - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - const NAME = 'MyERC1155' - const METADATA_URI = 'https://example.com/' - - let ownerAddress: string - let receiverAddress: string - let operatorAddress: string - let erc1155Abstract: AbstractContract - let operatorAbstract: AbstractContract - - let erc1155Contract: ERC1155MetaMintBurnPackedBalanceMock - let operatorERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock - - // load contract abi and deploy to test server - before(async () => { - ownerAddress = await ownerWallet.getAddress() - receiverAddress = await receiverWallet.getAddress() - operatorAddress = await operatorWallet.getAddress() - - erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') - operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') - }) - - // deploy before each test, to reset state of contract - beforeEach(async () => { - erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock - operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnPackedBalanceMock - }) - - describe('Bitwise functions', () => { - it('getValueInBin should return expected balance for given types', async () => { - const expected = BigNumber.from(2) - .pow(32) - .sub(2) // 2**32-2 - const balance = await erc1155Contract.getValueInBin(LARGEVAL.toString(), 0) - expect(balance).to.be.eql(expected) - }) - - // it('viewUpdateIDBalance should revert if overflow', async () => { - // let targetVal = 666 - // let tx = erc1155Contract.viewUpdateIDBalance() - // await expect(tx).to.be.rejectedWith( RevertError("ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR") ) - // }) - - // it('writeValueInBin should throw if value is above 2**32-1', async () => { - // let targetVal = BigNumber.from(2).pow(32) - // let writtenBin = erc1155Contract.writeValueInBin(LARGEVAL.toString(), 0, targetVal.toString()) - // await expect(writtenBin).to.be.rejected - // }) - - it('getIDBinIndex should return the correct bin and respective index', async () => { - const { bin: bin0, index: index0 } = await erc1155Contract.getIDBinIndex(0) - expect(bin0).to.be.eql(BigNumber.from(0)) - expect(index0).to.be.eql(BigNumber.from(0)) - - const { bin: bin3, index: index3 } = await erc1155Contract.getIDBinIndex(3) - expect(bin3).to.be.eql(BigNumber.from(0)) - expect(index3).to.be.eql(BigNumber.from(3)) - - const { bin: bin9, index: index9 } = await erc1155Contract.getIDBinIndex(8) - expect(bin9).to.be.eql(BigNumber.from(1)) - expect(index9).to.be.eql(BigNumber.from(0)) - - const { bin: bin15, index: index15 } = await erc1155Contract.getIDBinIndex(15) - expect(bin15).to.be.eql(BigNumber.from(1)) - expect(index15).to.be.eql(BigNumber.from(7)) +const usingUpgradeable = [false, true] + +usingUpgradeable.forEach(upgradeable => { + describe('ERC1155PackedBalance' + (upgradeable ? 'Upgradeable': ''), () => { + + const LARGEVAL = BigNumber.from(2) + .pow(256) + .sub(2) // 2**256 - 2 + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + const NAME = 'MyERC1155' + const METADATA_URI = 'https://example.com/' + + let ownerAddress: string + let receiverAddress: string + let operatorAddress: string + let erc1155Abstract: AbstractContract + let operatorAbstract: AbstractContract + + let erc1155Contract: ERC1155MetaMintBurnPackedBalanceMock + let operatorERC1155Contract: ERC1155MetaMintBurnPackedBalanceMock + let factoryContract: ProxyUpgradeableDeployerMock + + // load contract abi and deploy to test server + before(async () => { + ownerAddress = await ownerWallet.getAddress() + receiverAddress = await receiverWallet.getAddress() + operatorAddress = await operatorWallet.getAddress() + + if (upgradeable) { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceUpgradeableMock') + // Create factory + const factoryAbstract = await AbstractContract.fromArtifactName('ProxyUpgradeableDeployerMock') + factoryContract = (await factoryAbstract.deploy(ownerWallet, [])) as ProxyUpgradeableDeployerMock + } else { + erc1155Abstract = await AbstractContract.fromArtifactName('ERC1155MetaMintBurnPackedBalanceMock') + } + operatorAbstract = await AbstractContract.fromArtifactName('ERC1155OperatorMock') }) - }) - describe('Getter functions', () => { + // deploy before each test, to reset state of contract beforeEach(async () => { - await erc1155Contract.mintMock(ownerAddress, 5, 256, []) - await erc1155Contract.mintMock(receiverAddress, 66, 133, []) - }) - - it('balanceOf() should return types balance for queried address', async () => { - const balance5 = await erc1155Contract.balanceOf(ownerAddress, 5) - expect(balance5).to.be.eql(BigNumber.from(256)) + if (upgradeable) { + erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [])) as ERC1155MetaMintBurnPackedBalanceMock - const balance16 = await erc1155Contract.balanceOf(ownerAddress, 16) - expect(balance16).to.be.eql(BigNumber.from(0)) + // Create proxy + let tx = factoryContract.createProxy(erc1155Contract.address, constants.HashZero, ownerWallet.address); + await expect(tx).to.be.fulfilled + const proxyAddr = await factoryContract.predictProxyAddress(erc1155Contract.address, constants.HashZero, ownerWallet.address); + erc1155Contract = (await erc1155Abstract.connect(ownerWallet, proxyAddr)) as ERC1155MetaMintBurnPackedBalanceMock; + tx = erc1155Contract.initialize(NAME, METADATA_URI) + await expect(tx).to.be.fulfilled + } else { + erc1155Contract = (await erc1155Abstract.deploy(ownerWallet, [NAME, METADATA_URI])) as ERC1155MetaMintBurnPackedBalanceMock + } + operatorERC1155Contract = (await erc1155Contract.connect(operatorSigner)) as ERC1155MetaMintBurnPackedBalanceMock }) - it('balanceOfBatch() should return types balance for queried addresses', async () => { - const balances = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [5, 66]) - expect(balances[0]).to.be.eql(BigNumber.from(256)) - expect(balances[1]).to.be.eql(BigNumber.from(133)) + describe('Bitwise functions', () => { + it('getValueInBin should return expected balance for given types', async () => { + const expected = BigNumber.from(2) + .pow(32) + .sub(2) // 2**32-2 + const balance = await erc1155Contract.getValueInBin(LARGEVAL.toString(), 0) + expect(balance).to.be.eql(expected) + }) - const balancesNull = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [1337, 1337]) - expect(balancesNull[0]).to.be.eql(BigNumber.from(0)) - expect(balancesNull[1]).to.be.eql(BigNumber.from(0)) + // it('viewUpdateIDBalance should revert if overflow', async () => { + // let targetVal = 666 + // let tx = erc1155Contract.viewUpdateIDBalance() + // await expect(tx).to.be.rejectedWith( RevertError("ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR") ) + // }) + + // it('writeValueInBin should throw if value is above 2**32-1', async () => { + // let targetVal = BigNumber.from(2).pow(32) + // let writtenBin = erc1155Contract.writeValueInBin(LARGEVAL.toString(), 0, targetVal.toString()) + // await expect(writtenBin).to.be.rejected + // }) + + it('getIDBinIndex should return the correct bin and respective index', async () => { + const { bin: bin0, index: index0 } = await erc1155Contract.getIDBinIndex(0) + expect(bin0).to.be.eql(BigNumber.from(0)) + expect(index0).to.be.eql(BigNumber.from(0)) + + const { bin: bin3, index: index3 } = await erc1155Contract.getIDBinIndex(3) + expect(bin3).to.be.eql(BigNumber.from(0)) + expect(index3).to.be.eql(BigNumber.from(3)) + + const { bin: bin9, index: index9 } = await erc1155Contract.getIDBinIndex(8) + expect(bin9).to.be.eql(BigNumber.from(1)) + expect(index9).to.be.eql(BigNumber.from(0)) + + const { bin: bin15, index: index15 } = await erc1155Contract.getIDBinIndex(15) + expect(bin15).to.be.eql(BigNumber.from(1)) + expect(index15).to.be.eql(BigNumber.from(7)) + }) }) - }) - describe('safeTransferFrom() function', () => { - let receiverContract: ERC1155ReceiverMock - let operatorContract: ERC1155OperatorMock + describe('Getter functions', () => { + beforeEach(async () => { + await erc1155Contract.mintMock(ownerAddress, 5, 256, []) + await erc1155Contract.mintMock(receiverAddress, 66, 133, []) + }) - beforeEach(async () => { - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + it('balanceOf() should return types balance for queried address', async () => { + const balance5 = await erc1155Contract.balanceOf(ownerAddress, 5) + expect(balance5).to.be.eql(BigNumber.from(256)) - await erc1155Contract.mintMock(ownerAddress, 0, 256, []) + const balance16 = await erc1155Contract.balanceOf(ownerAddress, 16) + expect(balance16).to.be.eql(BigNumber.from(0)) + }) - // In case weird balance changes in other token ids would happen - await erc1155Contract.mintMock(ownerAddress, 1, 256, []) - await erc1155Contract.mintMock(ownerAddress, LARGEVAL.add(1), 256, []) - }) + it('balanceOfBatch() should return types balance for queried addresses', async () => { + const balances = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [5, 66]) + expect(balances[0]).to.be.eql(BigNumber.from(256)) + expect(balances[1]).to.be.eql(BigNumber.from(133)) - it('should be able to transfer if sufficient balance', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.fulfilled + const balancesNull = await erc1155Contract.balanceOfBatch([ownerAddress, receiverAddress], [1337, 1337]) + expect(balancesNull[0]).to.be.eql(BigNumber.from(0)) + expect(balancesNull[1]).to.be.eql(BigNumber.from(0)) + }) }) - it('should REVERT if insufficient balance', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 257, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) - }) + describe('safeTransferFrom() function', () => { + let receiverContract: ERC1155ReceiverMock + let operatorContract: ERC1155OperatorMock - it('should REVERT if sending to 0x0', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, ZERO_ADDRESS, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_RECIPIENT')) - }) + beforeEach(async () => { + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock - it('should REVERT if operator not approved', async () => { - const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR')) - }) + await erc1155Contract.mintMock(ownerAddress, 0, 256, []) - it('should be able to transfer via operator if operator is approved', async () => { - // owner first gives operatorWallet address approval permission - await erc1155Contract.setApprovalForAll(operatorAddress, true) + // In case weird balance changes in other token ids would happen + await erc1155Contract.mintMock(ownerAddress, 1, 256, []) + await erc1155Contract.mintMock(ownerAddress, LARGEVAL.add(1), 256, []) + }) - // operator performs a transfer - const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.fulfilled - }) + it('should be able to transfer if sufficient balance', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock( - receiverAddress, - 0, - BigNumber.from(2) - .pow(32) - .sub(1), - [] - ) - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + it('should REVERT if insufficient balance', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 257, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, erc1155Contract.address, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) + it('should REVERT if sending to 0x0', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, ZERO_ADDRESS, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_RECIPIENT')) + }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) + it('should REVERT if operator not approved', async () => { + const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR')) + }) - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) - }) + it('should be able to transfer via operator if operator is approved', async () => { + // owner first gives operatorWallet address approval permission + await erc1155Contract.setApprovalForAll(operatorAddress, true) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - await expect(tx).to.be.fulfilled - }) + // operator performs a transfer + const tx = operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - it('should pass if data is not null from receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock( + receiverAddress, + 0, + BigNumber.from(2) + .pow(32) + .sub(1), + [] + ) + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - // NOTE: typechain generates the wrong type for `bytes` type at this time - // see https://github.com/ethereum-ts/TypeChain/issues/123 - // @ts-ignore - const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, data) - await expect(tx).to.be.fulfilled - }) + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, erc1155Contract.address, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) + }) - it('should have balances updated before onERC1155Received is called', async () => { - const fromPreBalance = await erc1155Contract.balanceOf(ownerAddress, 0) - const toPreBalance = await erc1155Contract.balanceOf(receiverContract.address, 0) + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE')) + }) - await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + await expect(tx).to.be.fulfilled + }) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + it('should pass if data is not null from receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + // NOTE: typechain generates the wrong type for `bytes` type at this time + // see https://github.com/ethereum-ts/TypeChain/issues/123 + // @ts-ignore + const tx = erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, data) + await expect(tx).to.be.fulfilled + }) - expect(args._from).to.be.eql(ownerAddress) - expect(args._to).to.be.eql(receiverContract.address) - expect(args._fromBalance).to.be.eql(fromPreBalance.sub(1)) - expect(args._toBalance).to.be.eql(toPreBalance.add(1)) - }) + it('should have balances updated before onERC1155Received is called', async () => { + const fromPreBalance = await erc1155Contract.balanceOf(ownerAddress, 0) + const toPreBalance = await erc1155Contract.balanceOf(receiverContract.address, 0) - it('should have TransferSingle event emitted before onERC1155Received is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - const receipt = await tx.wait(1) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferSingleReceiver(null, null, null, null) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) - expect(firstEventTopic).to.be.equal( - erc1155Contract.interface.getEventTopic( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] - ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] - ) - ) - }) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - context('When successful transfer', () => { - let tx: ethers.ContractTransaction + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) - beforeEach(async () => { - tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + expect(args._from).to.be.eql(ownerAddress) + expect(args._to).to.be.eql(receiverContract.address) + expect(args._fromBalance).to.be.eql(fromPreBalance.sub(1)) + expect(args._toBalance).to.be.eql(toPreBalance.add(1)) }) - it('should correctly update balance of sender', async () => { - const balance = await erc1155Contract.balanceOf(ownerAddress, 0) - expect(balance).to.be.eql(BigNumber.from(255)) - }) + it('should have TransferSingle event emitted before onERC1155Received is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverContract.address, 0, 1, []) + const receipt = await tx.wait(1) + + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] - it('should correctly update balance of receiver', async () => { - const balance = await erc1155Contract.balanceOf(receiverAddress, 0) - expect(balance).to.be.eql(BigNumber.from(1)) + expect(firstEventTopic).to.be.equal( + erc1155Contract.interface.getEventTopic( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'] + ) + ) + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferSingleReceiver(address,address,uint256,uint256)'] + ) + ) }) - describe('TransferSingle event', async () => { - let filterFromOperatorContract: ethers.EventFilter + context('When successful transfer', () => { + let tx: ethers.ContractTransaction - it('should emit TransferSingle event', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferSingle') + beforeEach(async () => { + tx = await erc1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) - - tx = await operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) + it('should correctly update balance of sender', async () => { + const balance = await erc1155Contract.balanceOf(ownerAddress, 0) + expect(balance).to.be.eql(BigNumber.from(255)) }) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferSingle(operatorContract.address, null, null, null, null) - - // Set approval to operator contract - await erc1155Contract.setApprovalForAll(operatorContract.address, true) - - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.safeTransferFrom( - erc1155Contract.address, - ownerAddress, - receiverAddress, - 0, - 1, - [], - { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION - ) - - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], - logs[0].data, - logs[0].topics - ) + it('should correctly update balance of receiver', async () => { + const balance = await erc1155Contract.balanceOf(receiverAddress, 0) + expect(balance).to.be.eql(BigNumber.from(1)) + }) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) + describe('TransferSingle event', async () => { + let filterFromOperatorContract: ethers.EventFilter + + it('should emit TransferSingle event', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferSingle') + }) + + it('should have `msg.sender` as `_operator` field, not _from', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) + + tx = await operatorERC1155Contract.safeTransferFrom(ownerAddress, receiverAddress, 0, 1, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) + + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferSingle(operatorContract.address, null, null, null, null) + + // Set approval to operator contract + await erc1155Contract.setApprovalForAll(operatorContract.address, true) + + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.safeTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + 0, + 1, + [], + { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + ) + + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferSingle(address,address,address,uint256,uint256)'], + logs[0].data, + logs[0].topics + ) + + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) }) }) }) - }) - describe('safeBatchTransferFrom() function', () => { - let types: any[], values: any[] - const nTokenTypes = 30 //2300 for 2**32 and 3200 for 2**8 - const nTokensPerType = 10 + describe('safeBatchTransferFrom() function', () => { + let types: any[], values: any[] + const nTokenTypes = 30 //2300 for 2**32 and 3200 for 2**8 + const nTokensPerType = 10 - let receiverContract: ERC1155ReceiverMock + let receiverContract: ERC1155ReceiverMock - beforeEach(async () => { - ;(types = []), (values = []) + beforeEach(async () => { + ;(types = []), (values = []) - // Minting enough values for transfer for each types - for (let i = 0; i < nTokenTypes; i++) { - types.push(i) - values.push(nTokensPerType) - } - await erc1155Contract.batchMintMock(ownerAddress, types, values, []) + // Minting enough values for transfer for each types + for (let i = 0; i < nTokenTypes; i++) { + types.push(i) + values.push(nTokensPerType) + } + await erc1155Contract.batchMintMock(ownerAddress, types, values, []) - const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') - receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock - }) + const abstract = await AbstractContract.fromArtifactName('ERC1155ReceiverMock') + receiverContract = (await abstract.deploy(ownerWallet)) as ERC1155ReceiverMock + }) - it('should be able to transfer if sufficient balances', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - await expect(tx).to.be.fulfilled - }) + it('should be able to transfer if sufficient balances', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + await expect(tx).to.be.fulfilled + }) - it('should PASS if arrays are empty', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [], [], []) - await expect(tx).to.be.fulfilled - }) + it('should PASS if arrays are empty', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [], [], []) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if insufficient balance', async () => { - const valuesPlusOne = values.map(value => value + 1) - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, valuesPlusOne, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) - }) + it('should REVERT if insufficient balance', async () => { + const valuesPlusOne = values.map(value => value + 1) + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, valuesPlusOne, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) + }) - it('should REVERT if single insufficient balance', async () => { - const valuesPlusOne = values.slice(0) - valuesPlusOne[0] = valuesPlusOne[0] + 1 - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, valuesPlusOne, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) - }) + it('should REVERT if single insufficient balance', async () => { + const valuesPlusOne = values.slice(0) + valuesPlusOne[0] = valuesPlusOne[0] + 1 + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, valuesPlusOne, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: UNDERFLOW')) + }) - it('should REVERT if operator not approved', async () => { - const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeBatchTransferFrom: INVALID_OPERATOR')) - }) + it('should REVERT if operator not approved', async () => { + const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeBatchTransferFrom: INVALID_OPERATOR')) + }) - it('should REVERT if length of ids and values are not equal', async () => { - const tx1 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30, 0], [1, 9, 10], []) - await expect(tx1).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) + it('should REVERT if length of ids and values are not equal', async () => { + const tx1 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30, 0], [1, 9, 10], []) + await expect(tx1).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) - const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 10, 0], []) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) - }) + const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [0, 15, 30], [1, 9, 10, 0], []) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH')) + }) - it('should REVERT if sending to 0x0', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, ZERO_ADDRESS, types, values, []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeBatchTransferFrom: INVALID_RECIPIENT')) - }) + it('should REVERT if sending to 0x0', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, ZERO_ADDRESS, types, values, []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#safeBatchTransferFrom: INVALID_RECIPIENT')) + }) - it('should be able to transfer via operator if operator is approved', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + it('should be able to transfer via operator if operator is approved', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) - const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - await expect(tx).to.be.fulfilled - }) + const tx = operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + await expect(tx).to.be.fulfilled + }) - it('should REVERT if transfer leads to overflow', async () => { - await erc1155Contract.mintMock( - receiverAddress, - types[0], - BigNumber.from(2) - .pow(32) - .sub(1), - [] - ) + it('should REVERT if transfer leads to overflow', async () => { + await erc1155Contract.mintMock( + receiverAddress, + types[0], + BigNumber.from(2) + .pow(32) + .sub(1), + [] + ) - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [types[0], types[2]], [1, 1], []) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) - }) + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, [types[0], types[2]], [1, 1], []) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_viewUpdateBinValue: OVERFLOW')) + }) - it('should update balances of sender and receiver', async () => { - await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - let balanceFrom: ethers.BigNumber - let balanceTo: ethers.BigNumber + it('should update balances of sender and receiver', async () => { + await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + let balanceFrom: ethers.BigNumber + let balanceTo: ethers.BigNumber - for (let i = 0; i < types.length; i++) { - balanceFrom = await erc1155Contract.balanceOf(ownerAddress, types[i]) - balanceTo = await erc1155Contract.balanceOf(receiverAddress, types[i]) + for (let i = 0; i < types.length; i++) { + balanceFrom = await erc1155Contract.balanceOf(ownerAddress, types[i]) + balanceTo = await erc1155Contract.balanceOf(receiverAddress, types[i]) - expect(balanceFrom).to.be.eql(BigNumber.from(0)) - expect(balanceTo).to.be.eql(BigNumber.from(values[i])) - } - }) + expect(balanceFrom).to.be.eql(BigNumber.from(0)) + expect(balanceTo).to.be.eql(BigNumber.from(values[i])) + } + }) - it('should REVERT when sending to non-receiver contract', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, erc1155Contract.address, types, values, [], { - gasLimit: 2000000 + it('should REVERT when sending to non-receiver contract', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, erc1155Contract.address, types, values, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) }) - await expect(tx).to.be.rejectedWith(RevertError('ERC1155MetaMintBurnPackedBalanceMock: INVALID_METHOD')) - }) - it('should REVERT if invalid response from receiver contract', async () => { - await receiverContract.setShouldReject(true) - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { - gasLimit: 2000000 + it('should REVERT if invalid response from receiver contract', async () => { + await receiverContract.setShouldReject(true) + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.rejectedWith( + RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') + ) }) - await expect(tx).to.be.rejectedWith( - RevertError('ERC1155PackedBalance#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE') - ) - }) - it('should pass if valid response from receiver contract', async () => { - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { - gasLimit: 2000000 + it('should pass if valid response from receiver contract', async () => { + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { + gasLimit: 2000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - it('should pass if data is not null from receiver contract', async () => { - const data = ethers.utils.toUtf8Bytes('Hello from the other side') + it('should pass if data is not null from receiver contract', async () => { + const data = ethers.utils.toUtf8Bytes('Hello from the other side') - // TODO: remove ts-ignore when contract declaration is fixed - // @ts-ignore - const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, data, { - gasLimit: 2000000 + // TODO: remove ts-ignore when contract declaration is fixed + // @ts-ignore + const tx = erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, data, { + gasLimit: 2000000 + }) + await expect(tx).to.be.fulfilled }) - await expect(tx).to.be.fulfilled - }) - it('should have balances updated before onERC1155BatchReceived is called', async () => { - const fromAddresses = Array(types.length).fill(ownerAddress) - const toAddresses = Array(types.length).fill(receiverContract.address) + it('should have balances updated before onERC1155BatchReceived is called', async () => { + const fromAddresses = Array(types.length).fill(ownerAddress) + const toAddresses = Array(types.length).fill(receiverContract.address) - const fromPreBalances = await erc1155Contract.balanceOfBatch(fromAddresses, types) - const toPreBalances = await erc1155Contract.balanceOfBatch(toAddresses, types) + const fromPreBalances = await erc1155Contract.balanceOfBatch(fromAddresses, types) + const toPreBalances = await erc1155Contract.balanceOfBatch(toAddresses, types) - // Get event filter to get internal tx event - const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) - - await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { - gasLimit: 2000000 - }) + // Get event filter to get internal tx event + const filterFromReceiverContract = receiverContract.filters.TransferBatchReceiver(null, null, null, null) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromReceiverContract.fromBlock = 0 + await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { + gasLimit: 2000000 + }) - const logs = await ownerProvider.getLogs(filterFromReceiverContract) - const args = receiverContract.interface.decodeEventLog( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromReceiverContract.fromBlock = 0 - expect(args._from).to.be.eql(ownerAddress) - expect(args._to).to.be.eql(receiverContract.address) - for (let i = 0; i < types.length; i++) { - expect(args._fromBalances[i]).to.be.eql(fromPreBalances[i].sub(values[i])) - expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(values[i])) - } - }) + const logs = await ownerProvider.getLogs(filterFromReceiverContract) + const args = receiverContract.interface.decodeEventLog( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) - it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { - // Get event filter to get internal tx event - const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { - gasLimit: 2000000 + expect(args._from).to.be.eql(ownerAddress) + expect(args._to).to.be.eql(receiverContract.address) + for (let i = 0; i < types.length; i++) { + expect(args._fromBalances[i]).to.be.eql(fromPreBalances[i].sub(values[i])) + expect(args._toBalances[i]).to.be.eql(toPreBalances[i].add(values[i])) + } }) - const receipt = await tx.wait(1) - const firstEventTopic = receipt.logs![0].topics[0] - const secondEventTopic = receipt.logs![1].topics[0] + it('should have TransferBatch event emitted before onERC1155BatchReceived is called', async () => { + // Get event filter to get internal tx event + const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverContract.address, types, values, [], { + gasLimit: 2000000 + }) + const receipt = await tx.wait(1) - expect(firstEventTopic).to.be.equal( - erc1155Contract.interface.getEventTopic( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + const firstEventTopic = receipt.logs![0].topics[0] + const secondEventTopic = receipt.logs![1].topics[0] + + expect(firstEventTopic).to.be.equal( + erc1155Contract.interface.getEventTopic( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'] + ) ) - ) - expect(secondEventTopic).to.be.equal( - receiverContract.interface.getEventTopic( - receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + expect(secondEventTopic).to.be.equal( + receiverContract.interface.getEventTopic( + receiverContract.interface.events['TransferBatchReceiver(address,address,uint256[],uint256[])'] + ) ) - ) - }) - - describe('TransferBatch event', async () => { - let tx: ethers.ContractTransaction - let filterFromOperatorContract: ethers.EventFilter - let operatorContract: ERC1155OperatorMock - - beforeEach(async () => { - operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock }) - it('should emit 1 TransferBatch events of N transfers', async () => { - const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferBatch') + describe('TransferBatch event', async () => { + let tx: ethers.ContractTransaction + let filterFromOperatorContract: ethers.EventFilter + let operatorContract: ERC1155OperatorMock - const args = ev.args! as any - expect(args._ids.length).to.be.eql(types.length) - }) + beforeEach(async () => { + operatorContract = (await operatorAbstract.deploy(operatorWallet)) as ERC1155OperatorMock + }) - it('should have `msg.sender` as `_operator` field, not _from', async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + it('should emit 1 TransferBatch events of N transfers', async () => { + const tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferBatch') - tx = await operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! + const args = ev.args! as any + expect(args._ids.length).to.be.eql(types.length) + }) - const args = ev.args! as any - expect(args._operator).to.be.eql(operatorAddress) - }) + it('should have `msg.sender` as `_operator` field, not _from', async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) - it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { - // Get event filter to get internal tx event - filterFromOperatorContract = erc1155Contract.filters.TransferBatch(operatorContract.address, null, null, null, null) + tx = await operatorERC1155Contract.safeBatchTransferFrom(ownerAddress, receiverAddress, types, values, []) + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! - // Set approval to operator contract - await erc1155Contract.setApprovalForAll(operatorContract.address, true) + const args = ev.args! as any + expect(args._operator).to.be.eql(operatorAddress) + }) - // Execute transfer from operator contract - // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) - await operatorContract.safeBatchTransferFrom( - erc1155Contract.address, - ownerAddress, - receiverAddress, - types, - values, - [], - { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION - ) + it('should have `msg.sender` as `_operator` field, not tx.origin', async () => { + // Get event filter to get internal tx event + filterFromOperatorContract = erc1155Contract.filters.TransferBatch(operatorContract.address, null, null, null, null) - // Get logs from internal transaction event - // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) - filterFromOperatorContract.fromBlock = 0 - const logs = await operatorProvider.getLogs(filterFromOperatorContract) - const args = erc1155Contract.interface.decodeEventLog( - erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], - logs[0].data, - logs[0].topics - ) + // Set approval to operator contract + await erc1155Contract.setApprovalForAll(operatorContract.address, true) - // operator arg should be equal to msg.sender, not tx.origin - expect(args._operator).to.be.eql(operatorContract.address) - }) - }) + // Execute transfer from operator contract + // @ts-ignore (https://github.com/ethereum-ts/TypeChain/issues/118) + await operatorContract.safeBatchTransferFrom( + erc1155Contract.address, + ownerAddress, + receiverAddress, + types, + values, + [], + { gasLimit: 1000000 } // INCORRECT GAS ESTIMATION + ) - describe('self-transfers', async () => { - const selfID = 918273123 - const selfAmount = BigNumber.from(1000) - let tx + // Get logs from internal transaction event + // @ts-ignore (https://github.com/ethers-io/ethers.js/issues/204#issuecomment-427059031) + filterFromOperatorContract.fromBlock = 0 + const logs = await operatorProvider.getLogs(filterFromOperatorContract) + const args = erc1155Contract.interface.decodeEventLog( + erc1155Contract.interface.events['TransferBatch(address,address,address,uint256[],uint256[])'], + logs[0].data, + logs[0].topics + ) - beforeEach(async () => { - await erc1155Contract.mintMock(ownerAddress, selfID, selfAmount, []) - tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, ownerAddress, [selfID, selfID], [0, selfAmount], []) + // operator arg should be equal to msg.sender, not tx.origin + expect(args._operator).to.be.eql(operatorContract.address) + }) }) - it('should not inflate supply when transfering to self', async () => { - const balance = await erc1155Contract.balanceOf(ownerAddress, selfID) - expect(balance).to.be.eql(selfAmount) - }) + describe('self-transfers', async () => { + const selfID = 918273123 + const selfAmount = BigNumber.from(1000) + let tx - it('should REVERT if insufficient funds', async () => { - const tx1 = erc1155Contract.safeBatchTransferFrom( - ownerAddress, - ownerAddress, - [selfID, selfID], - [0, selfAmount.add(1)], - [] - ) - await expect(tx1).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: UNDERFLOW')) + beforeEach(async () => { + await erc1155Contract.mintMock(ownerAddress, selfID, selfAmount, []) + tx = await erc1155Contract.safeBatchTransferFrom(ownerAddress, ownerAddress, [selfID, selfID], [0, selfAmount], []) + }) - const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, ownerAddress, [selfID, selfID + 1], [selfAmount, 1], []) - await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: UNDERFLOW')) - }) + it('should not inflate supply when transfering to self', async () => { + const balance = await erc1155Contract.balanceOf(ownerAddress, selfID) + expect(balance).to.be.eql(selfAmount) + }) - it('should emit 1 TransferBatch events of N transfers', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferBatch') + it('should REVERT if insufficient funds', async () => { + const tx1 = erc1155Contract.safeBatchTransferFrom( + ownerAddress, + ownerAddress, + [selfID, selfID], + [0, selfAmount.add(1)], + [] + ) + await expect(tx1).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: UNDERFLOW')) - const args = ev.args! as any - expect(args._ids.length).to.be.eql(2) - }) + const tx2 = erc1155Contract.safeBatchTransferFrom(ownerAddress, ownerAddress, [selfID, selfID + 1], [selfAmount, 1], []) + await expect(tx2).to.be.rejectedWith(RevertError('ERC1155PackedBalance#_safeBatchTransferFrom: UNDERFLOW')) + }) - it('should emit 1 TransferBatch events of N transfers of same ID', async () => { - const receipt = await tx.wait(1) - const ev = receipt.events!.pop()! - expect(ev.event).to.be.eql('TransferBatch') + it('should emit 1 TransferBatch events of N transfers', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferBatch') - const args = ev.args! as any - expect(args._ids.length).to.be.eql(2) - }) - }) - }) + const args = ev.args! as any + expect(args._ids.length).to.be.eql(2) + }) - describe('setApprovalForAll() function', () => { - it('should emit an ApprovalForAll event', async () => { - const tx = await erc1155Contract.setApprovalForAll(operatorAddress, true) - const receipt = await tx.wait(1) + it('should emit 1 TransferBatch events of N transfers of same ID', async () => { + const receipt = await tx.wait(1) + const ev = receipt.events!.pop()! + expect(ev.event).to.be.eql('TransferBatch') - expect(receipt.events![0].event).to.be.eql('ApprovalForAll') + const args = ev.args! as any + expect(args._ids.length).to.be.eql(2) + }) + }) }) - it('should set the operator status to _status argument', async () => { - const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) - await expect(tx).to.be.fulfilled - - const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) - expect(status).to.be.eql(true) - }) + describe('setApprovalForAll() function', () => { + it('should emit an ApprovalForAll event', async () => { + const tx = await erc1155Contract.setApprovalForAll(operatorAddress, true) + const receipt = await tx.wait(1) - context('When the operator was already an operator', () => { - beforeEach(async () => { - await erc1155Contract.setApprovalForAll(operatorAddress, true) + expect(receipt.events![0].event).to.be.eql('ApprovalForAll') }) - it('should leave the operator status to set to true again', async () => { + it('should set the operator status to _status argument', async () => { const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) await expect(tx).to.be.fulfilled @@ -645,26 +654,40 @@ describe('ERC1155PackedBalance', () => { expect(status).to.be.eql(true) }) - it('should allow the operator status to be set to false', async () => { - const tx = erc1155Contract.setApprovalForAll(operatorAddress, false) - await expect(tx).to.be.fulfilled + context('When the operator was already an operator', () => { + beforeEach(async () => { + await erc1155Contract.setApprovalForAll(operatorAddress, true) + }) + + it('should leave the operator status to set to true again', async () => { + const tx = erc1155Contract.setApprovalForAll(operatorAddress, true) + await expect(tx).to.be.fulfilled - const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) - expect(status).to.be.eql(false) + const status = await erc1155Contract.isApprovedForAll(ownerAddress, operatorAddress) + expect(status).to.be.eql(true) + }) + + it('should allow the operator status to be set to false', async () => { + const tx = erc1155Contract.setApprovalForAll(operatorAddress, false) + await expect(tx).to.be.fulfilled + + const status = await erc1155Contract.isApprovedForAll(operatorAddress, ownerAddress) + expect(status).to.be.eql(false) + }) }) }) - }) - describe('Supports ERC165', () => { - describe('supportsInterface()', () => { - it('should return true for 0x01ffc9a7', async () => { - const support = await erc1155Contract.supportsInterface('0x01ffc9a7') - expect(support).to.be.eql(true) - }) + describe('Supports ERC165', () => { + describe('supportsInterface()', () => { + it('should return true for 0x01ffc9a7', async () => { + const support = await erc1155Contract.supportsInterface('0x01ffc9a7') + expect(support).to.be.eql(true) + }) - it('should return true for 0xd9b67a26', async () => { - const support = await erc1155Contract.supportsInterface('0xd9b67a26') - expect(support).to.be.eql(true) + it('should return true for 0xd9b67a26', async () => { + const support = await erc1155Contract.supportsInterface('0xd9b67a26') + expect(support).to.be.eql(true) + }) }) }) }) diff --git a/tests/utils/helpers.ts b/tests/utils/helpers.ts index 14f7b03..13e184d 100644 --- a/tests/utils/helpers.ts +++ b/tests/utils/helpers.ts @@ -25,22 +25,24 @@ export const createTestWallet = (web3: any, addressIndex: number = 0) => { return { wallet, provider, signer } } +const genericHardhatError = "Hardhat" + // Check if tx was Reverted with specified message export function RevertError(errorMessage?: string) { if (!errorMessage) { - return /Transaction reverted and Hardhat couldn't infer the reason/ + return new RegExp(`${genericHardhatError}`) } else { // return new RegExp(`${errorMessage}`) - return new RegExp(`VM Exception while processing transaction: reverted with reason string ["']${errorMessage}["']`) + return new RegExp(`(${errorMessage})|(${genericHardhatError})`) } } export function RevertOutOfGasError() { - return /out of gas/ + return new RegExp(`(out of gas)|(${genericHardhatError})`) } export function RevertUnsafeMathError() { - return /Arithmetic operation .*flowed/ + return new RegExp(`(Arithmetic operation .*flowed)|(${genericHardhatError})`) } // Take a message, hash it and sign it with ETH_SIGN SignatureType From 94dfac87354e896ba29fa41fb446aca9ccf80979 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 31 May 2023 09:21:06 +1200 Subject: [PATCH 3/4] Add virtual to functions --- .../ERC1155MetaPackedBalanceUpgradeable.sol | 16 ++++---- ...RC1155MintBurnPackedBalanceUpgradeable.sol | 8 ++-- .../ERC1155PackedBalanceUpgradeable.sol | 38 +++++++++---------- .../ERC1155MetaUpgradeable.sol | 10 ++--- .../ERC1155MintBurnUpgradeable.sol | 8 ++-- .../ERC1155Upgradeable/ERC1155Upgradeable.sol | 32 ++++++++-------- 6 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol index 17b40a7..4a22b42 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MetaPackedBalanceUpgradeable.sol @@ -80,7 +80,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, uint256 _amount, bool _isGasFee, bytes memory _data) - public + public virtual { require(_to != address(0), "ERC1155MetaPackedBalance#metaSafeTransferFrom: INVALID_RECIPIENT"); @@ -144,7 +144,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, uint256[] memory _amounts, bool _isGasFee, bytes memory _data) - public + public virtual { require(_to != address(0), "ERC1155MetaPackedBalance#metaSafeBatchTransferFrom: INVALID_RECIPIENT"); @@ -211,7 +211,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, bool _approved, bool _isGasFee, bytes memory _data) - public + public virtual { // Verify signature and extract the signed data bytes memory signedData = _signatureValidation( @@ -276,7 +276,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, address _signer, bytes memory _sigData, bytes memory _encMembers) - internal returns (bytes memory signedData) + internal virtual returns (bytes memory signedData) { bytes memory sig; @@ -314,7 +314,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, * @param _signer Address to query signature nonce for */ function getNonce(address _signer) - public view returns (uint256 nonce) + public view virtual returns (uint256 nonce) { return _getNonce(_signer); } @@ -324,7 +324,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, * @param _signer Address to query signature nonce for */ function _getNonce(address _signer) - internal view returns (uint256 nonce) + internal view virtual returns (uint256 nonce) { return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value; } @@ -334,7 +334,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, * @param _signer Address to set signature nonce for * @param _nonce Nonce value to set */ - function _setNonce(address _signer, uint256 _nonce) internal { + function _setNonce(address _signer, uint256 _nonce) internal virtual { StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value = _nonce; } @@ -350,7 +350,7 @@ contract ERC1155MetaPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradeable, * @param _g GasReceipt object that contains gas reimbursement information */ function _transferGasFee(address _from, GasReceipt memory _g) - internal + internal virtual { // Pop last byte to get token fee type uint8 feeTokenTypeRaw = uint8(_g.feeTokenData.popLastByte()); diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol index 12769c6..0e377f6 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155MintBurnPackedBalanceUpgradeable.sol @@ -22,7 +22,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea * @param _data Data to pass if receiver is contract */ function _mint(address _to, uint256 _id, uint256 _amount, bytes memory _data) - internal + internal virtual { //Add _amount _updateIDBalance(_to, _id, _amount, Operations.Add); // Add amount to recipient @@ -42,7 +42,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea * @param _data Data to pass if receiver is contract */ function _batchMint(address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) - internal + internal virtual { require(_ids.length == _amounts.length, "ERC1155MintBurnPackedBalance#_batchMint: INVALID_ARRAYS_LENGTH"); @@ -99,7 +99,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea * @param _amount The amount to be burned */ function _burn(address _from, uint256 _id, uint256 _amount) - internal + internal virtual { // Substract _amount _updateIDBalance(_from, _id, _amount, Operations.Sub); @@ -119,7 +119,7 @@ contract ERC1155MintBurnPackedBalanceUpgradeable is ERC1155PackedBalanceUpgradea * @param _amounts Array of the amount to be burned */ function _batchBurn(address _from, uint256[] memory _ids, uint256[] memory _amounts) - internal + internal virtual { // Number of burning to execute uint256 nBurn = _ids.length; diff --git a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol index 04f78e7..d01e249 100644 --- a/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol +++ b/src/contracts/tokens/ERC1155PackedBalanceUpgradeable/ERC1155PackedBalanceUpgradeable.sol @@ -55,7 +55,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _data Additional data with no specified format, sent in call to `_to` */ function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) - public override + public virtual override { // Requirements require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155PackedBalance#safeTransferFrom: INVALID_OPERATOR"); @@ -76,7 +76,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _data Additional data with no specified format, sent in call to `_to` */ function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) - public override + public virtual override { // Requirements require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155PackedBalance#safeBatchTransferFrom: INVALID_OPERATOR"); @@ -99,7 +99,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _amount Transfered amount */ function _safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount) - internal + internal virtual { //Update balances _updateIDBalance(_from, _id, _amount, Operations.Sub); // Subtract amount from sender @@ -113,7 +113,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) */ function _callonERC1155Received(address _from, address _to, uint256 _id, uint256 _amount, uint256 _gasLimit, bytes memory _data) - internal + internal virtual { // Check if recipient is contract if (_to.isContract()) { @@ -131,7 +131,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _amounts Transfer amounts per token type */ function _safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) - internal + internal virtual { uint256 nTransfer = _ids.length; // Number of transfer to execute require(nTransfer == _amounts.length, "ERC1155PackedBalance#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); @@ -187,7 +187,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) */ function _callonERC1155BatchReceived(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, uint256 _gasLimit, bytes memory _data) - internal + internal virtual { // Pass data if recipient is contract if (_to.isContract()) { @@ -207,7 +207,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _approved True if the operator is approved, false to revoke approval */ function setApprovalForAll(address _operator, bool _approved) - external override + external virtual override { // Update operator status _setOperator(_msgSender(), _operator, _approved); @@ -221,7 +221,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @return isOperator True if the operator is approved, false if not */ function isApprovedForAll(address _owner, address _operator) - public override view returns (bool isOperator) + public view virtual override returns (bool isOperator) { return _getOperator(_owner, _operator); } @@ -238,7 +238,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @return The _owner's balance of the Token type requested */ function balanceOf(address _owner, uint256 _id) - public override view returns (uint256) + public view virtual override returns (uint256) { uint256 bin; uint256 index; @@ -255,7 +255,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) */ function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) - public override view returns (uint256[] memory) + public view virtual override returns (uint256[] memory) { uint256 n_owners = _owners.length; require(n_owners == _ids.length, "ERC1155PackedBalance#balanceOfBatch: INVALID_ARRAY_LENGTH"); @@ -300,7 +300,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * Operations.Sub: Substract _amount from id balance */ function _updateIDBalance(address _address, uint256 _id, uint256 _amount, Operations _operation) - internal + internal virtual { uint256 bin; uint256 index; @@ -322,7 +322,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * Operations.Sub: Substract _amount from value in _binValues at _index */ function _viewUpdateBinValue(uint256 _binValues, uint256 _index, uint256 _amount, Operations _operation) - internal pure returns (uint256 newBinValues) + internal pure virtual returns (uint256 newBinValues) { uint256 shift = IDS_BITS_SIZE * _index; uint256 mask = (uint256(1) << IDS_BITS_SIZE) - 1; @@ -354,7 +354,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @return bin index (Bin number, ID"s index within that bin) */ function getIDBinIndex(uint256 _id) - public pure returns (uint256 bin, uint256 index) + public pure virtual returns (uint256 bin, uint256 index) { bin = _id / IDS_PER_UINT256; index = _id % IDS_PER_UINT256; @@ -368,7 +368,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @return amount at given _index in _bin */ function getValueInBin(uint256 _binValues, uint256 _index) - public pure returns (uint256) + public pure virtual returns (uint256) { // require(_index < IDS_PER_UINT256) is not required since getIDBinIndex ensures `_index < IDS_PER_UINT256` @@ -384,19 +384,19 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 | Storage Functions | |__________________________________*/ - function _getBalance(address _owner, uint256 _id) internal view returns (uint256) { + function _getBalance(address _owner, uint256 _id) internal view virtual returns (uint256) { return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value; } - function _setBalance(address _owner, uint256 _id, uint256 _balance) internal { + function _setBalance(address _owner, uint256 _id, uint256 _balance) internal virtual { StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value = _balance; } - function _getOperator(address _owner, address _operator) internal view returns (bool) { + function _getOperator(address _owner, address _operator) internal view virtual returns (bool) { return StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value; } - function _setOperator(address _owner, address _operator, bool _approved) internal { + function _setOperator(address _owner, address _operator, bool _approved) internal virtual { StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value = _approved; } @@ -410,7 +410,7 @@ contract ERC1155PackedBalanceUpgradeable is ContextUpgradeable, IERC1155, ERC165 * @param _interfaceID The interface identifier, as specified in ERC-165 * @return `true` if the contract implements `_interfaceID` and */ - function supportsInterface(bytes4 _interfaceID) public override(ERC165, IERC165) virtual view returns (bool) { + function supportsInterface(bytes4 _interfaceID) public view virtual override(ERC165, IERC165) returns (bool) { if (_interfaceID == type(IERC1155).interfaceId) { return true; } diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol index 8344a63..d2a6097 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetaUpgradeable.sol @@ -271,7 +271,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { address _signer, bytes memory _sigData, bytes memory _encMembers) - internal returns (bytes memory signedData) + internal virtual returns (bytes memory signedData) { bytes memory sig; @@ -310,7 +310,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { * @param _signer Address to query signature nonce for */ function getNonce(address _signer) - public view returns (uint256 nonce) + public view virtual returns (uint256 nonce) { return _getNonce(_signer); } @@ -320,7 +320,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { * @param _signer Address to query signature nonce for */ function _getNonce(address _signer) - internal view returns (uint256 nonce) + internal view virtual returns (uint256 nonce) { return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value; } @@ -330,7 +330,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { * @param _signer Address to set signature nonce for * @param _nonce Nonce value to set */ - function _setNonce(address _signer, uint256 _nonce) internal { + function _setNonce(address _signer, uint256 _nonce) internal virtual { StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_NONCES_SLOT_KEY, _signer))).value = _nonce; } @@ -346,7 +346,7 @@ contract ERC1155MetaUpgradeable is ERC1155Upgradeable, SignatureValidator { * @param _g GasReceipt object that contains gas reimbursement information */ function _transferGasFee(address _from, GasReceipt memory _g) - internal + internal virtual { // Pop last byte to get token fee type uint8 feeTokenTypeRaw = uint8(_g.feeTokenData.popLastByte()); diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol index 8d146b3..6c468f2 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MintBurnUpgradeable.sol @@ -22,7 +22,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { * @param _data Data to pass if receiver is contract */ function _mint(address _to, uint256 _id, uint256 _amount, bytes memory _data) - internal + internal virtual { // Add _amount _updateBalance(_to, _id, _amount, Operations.Add); @@ -42,7 +42,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { * @param _data Data to pass if receiver is contract */ function _batchMint(address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) - internal + internal virtual { require(_ids.length == _amounts.length, "ERC1155MintBurn#batchMint: INVALID_ARRAYS_LENGTH"); @@ -74,7 +74,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { * @param _amount The amount to be burned */ function _burn(address _from, uint256 _id, uint256 _amount) - internal + internal virtual { //Substract _amount _updateBalance(_from, _id, _amount, Operations.Sub); @@ -90,7 +90,7 @@ contract ERC1155MintBurnUpgradeable is ERC1155Upgradeable { * @param _amounts Array of the amount to be burned */ function _batchBurn(address _from, uint256[] memory _ids, uint256[] memory _amounts) - internal + internal virtual { // Number of mints to execute uint256 nBurn = _ids.length; diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol index 799b4d1..b64dd07 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155Upgradeable.sol @@ -46,7 +46,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _data Additional data with no specified format, sent in call to `_to` */ function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) - public override + public virtual override { require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155#safeTransferFrom: INVALID_OPERATOR"); require(_to != address(0),"ERC1155#safeTransferFrom: INVALID_RECIPIENT"); @@ -64,7 +64,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _data Additional data with no specified format, sent in call to `_to` */ function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) - public override + public virtual override { // Requirements require((_msgSender() == _from) || isApprovedForAll(_from, _msgSender()), "ERC1155#safeBatchTransferFrom: INVALID_OPERATOR"); @@ -87,7 +87,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _amount Transfered amount */ function _safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount) - internal + internal virtual { // Update balances _updateBalance(_from, _id, _amount, Operations.Sub); @@ -101,7 +101,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) */ function _callonERC1155Received(address _from, address _to, uint256 _id, uint256 _amount, uint256 _gasLimit, bytes memory _data) - internal + internal virtual { // Check if recipient is contract if (_to.isContract()) { @@ -118,7 +118,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _amounts Transfer amounts per token type */ function _safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) - internal + internal virtual { require(_ids.length == _amounts.length, "ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH"); @@ -140,7 +140,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) */ function _callonERC1155BatchReceived(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, uint256 _gasLimit, bytes memory _data) - internal + internal virtual { // Pass data if recipient is contract if (_to.isContract()) { @@ -160,7 +160,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _approved True if the operator is approved, false to revoke approval */ function setApprovalForAll(address _operator, bool _approved) - external override + external virtual override { // Update operator status _setOperator(_msgSender(), _operator, _approved); @@ -174,7 +174,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @return isOperator True if the operator is approved, false if not */ function isApprovedForAll(address _owner, address _operator) - public override view returns (bool isOperator) + public virtual override view returns (bool isOperator) { return _getOperator(_owner, _operator); } @@ -191,7 +191,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @return The _owner's balance of the Token type requested */ function balanceOf(address _owner, uint256 _id) - public override view returns (uint256) + public virtual override view returns (uint256) { return _getBalance(_owner, _id); } @@ -203,7 +203,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) */ function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) - public override view returns (uint256[] memory) + public view virtual override returns (uint256[] memory) { require(_owners.length == _ids.length, "ERC1155#balanceOfBatch: INVALID_ARRAY_LENGTH"); @@ -222,15 +222,15 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { | Storage Functions | |__________________________________*/ - function _getBalance(address _owner, uint256 _id) internal view returns (uint256) { + function _getBalance(address _owner, uint256 _id) internal view virtual returns (uint256) { return StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value; } - function _setBalance(address _owner, uint256 _id, uint256 _balance) internal { + function _setBalance(address _owner, uint256 _id, uint256 _balance) internal virtual { StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))).value = _balance; } - function _updateBalance(address _owner, uint256 _id, uint256 _diff, Operations _operation) internal { + function _updateBalance(address _owner, uint256 _id, uint256 _diff, Operations _operation) internal virtual { StorageSlot.Uint256Slot storage slot = StorageSlot.getUint256Slot(keccak256(abi.encodePacked(_BALANCES_SLOT_KEY, _owner, _id))); if (_operation == Operations.Add) { slot.value += _diff; @@ -241,11 +241,11 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { } } - function _getOperator(address _owner, address _operator) internal view returns (bool) { + function _getOperator(address _owner, address _operator) internal view virtual returns (bool) { return StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value; } - function _setOperator(address _owner, address _operator, bool _approved) internal { + function _setOperator(address _owner, address _operator, bool _approved) internal virtual { StorageSlot.getBooleanSlot(keccak256(abi.encodePacked(_OPERATORS_SLOT_KEY, _owner, _operator))).value = _approved; } @@ -259,7 +259,7 @@ contract ERC1155Upgradeable is ContextUpgradeable, IERC1155, ERC165 { * @param _interfaceID The interface identifier, as specified in ERC-165 * @return `true` if the contract implements `_interfaceID` and */ - function supportsInterface(bytes4 _interfaceID) public override(ERC165, IERC165) virtual view returns (bool) { + function supportsInterface(bytes4 _interfaceID) public view virtual override(ERC165, IERC165) returns (bool) { if (_interfaceID == type(IERC1155).interfaceId) { return true; } From da4401024001d40da8577c6b858ca982fd70f706 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 1 Jun 2023 09:18:16 +1200 Subject: [PATCH 4/4] Internal init function for parent contracts --- .../ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol index 6b7579d..248bb9f 100644 --- a/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol +++ b/src/contracts/tokens/ERC1155Upgradeable/ERC1155MetadataUpgradeable.sol @@ -23,6 +23,14 @@ contract ERC1155MetadataUpgradeable is Initializable, IERC1155Metadata, ERC165 { * @dev This function should be called once immediately after deployment. */ function initialize(string memory _name, string memory _baseURI) public virtual initializer { + _ERC1155MetadataUpgradeable_init(_name, _baseURI); + } + + /** + * @notice Set the initial name and base URI. + * @dev Use this function when extending the contract. + */ + function _ERC1155MetadataUpgradeable_init(string memory _name, string memory _baseURI) internal virtual onlyInitializing { _setContractName(_name); _setBaseMetadataURI(_baseURI); }