diff --git a/artifacts/ChainExitERC1155Predicate.json b/artifacts/ChainExitERC1155Predicate.json new file mode 100644 index 00000000..2243eb1b --- /dev/null +++ b/artifacts/ChainExitERC1155Predicate.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"depositor","type":"address"},{"indexed":true,"internalType":"address","name":"depositReceiver","type":"address"},{"indexed":true,"internalType":"address","name":"rootToken","type":"address"},{"indexed":false,"internalType":"uint256[]","name":"ids","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"LockedBatchChainExitERC1155","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"inputs":[],"name":"CHAIN_EXIT_EVENT_SIG","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOKEN_TYPE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"depositor","type":"address"},{"internalType":"address","name":"depositReceiver","type":"address"},{"internalType":"address","name":"rootToken","type":"address"},{"internalType":"bytes","name":"depositData","type":"bytes"}],"name":"lockTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"rootToken","type":"address"},{"internalType":"bytes","name":"log","type":"bytes"}],"name":"exitTokens","outputs":[],"stateMutability":"nonpayable","type":"function"}]} diff --git a/artifacts/ChainExitERC1155PredicateProxy.json b/artifacts/ChainExitERC1155PredicateProxy.json new file mode 100644 index 00000000..ed7993e8 --- /dev/null +++ b/artifacts/ChainExitERC1155PredicateProxy.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[{"internalType":"address","name":"_proxyTo","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_new","type":"address"},{"indexed":false,"internalType":"address","name":"_old","type":"address"}],"name":"ProxyOwnerUpdate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_new","type":"address"},{"indexed":true,"internalType":"address","name":"_old","type":"address"}],"name":"ProxyUpdated","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxyOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxyType","outputs":[{"internalType":"uint256","name":"proxyTypeId","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferProxyOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newProxyTo","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"updateAndCall","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_newProxyTo","type":"address"}],"name":"updateImplementation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]} diff --git a/contracts/root/TokenPredicates/ChainExitERC1155Predicate.sol b/contracts/root/TokenPredicates/ChainExitERC1155Predicate.sol new file mode 100644 index 00000000..7200ad50 --- /dev/null +++ b/contracts/root/TokenPredicates/ChainExitERC1155Predicate.sol @@ -0,0 +1,221 @@ +pragma solidity 0.6.6; + +import {IMintableERC1155} from "../RootToken/IMintableERC1155.sol"; +import { + ERC1155Receiver +} from "@openzeppelin/contracts/token/ERC1155/ERC1155Receiver.sol"; +import {AccessControlMixin} from "../../common/AccessControlMixin.sol"; +import {RLPReader} from "../../lib/RLPReader.sol"; +import {ITokenPredicate} from "./ITokenPredicate.sol"; +import {Initializable} from "../../common/Initializable.sol"; + +contract ChainExitERC1155Predicate is + ITokenPredicate, + ERC1155Receiver, + AccessControlMixin, + Initializable +{ + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant TOKEN_TYPE = keccak256("ChainExitERC1155"); + // Only this event is considered in exit function : ChainExit(address indexed to, uint256[] tokenId, uint256[] amount, bytes data) + bytes32 public constant CHAIN_EXIT_EVENT_SIG = keccak256("ChainExit(address,uint256[],uint256[],bytes)"); + + event LockedBatchChainExitERC1155( + address indexed depositor, + address indexed depositReceiver, + address indexed rootToken, + uint256[] ids, + uint256[] amounts + ); + + constructor() public {} + + function initialize(address _owner) external initializer { + _setupContractId("ChainExitERC1155Predicate"); + _setupRole(DEFAULT_ADMIN_ROLE, _owner); + _setupRole(MANAGER_ROLE, _owner); + } + + /** + * @notice rejects single transfer + */ + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external override returns (bytes4) { + return 0; + } + + /** + * @notice accepts batch transfer + */ + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external override returns (bytes4) { + return ERC1155Receiver(0).onERC1155BatchReceived.selector; + } + + /** + * @notice Lock ERC1155 tokens for deposit, callable only by manager + * @param depositor Address who wants to deposit tokens + * @param depositReceiver Address (address) who wants to receive tokens on child chain + * @param rootToken Token which gets deposited + * @param depositData ABI encoded id array and amount array + */ + function lockTokens( + address depositor, + address depositReceiver, + address rootToken, + bytes calldata depositData + ) external override only(MANAGER_ROLE) { + // forcing batch deposit since supporting both single and batch deposit introduces too much complexity + ( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) = abi.decode(depositData, (uint256[], uint256[], bytes)); + + emit LockedBatchChainExitERC1155( + depositor, + depositReceiver, + rootToken, + ids, + amounts + ); + IMintableERC1155(rootToken).safeBatchTransferFrom( + depositor, + address(this), + ids, + amounts, + data + ); + } + + /** + * @notice Creates an array of `size` by repeating provided address, + * to be required for passing to batched balance checking function of ERC1155 tokens. + * @param addr Address to be repeated `size` times in resulting array + * @param size Size of resulting array + */ + function makeArrayWithAddress(address addr, uint256 size) + internal + pure + returns (address[] memory) + { + require( + addr != address(0), + "ChainExitERC1155Predicate: Invalid address" + ); + require( + size > 0, + "ChainExitERC1155Predicate: Invalid resulting array length" + ); + + address[] memory addresses = new address[](size); + + for (uint256 i = 0; i < size; i++) { + addresses[i] = addr; + } + + return addresses; + } + + /** + * @notice Calculates amount of tokens to be minted, by subtracting available + * token balances from amount of tokens to be exited + * @param balances Token balances this contract holds for some ordered token ids + * @param exitAmounts Amount of tokens being exited + */ + function calculateAmountsToBeMinted( + uint256[] memory balances, + uint256[] memory exitAmounts + ) internal pure returns (uint256[] memory, bool, bool) { + uint256 count = balances.length; + require( + count == exitAmounts.length, + "ChainExitERC1155Predicate: Array length mismatch found" + ); + + uint256[] memory toBeMinted = new uint256[](count); + bool needMintStep; + bool needTransferStep; + + for (uint256 i = 0; i < count; i++) { + if (balances[i] < exitAmounts[i]) { + toBeMinted[i] = exitAmounts[i] - balances[i]; + needMintStep = true; + } + + if(balances[i] != 0) { + needTransferStep = true; + } + } + + return (toBeMinted, needMintStep, needTransferStep); + } + + /** + * @notice Validates log signature, withdrawer address + * then sends the correct tokenId, amount to withdrawer + * callable only by manager + * @param rootToken Token which gets withdrawn + * @param log Valid ChainExit log from child chain + */ + function exitTokens( + address, + address rootToken, + bytes memory log + ) public override only(MANAGER_ROLE) { + RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList(); + RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); + bytes memory logData = logRLPList[2].toBytes(); + + if (bytes32(logTopicRLPList[0].toUint()) == CHAIN_EXIT_EVENT_SIG) { + + address withdrawer = address(logTopicRLPList[1].toUint()); + require(withdrawer != address(0), "ChainExitERC1155Predicate: INVALID_RECEIVER"); + + (uint256[] memory ids, uint256[] memory amounts, bytes memory data) = abi.decode( + logData, + (uint256[], uint256[], bytes) + ); + + IMintableERC1155 token = IMintableERC1155(rootToken); + + uint256[] memory balances = token.balanceOfBatch(makeArrayWithAddress(address(this), ids.length), ids); + (uint256[] memory toBeMinted, bool needMintStep, bool needTransferStep) = calculateAmountsToBeMinted(balances, amounts); + + if(needMintStep) { + token.mintBatch( + withdrawer, + ids, + toBeMinted, + data // passing data when minting to withdrawer + ); + } + + if(needTransferStep) { + token.safeBatchTransferFrom( + address(this), + withdrawer, + ids, + balances, + data // passing data when transferring unlocked tokens to withdrawer + ); + } + + } else { + revert("ChainExitERC1155Predicate: INVALID_WITHDRAW_SIG"); + } + } +} diff --git a/contracts/root/TokenPredicates/ChainExitERC1155PredicateProxy.sol b/contracts/root/TokenPredicates/ChainExitERC1155PredicateProxy.sol new file mode 100644 index 00000000..d393dc4a --- /dev/null +++ b/contracts/root/TokenPredicates/ChainExitERC1155PredicateProxy.sol @@ -0,0 +1,10 @@ +pragma solidity 0.6.6; + +import {UpgradableProxy} from "../../common/Proxy/UpgradableProxy.sol"; + +contract ChainExitERC1155PredicateProxy is UpgradableProxy { + constructor(address _proxyTo) + public + UpgradableProxy(_proxyTo) + {} +} diff --git a/flat/ChainExitERC1155Predicate.sol b/flat/ChainExitERC1155Predicate.sol new file mode 100644 index 00000000..7a1f0ce1 --- /dev/null +++ b/flat/ChainExitERC1155Predicate.sol @@ -0,0 +1,1494 @@ + +// File: @openzeppelin/contracts/introspection/IERC165.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +// File: @openzeppelin/contracts/token/ERC1155/IERC1155.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 is IERC165 { + /** + * @dev Emitted when `value` tokens of token type `id` are transfered from `from` to `to` by `operator`. + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; +} + +// File: contracts/root/RootToken/IMintableERC1155.sol + +pragma solidity 0.6.6; + +interface IMintableERC1155 is IERC1155 { + /** + * @notice Creates `amount` tokens of token type `id`, and assigns them to `account`. + * @dev Should be callable only by MintableERC1155Predicate + * Make sure minting is done only by this function + * @param account user address for whom token is being minted + * @param id token which is being minted + * @param amount amount of token being minted + * @param data extra byte data to be accompanied with minted tokens + */ + function mint(address account, uint256 id, uint256 amount, bytes calldata data) external; + + /** + * @notice Batched version of singular token minting, where + * for each token in `ids` respective amount to be minted from `amounts` + * array, for address `to`. + * @dev Should be callable only by MintableERC1155Predicate + * Make sure minting is done only by this function + * @param to user address for whom token is being minted + * @param ids tokens which are being minted + * @param amounts amount of each token being minted + * @param data extra byte data to be accompanied with minted tokens + */ + function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; +} + +// File: @openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + +/** + * _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + + /** + @dev Handles the receipt of a single ERC1155 token type. This function is + called at the end of a `safeTransferFrom` after the balance has been updated. + To accept the transfer, this must return + `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + (i.e. 0xf23a6e61, or its own function selector). + @param operator The address which initiated the transfer (i.e. msg.sender) + @param from The address which previously owned the token + @param id The ID of the token being transferred + @param value The amount of tokens being transferred + @param data Additional data with no specified format + @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) + external + returns(bytes4); + + /** + @dev Handles the receipt of a multiple ERC1155 token types. This function + is called at the end of a `safeBatchTransferFrom` after the balances have + been updated. To accept the transfer(s), this must return + `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + (i.e. 0xbc197c81, or its own function selector). + @param operator The address which initiated the batch transfer (i.e. msg.sender) + @param from The address which previously owned the token + @param ids An array containing ids of each token being transferred (order and length must match values array) + @param values An array containing amounts of each token being transferred (order and length must match ids array) + @param data Additional data with no specified format + @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) + external + returns(bytes4); +} + +// File: @openzeppelin/contracts/introspection/ERC165.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts may inherit from this and call {_registerInterface} to declare + * their support of an interface. + */ +contract ERC165 is IERC165 { + /* + * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7 + */ + bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; + + /** + * @dev Mapping of interface ids to whether or not it's supported. + */ + mapping(bytes4 => bool) private _supportedInterfaces; + + constructor () internal { + // Derived contracts need only register support for their own interfaces, + // we register support for ERC165 itself here + _registerInterface(_INTERFACE_ID_ERC165); + } + + /** + * @dev See {IERC165-supportsInterface}. + * + * Time complexity O(1), guaranteed to always use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return _supportedInterfaces[interfaceId]; + } + + /** + * @dev Registers the contract as an implementer of the interface defined by + * `interfaceId`. Support of the actual ERC165 interface is automatic and + * registering its interface id is not required. + * + * See {IERC165-supportsInterface}. + * + * Requirements: + * + * - `interfaceId` cannot be the ERC165 invalid interface (`0xffffffff`). + */ + function _registerInterface(bytes4 interfaceId) internal virtual { + require(interfaceId != 0xffffffff, "ERC165: invalid interface id"); + _supportedInterfaces[interfaceId] = true; + } +} + +// File: @openzeppelin/contracts/token/ERC1155/ERC1155Receiver.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + + +/** + * @dev _Available since v3.1._ + */ +abstract contract ERC1155Receiver is ERC165, IERC1155Receiver { + constructor() public { + _registerInterface( + ERC1155Receiver(0).onERC1155Received.selector ^ + ERC1155Receiver(0).onERC1155BatchReceived.selector + ); + } +} + +// File: @openzeppelin/contracts/utils/EnumerableSet.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.0.0, only sets of type `address` (`AddressSet`) and `uint256` + * (`UintSet`) are supported. + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping (bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + // When the value to delete is the last one, the swap operation is unnecessary. However, since this occurs + // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement. + + bytes32 lastvalue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastvalue; + // Update the index for the moved value + set._indexes[lastvalue] = toDeleteIndex + 1; // All indexes are 1-based + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + require(set._values.length > index, "EnumerableSet: index out of bounds"); + return set._values[index]; + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint256(_at(set._inner, index))); + } + + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } +} + +// File: @openzeppelin/contracts/utils/Address.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // According to EIP-1052, 0x0 is the value returned for not-yet created accounts + // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned + // for accounts without code, i.e. `keccak256('')` + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { codehash := extcodehash(account) } + return (codehash != accountHash && codehash != 0x0); + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return _functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + return _functionCallWithValue(target, data, value, errorMessage); + } + + function _functionCallWithValue(address target, bytes memory data, uint256 weiValue, string memory errorMessage) private returns (bytes memory) { + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: weiValue }(data); + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// File: @openzeppelin/contracts/GSN/Context.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/* + * @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 GSN 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 Context { + function _msgSender() internal view virtual returns (address payable) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +// File: @openzeppelin/contracts/access/AccessControl.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + + + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ``` + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ``` + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. + */ +abstract contract AccessControl is Context { + using EnumerableSet for EnumerableSet.AddressSet; + using Address for address; + + struct RoleData { + EnumerableSet.AddressSet members; + bytes32 adminRole; + } + + mapping (bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role].members.contains(account); + } + + /** + * @dev Returns the number of accounts that have `role`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(bytes32 role) public view returns (uint256) { + return _roles[role].members.length(); + } + + /** + * @dev Returns one of the accounts that have `role`. `index` must be a + * value between 0 and {getRoleMemberCount}, non-inclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may + * change at any point. + * + * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleMember(bytes32 role, uint256 index) public view returns (address) { + return _roles[role].members.at(index); + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) public virtual { + require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant"); + + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) public virtual { + require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke"); + + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) public virtual { + require(account == _msgSender(), "AccessControl: can only renounce roles for self"); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + */ + function _setupRole(bytes32 role, address account) internal virtual { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + emit RoleAdminChanged(role, _roles[role].adminRole, adminRole); + _roles[role].adminRole = adminRole; + } + + function _grantRole(bytes32 role, address account) private { + if (_roles[role].members.add(account)) { + emit RoleGranted(role, account, _msgSender()); + } + } + + function _revokeRole(bytes32 role, address account) private { + if (_roles[role].members.remove(account)) { + emit RoleRevoked(role, account, _msgSender()); + } + } +} + +// File: contracts/common/AccessControlMixin.sol + +pragma solidity 0.6.6; + + +contract AccessControlMixin is AccessControl { + string private _revertMsg; + function _setupContractId(string memory contractId) internal { + _revertMsg = string(abi.encodePacked(contractId, ": INSUFFICIENT_PERMISSIONS")); + } + + modifier only(bytes32 role) { + require( + hasRole(role, _msgSender()), + _revertMsg + ); + _; + } +} + +// File: contracts/lib/RLPReader.sol + +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + * https://github.com/hamdiallam/Solidity-RLP/blob/e681e25a376dbd5426b509380bc03446f05d0f97/contracts/RLPReader.sol + */ +pragma solidity 0.6.6; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) + internal + pure + returns (RLPItem memory) + { + require(item.length > 0, "RLPReader: INVALID_BYTES_LENGTH"); + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) + internal + pure + returns (RLPItem[] memory) + { + require(isList(item), "RLPReader: ITEM_NOT_LIST"); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + uint256 listLength = _itemLength(item.memPtr); + require(listLength == item.len, "RLPReader: LIST_DECODED_LENGTH_MISMATCH"); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) + internal + pure + returns (bytes memory) + { + bytes memory result = new bytes(item.len); + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + require(!isList(item), "RLPReader: DECODING_LIST_AS_ADDRESS"); + // 1 byte for the length prefix + require(item.len == 21, "RLPReader: INVALID_ADDRESS_LENGTH"); + + return address(toUint(item)); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(!isList(item), "RLPReader: DECODING_LIST_AS_UINT"); + require(item.len <= 33, "RLPReader: INVALID_UINT_LENGTH"); + + uint256 itemLength = _itemLength(item.memPtr); + require(itemLength == item.len, "RLPReader: UINT_DECODED_LENGTH_MISMATCH"); + + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + uint256 result; + uint256 memPtr = item.memPtr + offset; + assembly { + result := mload(memPtr) + + // shfit to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + uint256 itemLength = _itemLength(item.memPtr); + require(itemLength == item.len, "RLPReader: UINT_STRICT_DECODED_LENGTH_MISMATCH"); + // one byte prefix + require(item.len == 33, "RLPReader: INVALID_UINT_STRICT_LENGTH"); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + uint256 listLength = _itemLength(item.memPtr); + require(listLength == item.len, "RLPReader: BYTES_DECODED_LENGTH_MISMATCH"); + uint256 offset = _payloadOffset(item.memPtr); + + uint256 len = item.len - offset; // data length + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + // add `isList` check if `item` is expected to be passsed without a check from calling function + // require(isList(item), "RLPReader: NUM_ITEMS_NOT_LIST"); + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + require(currPtr <= endPtr, "RLPReader: NUM_ITEMS_DECODED_LENGTH_MISMATCH"); + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) itemLen = 1; + else if (byte0 < STRING_LONG_START) + itemLen = byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) return 0; + else if ( + byte0 < STRING_LONG_START || + (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START) + ) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint256 src, + uint256 dest, + uint256 len + ) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } +} + +// File: contracts/root/TokenPredicates/ITokenPredicate.sol + +pragma solidity 0.6.6; + + +/// @title Token predicate interface for all pos portal predicates +/// @notice Abstract interface that defines methods for custom predicates +interface ITokenPredicate { + + /** + * @notice Deposit tokens into pos portal + * @dev When `depositor` deposits tokens into pos portal, tokens get locked into predicate contract. + * @param depositor Address who wants to deposit tokens + * @param depositReceiver Address (address) who wants to receive tokens on side chain + * @param rootToken Token which gets deposited + * @param depositData Extra data for deposit (amount for ERC20, token id for ERC721 etc.) [ABI encoded] + */ + function lockTokens( + address depositor, + address depositReceiver, + address rootToken, + bytes calldata depositData + ) external; + + /** + * @notice Validates and processes exit while withdraw process + * @dev Validates exit log emitted on sidechain. Reverts if validation fails. + * @dev Processes withdraw based on custom logic. Example: transfer ERC20/ERC721, mint ERC721 if mintable withdraw + * @param sender Address + * @param rootToken Token which gets withdrawn + * @param logRLPList Valid sidechain log for data like amount, token id etc. + */ + function exitTokens( + address sender, + address rootToken, + bytes calldata logRLPList + ) external; +} + +// File: contracts/common/Initializable.sol + +pragma solidity 0.6.6; + +contract Initializable { + bool inited = false; + + modifier initializer() { + require(!inited, "already inited"); + _; + inited = true; + } +} + +// File: contracts/root/TokenPredicates/ChainExitERC1155Predicate.sol + +pragma solidity 0.6.6; + + + ERC1155Receiver +} from "@openzeppelin/contracts/token/ERC1155/ERC1155Receiver.sol"; + + + + + +contract ChainExitERC1155Predicate is + ITokenPredicate, + ERC1155Receiver, + AccessControlMixin, + Initializable +{ + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant TOKEN_TYPE = keccak256("ChainExitERC1155"); + // Only this event is considered in exit function : ChainExit(address indexed to, uint256[] tokenId, uint256[] amount, bytes data) + bytes32 public constant CHAIN_EXIT_EVENT_SIG = keccak256("ChainExit(address,uint256[],uint256[],bytes)"); + + event LockedBatchChainExitERC1155( + address indexed depositor, + address indexed depositReceiver, + address indexed rootToken, + uint256[] ids, + uint256[] amounts + ); + + constructor() public {} + + function initialize(address _owner) external initializer { + _setupContractId("ChainExitERC1155Predicate"); + _setupRole(DEFAULT_ADMIN_ROLE, _owner); + _setupRole(MANAGER_ROLE, _owner); + } + + /** + * @notice rejects single transfer + */ + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external override returns (bytes4) { + return 0; + } + + /** + * @notice accepts batch transfer + */ + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external override returns (bytes4) { + return ERC1155Receiver(0).onERC1155BatchReceived.selector; + } + + /** + * @notice Lock ERC1155 tokens for deposit, callable only by manager + * @param depositor Address who wants to deposit tokens + * @param depositReceiver Address (address) who wants to receive tokens on child chain + * @param rootToken Token which gets deposited + * @param depositData ABI encoded id array and amount array + */ + function lockTokens( + address depositor, + address depositReceiver, + address rootToken, + bytes calldata depositData + ) external override only(MANAGER_ROLE) { + // forcing batch deposit since supporting both single and batch deposit introduces too much complexity + ( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) = abi.decode(depositData, (uint256[], uint256[], bytes)); + + emit LockedBatchChainExitERC1155( + depositor, + depositReceiver, + rootToken, + ids, + amounts + ); + IMintableERC1155(rootToken).safeBatchTransferFrom( + depositor, + address(this), + ids, + amounts, + data + ); + } + + /** + * @notice Creates an array of `size` by repeating provided address, + * to be required for passing to batched balance checking function of ERC1155 tokens. + * @param addr Address to be repeated `size` times in resulting array + * @param size Size of resulting array + */ + function makeArrayWithAddress(address addr, uint256 size) + internal + pure + returns (address[] memory) + { + require( + addr != address(0), + "ChainExitERC1155Predicate: Invalid address" + ); + require( + size > 0, + "ChainExitERC1155Predicate: Invalid resulting array length" + ); + + address[] memory addresses = new address[](size); + + for (uint256 i = 0; i < size; i++) { + addresses[i] = addr; + } + + return addresses; + } + + /** + * @notice Calculates amount of tokens to be minted, by subtracting available + * token balances from amount of tokens to be exited + * @param balances Token balances this contract holds for some ordered token ids + * @param exitAmounts Amount of tokens being exited + */ + function calculateAmountsToBeMinted( + uint256[] memory balances, + uint256[] memory exitAmounts + ) internal pure returns (uint256[] memory, bool, bool) { + uint256 count = balances.length; + require( + count == exitAmounts.length, + "ChainExitERC1155Predicate: Array length mismatch found" + ); + + uint256[] memory toBeMinted = new uint256[](count); + bool needMintStep; + bool needTransferStep; + + for (uint256 i = 0; i < count; i++) { + if (balances[i] < exitAmounts[i]) { + toBeMinted[i] = exitAmounts[i] - balances[i]; + needMintStep = true; + } + + if(balances[i] != 0) { + needTransferStep = true; + } + } + + return (toBeMinted, needMintStep, needTransferStep); + } + + /** + * @notice Validates log signature, withdrawer address + * then sends the correct tokenId, amount to withdrawer + * callable only by manager + * @param rootToken Token which gets withdrawn + * @param log Valid ChainExit log from child chain + */ + function exitTokens( + address, + address rootToken, + bytes memory log + ) public override only(MANAGER_ROLE) { + RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList(); + RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); + bytes memory logData = logRLPList[2].toBytes(); + + if (bytes32(logTopicRLPList[0].toUint()) == CHAIN_EXIT_EVENT_SIG) { + + address withdrawer = address(logTopicRLPList[1].toUint()); + require(withdrawer != address(0), "ChainExitERC1155Predicate: INVALID_RECEIVER"); + + (uint256[] memory ids, uint256[] memory amounts, bytes memory data) = abi.decode( + logData, + (uint256[], uint256[], bytes) + ); + + IMintableERC1155 token = IMintableERC1155(rootToken); + + uint256[] memory balances = token.balanceOfBatch(makeArrayWithAddress(address(this), ids.length), ids); + (uint256[] memory toBeMinted, bool needMintStep, bool needTransferStep) = calculateAmountsToBeMinted(balances, amounts); + + if(needMintStep) { + token.mintBatch( + withdrawer, + ids, + toBeMinted, + data // passing data when minting to withdrawer + ); + } + + if(needTransferStep) { + token.safeBatchTransferFrom( + address(this), + withdrawer, + ids, + balances, + data // passing data when transferring unlocked tokens to withdrawer + ); + } + + } else { + revert("ChainExitERC1155Predicate: INVALID_WITHDRAW_SIG"); + } + } +} diff --git a/flat/ChainExitERC1155PredicateProxy.sol b/flat/ChainExitERC1155PredicateProxy.sol new file mode 100644 index 00000000..64a17d41 --- /dev/null +++ b/flat/ChainExitERC1155PredicateProxy.sol @@ -0,0 +1,169 @@ + +// File: contracts/common/Proxy/IERCProxy.sol + +pragma solidity 0.6.6; + +interface IERCProxy { + function proxyType() external pure returns (uint256 proxyTypeId); + + function implementation() external view returns (address codeAddr); +} + +// File: contracts/common/Proxy/Proxy.sol + +pragma solidity 0.6.6; + + +abstract contract Proxy is IERCProxy { + function delegatedFwd(address _dst, bytes memory _calldata) internal { + // solium-disable-next-line security/no-inline-assembly + assembly { + let result := delegatecall( + sub(gas(), 10000), + _dst, + add(_calldata, 0x20), + mload(_calldata), + 0, + 0 + ) + let size := returndatasize() + + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + + // revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas. + // if the call returned error data, forward it + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } + + function proxyType() external virtual override pure returns (uint256 proxyTypeId) { + // Upgradeable proxy + proxyTypeId = 2; + } + + function implementation() external virtual override view returns (address); +} + +// File: contracts/common/Proxy/UpgradableProxy.sol + +pragma solidity 0.6.6; + + +contract UpgradableProxy is Proxy { + event ProxyUpdated(address indexed _new, address indexed _old); + event ProxyOwnerUpdate(address _new, address _old); + + bytes32 constant IMPLEMENTATION_SLOT = keccak256("matic.network.proxy.implementation"); + bytes32 constant OWNER_SLOT = keccak256("matic.network.proxy.owner"); + + constructor(address _proxyTo) public { + setProxyOwner(msg.sender); + setImplementation(_proxyTo); + } + + fallback() external payable { + delegatedFwd(loadImplementation(), msg.data); + } + + receive() external payable { + delegatedFwd(loadImplementation(), msg.data); + } + + modifier onlyProxyOwner() { + require(loadProxyOwner() == msg.sender, "NOT_OWNER"); + _; + } + + function proxyOwner() external view returns(address) { + return loadProxyOwner(); + } + + function loadProxyOwner() internal view returns(address) { + address _owner; + bytes32 position = OWNER_SLOT; + assembly { + _owner := sload(position) + } + return _owner; + } + + function implementation() external override view returns (address) { + return loadImplementation(); + } + + function loadImplementation() internal view returns(address) { + address _impl; + bytes32 position = IMPLEMENTATION_SLOT; + assembly { + _impl := sload(position) + } + return _impl; + } + + function transferProxyOwnership(address newOwner) public onlyProxyOwner { + require(newOwner != address(0), "ZERO_ADDRESS"); + emit ProxyOwnerUpdate(newOwner, loadProxyOwner()); + setProxyOwner(newOwner); + } + + function setProxyOwner(address newOwner) private { + bytes32 position = OWNER_SLOT; + assembly { + sstore(position, newOwner) + } + } + + function updateImplementation(address _newProxyTo) public onlyProxyOwner { + require(_newProxyTo != address(0x0), "INVALID_PROXY_ADDRESS"); + require(isContract(_newProxyTo), "DESTINATION_ADDRESS_IS_NOT_A_CONTRACT"); + + emit ProxyUpdated(_newProxyTo, loadImplementation()); + + setImplementation(_newProxyTo); + } + + function updateAndCall(address _newProxyTo, bytes memory data) payable public onlyProxyOwner { + updateImplementation(_newProxyTo); + + (bool success, bytes memory returnData) = address(this).call{value: msg.value}(data); + require(success, string(returnData)); + } + + function setImplementation(address _newProxyTo) private { + bytes32 position = IMPLEMENTATION_SLOT; + assembly { + sstore(position, _newProxyTo) + } + } + + function isContract(address _target) internal view returns (bool) { + if (_target == address(0)) { + return false; + } + + uint256 size; + assembly { + size := extcodesize(_target) + } + return size > 0; + } +} + +// File: contracts/root/TokenPredicates/ChainExitERC1155PredicateProxy.sol + +pragma solidity 0.6.6; + + +contract ChainExitERC1155PredicateProxy is UpgradableProxy { + constructor(address _proxyTo) + public + UpgradableProxy(_proxyTo) + {} +} diff --git a/scripts/flatten-contracts.js b/scripts/flatten-contracts.js index 8d1b206a..2c4fdad6 100644 --- a/scripts/flatten-contracts.js +++ b/scripts/flatten-contracts.js @@ -59,6 +59,14 @@ const contractsToFlatten = [ path: 'contracts/root/TokenPredicates', fileName: 'MintableERC1155PredicateProxy.sol' }, + { + path: 'contracts/root/TokenPredicates', + fileName: 'ChainExitERC1155Predicate.sol' + }, + { + path: 'contracts/root/TokenPredicates', + fileName: 'ChainExitERC1155PredicateProxy.sol' + }, { path: 'contracts/root/TokenPredicates', fileName: 'EtherPredicate.sol' diff --git a/scripts/generate-artifacts.js b/scripts/generate-artifacts.js index 260180f0..5a844dac 100644 --- a/scripts/generate-artifacts.js +++ b/scripts/generate-artifacts.js @@ -19,7 +19,9 @@ const artifactsToGenerate = [ 'BaseRootTunnel.json', 'BaseChildTunnel.json', 'RootTunnel.json', - 'ChildTunnel.json' + 'ChildTunnel.json', + 'ChainExitERC1155Predicate.json', + 'ChainExitERC1155PredicateProxy.json' ] artifactsToGenerate.forEach(a => { diff --git a/test/helpers/constants.js b/test/helpers/constants.js index 6dbe2145..41122c54 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -20,6 +20,7 @@ export const erc721TransferEventSig = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f export const erc721TransferWithMetadataEventSig = '0xf94915c6d1fd521cee85359239227480c7e8776d7caf1fc3bacad5c269b66a14' export const erc1155TransferSingleEventSig = '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62' export const erc1155TransferBatchEventSig = '0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb' +export const erc1155ChainExitEventSig = '0xc7b80b68f1c661da97dbd7e6e143a0c7c587dfc522cb2ac508b9084fecc492bc' export const mockValues = { zeroAddress: '0x0000000000000000000000000000000000000000', diff --git a/test/helpers/contracts.js b/test/helpers/contracts.js index bced9f99..551c382e 100644 --- a/test/helpers/contracts.js +++ b/test/helpers/contracts.js @@ -19,6 +19,8 @@ const ERC1155Predicate = artifacts.require('ERC1155Predicate') const ERC1155PredicateProxy = artifacts.require('ERC1155PredicateProxy') const MintableERC1155Predicate = artifacts.require('MintableERC1155Predicate') const MintableERC1155PredicateProxy = artifacts.require('MintableERC1155PredicateProxy') +const ChainExitERC1155Predicate = artifacts.require('ChainExitERC1155Predicate') +const ChainExitERC1155PredicateProxy = artifacts.require('ChainExitERC1155PredicateProxy') const EtherPredicate = artifacts.require('EtherPredicate') const EtherPredicateProxy = artifacts.require('EtherPredicateProxy') const DummyERC20 = artifacts.require('DummyERC20') @@ -81,6 +83,8 @@ setWeb3(ERC1155Predicate, rootWeb3) setWeb3(ERC1155PredicateProxy, rootWeb3) setWeb3(MintableERC1155Predicate, rootWeb3) setWeb3(MintableERC1155PredicateProxy, rootWeb3) +setWeb3(ChainExitERC1155Predicate, rootWeb3) +setWeb3(ChainExitERC1155PredicateProxy, rootWeb3) setWeb3(EtherPredicate, rootWeb3) setWeb3(EtherPredicateProxy, rootWeb3) setWeb3(DummyERC20, rootWeb3) @@ -130,6 +134,8 @@ export default { ERC1155PredicateProxy, MintableERC1155Predicate, MintableERC1155PredicateProxy, + ChainExitERC1155Predicate, + ChainExitERC1155PredicateProxy, EtherPredicate, EtherPredicateProxy, DummyERC20, diff --git a/test/helpers/deployer.js b/test/helpers/deployer.js index fb89514c..6a0a0e21 100644 --- a/test/helpers/deployer.js +++ b/test/helpers/deployer.js @@ -12,6 +12,7 @@ export const deployFreshRootContracts = async(accounts) => { mintableERC721PredicateLogic, erc1155PredicateLogic, mintableERC1155PredicateLogic, + chainExitERC1155PredicateLogic, etherPredicateLogic, dummyERC20, dummyMintableERC20, @@ -29,6 +30,7 @@ export const deployFreshRootContracts = async(accounts) => { contracts.MintableERC721Predicate.new(), contracts.ERC1155Predicate.new(), contracts.MintableERC1155Predicate.new(), + contracts.ChainExitERC1155Predicate.new(), contracts.EtherPredicate.new(), contracts.DummyERC20.new('Dummy ERC20', 'DERC20'), contracts.DummyMintableERC20.new('Dummy Mintable ERC20', 'DMERC20'), @@ -66,6 +68,10 @@ export const deployFreshRootContracts = async(accounts) => { await mintableERC1155PredicateProxy.updateAndCall(mintableERC1155PredicateLogic.address, mintableERC1155PredicateLogic.contract.methods.initialize(accounts[0]).encodeABI()) const mintableERC1155Predicate = await contracts.MintableERC1155Predicate.at(mintableERC1155PredicateProxy.address) + const chainExitERC1155PredicateProxy = await contracts.ChainExitERC1155PredicateProxy.new('0x0000000000000000000000000000000000000000') + await chainExitERC1155PredicateProxy.updateAndCall(chainExitERC1155PredicateLogic.address, chainExitERC1155PredicateLogic.contract.methods.initialize(accounts[0]).encodeABI()) + const chainExitERC1155Predicate = await contracts.ChainExitERC1155Predicate.at(chainExitERC1155PredicateProxy.address) + const etherPredicateProxy = await contracts.EtherPredicateProxy.new('0x0000000000000000000000000000000000000000') await etherPredicateProxy.updateAndCall(etherPredicateLogic.address, etherPredicateLogic.contract.methods.initialize(accounts[0]).encodeABI()) const etherPredicate = await contracts.EtherPredicate.at(etherPredicateProxy.address) @@ -80,6 +86,7 @@ export const deployFreshRootContracts = async(accounts) => { mintableERC721Predicate, erc1155Predicate, mintableERC1155Predicate, + chainExitERC1155Predicate, etherPredicate, dummyERC20, dummyMintableERC20, @@ -184,6 +191,12 @@ export const deployInitializedContracts = async(accounts) => { await root.dummyMintableERC1155.grantRole(PREDICATE_ROLE, root.mintableERC1155Predicate.address) + const ChainExitERC1155Type = await root.chainExitERC1155Predicate.TOKEN_TYPE() + await root.chainExitERC1155Predicate.grantRole(MANAGER_ROLE, root.rootChainManager.address) + await root.rootChainManager.registerPredicate(ChainExitERC1155Type, root.chainExitERC1155Predicate.address) + + await root.dummyMintableERC1155.grantRole(PREDICATE_ROLE, root.chainExitERC1155Predicate.address) + const EtherType = await root.etherPredicate.TOKEN_TYPE() await root.etherPredicate.grantRole(MANAGER_ROLE, root.rootChainManager.address) await root.rootChainManager.registerPredicate(EtherType, root.etherPredicate.address) diff --git a/test/helpers/logs.js b/test/helpers/logs.js index d729c161..05c60afa 100644 --- a/test/helpers/logs.js +++ b/test/helpers/logs.js @@ -1,12 +1,13 @@ import { RLP } from 'ethers/utils' -import { AbiCoder } from 'ethers/utils' +import { AbiCoder, toUtf8Bytes } from 'ethers/utils' import { erc20TransferEventSig, erc721TransferEventSig, erc721TransferWithMetadataEventSig, erc1155TransferSingleEventSig, - erc1155TransferBatchEventSig + erc1155TransferBatchEventSig, + erc1155ChainExitEventSig } from './constants' const abi = new AbiCoder() @@ -121,3 +122,31 @@ export const getERC1155TransferBatchLog = ({ ) ]) } + +export const getERC1155ChainExitLog = ({ + overrideSig, + to, + tokenIds, + amounts, + data +}) => { + return RLP.encode([ + '0x0', + [ + overrideSig || erc1155ChainExitEventSig, + to + ], + abi.encode( + [ + 'uint256[]', + 'uint256[]', + 'bytes' + ], + [ + tokenIds.map(t => '0x' + t.toString(16)), + amounts.map(a => '0x' + a.toString(16)), + `0x${Buffer.from(toUtf8Bytes(data || 'Hello World')).toString('hex')}` + ] + ) + ]) +} diff --git a/test/predicates/ChainExitERC1155Predicate.test.js b/test/predicates/ChainExitERC1155Predicate.test.js new file mode 100644 index 00000000..847ad369 --- /dev/null +++ b/test/predicates/ChainExitERC1155Predicate.test.js @@ -0,0 +1,306 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import chaiBN from 'chai-bn' +import BN from 'bn.js' +import { expectRevert } from '@openzeppelin/test-helpers' + +import * as deployer from '../helpers/deployer' +import { mockValues } from '../helpers/constants' +import logDecoder from '../helpers/log-decoder.js' +import { constructERC1155DepositData } from '../helpers/utils' +import { getERC1155ChainExitLog } from '../helpers/logs' + +// Enable and inject BN dependency +chai + .use(chaiAsPromised) + .use(chaiBN(BN)) + .should() + +const should = chai.should() + +contract('ChainExitERC1155Predicate', (accounts) => { + describe('lockTokens', () => { + const tokenIdA = mockValues.numbers[2] + const tokenIdB = mockValues.numbers[7] + const amountA = mockValues.amounts[0] + const amountB = mockValues.amounts[1] + const depositReceiver = mockValues.addresses[7] + const depositor = accounts[1] + const depositData = constructERC1155DepositData([tokenIdA, tokenIdB], [amountA, amountB]) + + let dummyMintableERC1155 + let chainExitERC1155Predicate + let lockTokensTx + let lockedLog + let oldAccountBalanceA + let oldAccountBalanceB + let oldContractBalanceA + let oldContractBalanceB + + before(async () => { + const contracts = await deployer.deployFreshRootContracts(accounts) + dummyMintableERC1155 = contracts.dummyMintableERC1155 + chainExitERC1155Predicate = contracts.chainExitERC1155Predicate + + const PREDICATE_ROLE = await dummyMintableERC1155.PREDICATE_ROLE() + await dummyMintableERC1155.grantRole(PREDICATE_ROLE, chainExitERC1155Predicate.address) + + await dummyMintableERC1155.mintBatch(depositor, [tokenIdA, tokenIdB], [amountA, amountB], '0x0') + await dummyMintableERC1155.setApprovalForAll(chainExitERC1155Predicate.address, true, { from: depositor }) + + oldAccountBalanceA = await dummyMintableERC1155.balanceOf(depositor, tokenIdA) + oldAccountBalanceB = await dummyMintableERC1155.balanceOf(depositor, tokenIdB) + oldContractBalanceA = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenIdA) + oldContractBalanceB = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenIdB) + }) + + it('Depositor should have balance', () => { + amountA.should.be.a.bignumber.at.most(oldAccountBalanceA) + amountB.should.be.a.bignumber.at.most(oldAccountBalanceB) + }) + + it('Depositor should have approved token transfer', async () => { + const approved = await dummyMintableERC1155.isApprovedForAll(depositor, chainExitERC1155Predicate.address) + approved.should.equal(true) + }) + + it('Should be able to receive lockTokens tx', async () => { + lockTokensTx = await chainExitERC1155Predicate.lockTokens(depositor, depositReceiver, dummyMintableERC1155.address, depositData) + should.exist(lockTokensTx) + }) + + it('Should emit LockedBatchChainExitERC1155 log', () => { + const logs = logDecoder.decodeLogs(lockTokensTx.receipt.rawLogs) + lockedLog = logs.find(l => l.event === 'LockedBatchChainExitERC1155') + should.exist(lockedLog) + }) + + describe('Correct values should be emitted in LockedBatchChainExitERC1155 log', () => { + it('Event should be emitted by correct contract', () => { + lockedLog.address.should.equal( + chainExitERC1155Predicate.address.toLowerCase() + ) + }) + + it('Should emit proper depositor', () => { + lockedLog.args.depositor.should.equal(depositor) + }) + + it('Should emit proper deposit receiver', () => { + lockedLog.args.depositReceiver.should.equal(depositReceiver) + }) + + it('Should emit proper root token', () => { + lockedLog.args.rootToken.should.equal(dummyMintableERC1155.address) + }) + + it('Should emit proper token id for A', () => { + const id = lockedLog.args.ids[0].toNumber() + id.should.equal(tokenIdA) + }) + + it('Should emit proper token id for B', () => { + const id = lockedLog.args.ids[1].toNumber() + id.should.equal(tokenIdB) + }) + + it('Should emit proper amount for A', () => { + const amounts = lockedLog.args.amounts + const amount = new BN(amounts[0].toString()) + amount.should.be.a.bignumber.that.equals(amountA) + }) + + it('Should emit proper amount for B', () => { + const amounts = lockedLog.args.amounts + const amount = new BN(amounts[1].toString()) + amount.should.be.a.bignumber.that.equals(amountB) + }) + }) + + it('Deposit amount should be deducted from depositor account for A', async () => { + const newAccountBalance = await dummyMintableERC1155.balanceOf(depositor, tokenIdA) + newAccountBalance.should.be.a.bignumber.that.equals( + oldAccountBalanceA.sub(amountA) + ) + }) + + it('Deposit amount should be deducted from depositor account for B', async () => { + const newAccountBalance = await dummyMintableERC1155.balanceOf(depositor, tokenIdB) + newAccountBalance.should.be.a.bignumber.that.equals( + oldAccountBalanceB.sub(amountB) + ) + }) + + it('Deposit amount should be credited to correct contract for A', async () => { + const newContractBalance = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenIdA) + newContractBalance.should.be.a.bignumber.that.equals( + oldContractBalanceA.add(amountA) + ) + }) + + it('Deposit amount should be credited to correct contract for B', async () => { + const newContractBalance = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenIdB) + newContractBalance.should.be.a.bignumber.that.equals( + oldContractBalanceB.add(amountB) + ) + }) + }) + + describe('lockTokens called by non manager', () => { + const tokenId = mockValues.numbers[5] + const amount = mockValues.amounts[9] + const depositData = constructERC1155DepositData([tokenId], [amount]) + const depositor = accounts[1] + const depositReceiver = accounts[2] + let dummyMintableERC1155 + let chainExitERC1155Predicate + + before(async () => { + const contracts = await deployer.deployFreshRootContracts(accounts) + dummyMintableERC1155 = contracts.dummyMintableERC1155 + chainExitERC1155Predicate = contracts.chainExitERC1155Predicate + + const PREDICATE_ROLE = await dummyMintableERC1155.PREDICATE_ROLE() + await dummyMintableERC1155.grantRole(PREDICATE_ROLE, chainExitERC1155Predicate.address) + + await dummyMintableERC1155.mint(depositor, tokenId, amount, '0x0') + await dummyMintableERC1155.setApprovalForAll(chainExitERC1155Predicate.address, true, { from: depositor }) + }) + + it('Should revert with correct reason', async () => { + await expectRevert( + chainExitERC1155Predicate.lockTokens(depositor, depositReceiver, dummyMintableERC1155.address, depositData, { from: depositor }), + 'ChainExitERC1155Predicate: INSUFFICIENT_PERMISSIONS') + }) + }) + + describe('exitTokens', () => { + const amount = mockValues.amounts[9] + const tokenId = mockValues.numbers[4] + const depositData = constructERC1155DepositData([tokenId], [amount]) + const depositor = accounts[1] + const withdrawer = mockValues.addresses[8] + let dummyMintableERC1155 + let chainExitERC1155Predicate + let exitTokensTx + let oldAccountBalance + let oldContractBalance + + before(async () => { + const contracts = await deployer.deployFreshRootContracts(accounts) + dummyMintableERC1155 = contracts.dummyMintableERC1155 + chainExitERC1155Predicate = contracts.chainExitERC1155Predicate + + const PREDICATE_ROLE = await dummyMintableERC1155.PREDICATE_ROLE() + await dummyMintableERC1155.grantRole(PREDICATE_ROLE, chainExitERC1155Predicate.address) + + await dummyMintableERC1155.mint(depositor, tokenId, amount, '0x0') + await dummyMintableERC1155.setApprovalForAll(chainExitERC1155Predicate.address, true, { from: depositor }) + + await chainExitERC1155Predicate.lockTokens(depositor, mockValues.addresses[2], dummyMintableERC1155.address, depositData) + oldAccountBalance = await dummyMintableERC1155.balanceOf(withdrawer, tokenId) + oldContractBalance = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenId) + }) + + it('Predicate should have the token', async () => { + amount.should.be.a.bignumber.at.most(oldContractBalance) + }) + + it('Should be able to receive exitTokens tx', async () => { + const burnLog = getERC1155ChainExitLog({ + to: withdrawer, + tokenIds: [tokenId], + amounts: [amount], + data: 'Hello 👋' + }) + + exitTokensTx = await chainExitERC1155Predicate.exitTokens(withdrawer, dummyMintableERC1155.address, burnLog) + should.exist(exitTokensTx) + }) + + it('Withdraw amount should be deducted from contract', async () => { + const newContractBalance = await dummyMintableERC1155.balanceOf(chainExitERC1155Predicate.address, tokenId) + newContractBalance.should.be.a.bignumber.that.equals( + oldContractBalance.sub(amount) + ) + }) + + it('Withdraw amount should be credited to withdrawer', async () => { + const newAccountBalance = await dummyMintableERC1155.balanceOf(withdrawer, tokenId) + newAccountBalance.should.be.a.bignumber.that.equals( + oldAccountBalance.add(amount) + ) + }) + }) + + describe('exitTokens with real burn log', () => { + const amount = mockValues.amounts[9] + const tokenId = mockValues.numbers[4] + const depositData = constructERC1155DepositData([tokenId], [amount]) + const depositor = accounts[1] + const withdrawer = mockValues.addresses[8] + + let dummyMintableERC1155 + let chainExitERC1155Predicate + + before(async () => { + const contracts = await deployer.deployFreshRootContracts(accounts) + dummyMintableERC1155 = contracts.dummyMintableERC1155 + chainExitERC1155Predicate = contracts.chainExitERC1155Predicate + + const PREDICATE_ROLE = await dummyMintableERC1155.PREDICATE_ROLE() + await dummyMintableERC1155.grantRole(PREDICATE_ROLE, chainExitERC1155Predicate.address) + + await dummyMintableERC1155.mint(depositor, tokenId, amount, '0x0') + await dummyMintableERC1155.setApprovalForAll(chainExitERC1155Predicate.address, true, { from: depositor }) + + await chainExitERC1155Predicate.lockTokens(depositor, mockValues.addresses[2], dummyMintableERC1155.address, depositData) + }) + + it('Should revert with correct reason', async () => { + const burnLog = getERC1155ChainExitLog({ + to: mockValues.zeroAddress, + tokenIds: [tokenId], + amounts: [amount], + data: 'Hello 👋' + }) + await expectRevert(chainExitERC1155Predicate.exitTokens(withdrawer, dummyMintableERC1155.address, burnLog), 'ChainExitERC1155Predicate: INVALID_RECEIVER') + }) + }) + + describe('exitTokens with unsupported burn log', () => { + const amount = mockValues.amounts[9] + const tokenId = mockValues.numbers[4] + const depositData = constructERC1155DepositData([tokenId], [amount]) + const depositor = accounts[1] + const withdrawer = mockValues.addresses[8] + + let dummyMintableERC1155 + let chainExitERC1155Predicate + + before(async () => { + const contracts = await deployer.deployFreshRootContracts(accounts) + dummyMintableERC1155 = contracts.dummyMintableERC1155 + chainExitERC1155Predicate = contracts.chainExitERC1155Predicate + + const PREDICATE_ROLE = await dummyMintableERC1155.PREDICATE_ROLE() + await dummyMintableERC1155.grantRole(PREDICATE_ROLE, chainExitERC1155Predicate.address) + + await dummyMintableERC1155.mint(depositor, tokenId, amount, '0x0') + await dummyMintableERC1155.setApprovalForAll(chainExitERC1155Predicate.address, true, { from: depositor }) + + await chainExitERC1155Predicate.lockTokens(depositor, mockValues.addresses[2], dummyMintableERC1155.address, depositData) + }) + + it('Should revert with correct reason', async () => { + const burnLog = getERC1155ChainExitLog({ + overrideSig: mockValues.bytes32[2], + to: withdrawer, + tokenIds: [tokenId], + amounts: [amount], + data: 'Hello 👋' + }) + await expectRevert(chainExitERC1155Predicate.exitTokens(withdrawer, dummyMintableERC1155.address, burnLog), 'ChainExitERC1155Predicate: INVALID_WITHDRAW_SIG') + }) + }) +})