diff --git a/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_DepositedERC721.sol b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_DepositedERC721.sol new file mode 100644 index 000000000..92be1e6bb --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_DepositedERC721.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_DepositedERC721 } from "../../../iOVM/bridge/tokens/iOVM_DepositedERC721.sol"; +import { iOVM_ERC721Gateway } from "../../../iOVM/bridge/tokens/iOVM_ERC721Gateway.sol"; + +/* Library Imports */ +import { OVM_CrossDomainEnabled } from "../../../libraries/bridge/OVM_CrossDomainEnabled.sol"; + +/** + * @title Abs_DepositedERC721 + * @dev An Deposited Token is a representation of funds which were deposited from the other side + * Usually contract mints new tokens when it hears about deposits from the other side. + * This contract also burns the tokens intended for withdrawal, informing the gateway to release the funds. + * + * NOTE: This abstract contract gives all the core functionality of a deposited token implementation except for the + * token's internal accounting itself. This gives developers an easy way to implement children with their own token code. + * + * Compiler used: solc, optimistic-solc + * Runtime target: EVM or OVM + */ +abstract contract Abs_DepositedERC721 is iOVM_DepositedERC721, OVM_CrossDomainEnabled { + + /******************* + * Contract Events * + *******************/ + + event Initialized(iOVM_ERC721Gateway tokenGateway); + + /******************************** + * External Contract References * + ********************************/ + + iOVM_ERC721Gateway public tokenGateway; + + /******************************** + * Constructor & Initialization * + ********************************/ + + /** + * @param _messenger Messenger address being used for cross-chain communications. + */ + constructor( + address _messenger + ) + OVM_CrossDomainEnabled(_messenger) + {} + + /** + * @dev Initialize this contract with the token gateway address on the otehr side. + * The flow: 1) this contract gets deployed on one side, 2) the + * gateway is deployed with addr from (1) on the other, 3) gateway address passed here. + * + * @param _tokenGateway Address of the corresponding gateway deployed to the other side + */ + + function init( + iOVM_ERC721Gateway _tokenGateway + ) + public + { + require(address(tokenGateway) == address(0), "Contract has already been initialized"); + + tokenGateway = _tokenGateway; + + emit Initialized(tokenGateway); + } + + /********************** + * Function Modifiers * + **********************/ + + modifier onlyInitialized() { + require(address(tokenGateway) != address(0), "Contract has not yet been initialized"); + _; + } + + /******************************** + * Overridable Accounting logic * + ********************************/ + + // Default gas value which can be overridden if more complex logic runs on L2. + uint32 constant DEFAULT_FINALIZE_WITHDRAWAL_GAS = 1200000; + + /** + * @dev Core logic to be performed when a withdrawal from L2 is initialized. + * In most cases, this will simply burn the withdrawn L2 funds. + * + * param _to Address being withdrawn to + * param _tokenId Token being withdrawn + */ + + function _handleInitiateWithdrawal( + address, // _to, + uint // _tokenId + ) + internal + virtual + { + revert("Accounting must be implemented by child contract."); + } + + /** + * @dev Core logic to be performed when a deposit is finalised. + * In most cases, this will simply _mint() the ERC721 the recipient. + * + * param _to Address being deposited to + * param _tokenId ERC721 which was deposited + * param _tokenURI URI of the ERC721 which was deposited + */ + function _handleFinalizeDeposit( + address, // _to + uint,// _tokenId + string memory + ) + internal + virtual + { + revert("Accounting must be implemented by child contract."); + } + + /** + * @dev Overridable getter for the *Other side* gas limit of settling the withdrawal, in the case it may be + * dynamic, and the above public constant does not suffice. + * + */ + + function getFinalizeWithdrawalGas() + public + view + virtual + returns( + uint32 + ) + { + return DEFAULT_FINALIZE_WITHDRAWAL_GAS; + } + + + /*************** + * Withdrawing * + ***************/ + + /** + * @dev initiate a withdraw of an ERC721 to the caller's account on the other side + * @param _tokenId ERC721 token to withdraw + */ + function withdraw( + uint _tokenId + ) + external + override + onlyInitialized() + { + _initiateWithdrawal(msg.sender, _tokenId); + } + + /** + * @dev initiate a withdraw of an ERC721 to a recipient's account on the other side + * @param _to adress to credit the withdrawal to + * @param _tokenId ERC721 token to withdraw + */ + function withdrawTo( + address _to, + uint _tokenId + ) + external + override + onlyInitialized() + { + _initiateWithdrawal(_to, _tokenId); + } + + /** + * @dev Performs the logic for withdrawals + * + * @param _to Account to give the withdrawal to on the other side + * @param _tokenId ERC721 token to withdraw + */ + function _initiateWithdrawal( + address _to, + uint _tokenId + ) + internal + { + // Call our withdrawal accounting handler implemented by child contracts (usually a _burn) + _handleInitiateWithdrawal(_to, _tokenId); + + // Construct calldata for ERC721Gateway.finalizeWithdrawal(_to, _tokenId) + bytes memory data = abi.encodeWithSelector( + iOVM_ERC721Gateway.finalizeWithdrawal.selector, + _to, + _tokenId + ); + + // Send message up to L1 gateway + sendCrossDomainMessage( + address(tokenGateway), + data, + getFinalizeWithdrawalGas() + ); + + emit WithdrawalInitiated(msg.sender, _to, _tokenId); + } + + /************************************ + * Cross-chain Function: Depositing * + ************************************/ + + /** + * @dev Complete a deposit, and credits funds to the recipient's balance of the + * specified ERC721 + * This call will fail if it did not originate from a corresponding deposit in OVM_ERC721Gateway. + * + * @param _to Address to receive the withdrawal at + * @param _tokenId ERC721 to deposit + * @param _tokenURI URI of the token being deposited + */ + function finalizeDeposit( + address _to, + uint _tokenId, + string memory _tokenURI + ) + external + override + onlyInitialized() + onlyFromCrossDomainAccount(address(tokenGateway)) + { + _handleFinalizeDeposit(_to, _tokenId, _tokenURI); + emit DepositFinalized(_to, _tokenId, _tokenURI); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_ERC721Gateway.sol b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_ERC721Gateway.sol new file mode 100644 index 000000000..a84d6eefd --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_ERC721Gateway.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_ERC721Gateway } from "../../../iOVM/bridge/tokens/iOVM_ERC721Gateway.sol"; +import { iOVM_DepositedERC721 } from "../../../iOVM/bridge/tokens/iOVM_DepositedERC721.sol"; +import { IERC721Metadata } from "../../../libraries/standards/ERC721/extensions/IERC721Metadata.sol"; +import { IERC721Receiver } from "../../../libraries/standards/ERC721/IERC721Receiver.sol"; + +/* Library Imports */ +import { OVM_CrossDomainEnabled } from "../../../libraries/bridge/OVM_CrossDomainEnabled.sol"; + +/** + * @title Abs_ERC721Gateway + * @dev An ERC721 Gateway is a contract which stores deposited ERC721 tokens that + * are in use on the other side of the bridge. + * It synchronizes a corresponding representation of the "deposited token" on + * the other side, informing it of new deposits and releasing tokens when there + * are newly finalized withdrawals. + * + * NOTE: This abstract contract gives all the core functionality of an ERC721 token gateway, + * but provides easy hooks in case developers need extensions in child contracts. + * In many cases, the default OVM_ERC721Gateway will suffice. + * + * Compiler used: solc, optimistic-solc + * Runtime target: EVM or OVM + */ +abstract contract Abs_ERC721Gateway is iOVM_ERC721Gateway, OVM_CrossDomainEnabled, IERC721Receiver { + + /******************************** + * External Contract References * + ********************************/ + + address public originalToken; + address public depositedToken; + + /*************** + * Constructor * + ***************/ + + /** + * @param _originalToken ERC721 address this gateway is deposits funds for + * @param _depositedToken iOVM_DepositedERC721-compatible address on the chain being deposited into. + * @param _messenger messenger address being used for cross-chain communications. + */ + constructor( + address _originalToken, + address _depositedToken, + address _messenger + ) + OVM_CrossDomainEnabled(_messenger) + { + originalToken = _originalToken; + depositedToken = _depositedToken; + } + + /******************************** + * Overridable Accounting logic * + ********************************/ + + // Default gas value which can be overridden if more complex logic runs on L2. + uint32 public DEFAULT_FINALIZE_DEPOSIT_GAS = 1200000; + + /** + * @dev Core logic to be performed when a withdrawal is finalized. + * In most cases, this will simply send locked funds to the withdrawer. + * + * param _to Address being withdrawn to. + * param _tokenId Token being withdrawn. + */ + function _handleFinalizeWithdrawal( + address, // _to, + uint256 // _tokenId + ) + internal + virtual + { + revert("Implement me in child contracts"); + } + + /** + * @dev Core logic to be performed when a deposit is initiated. + * In most cases, this will simply send the token to the Gateway contract. + * + * param _from Address being deposited from. + * param _to Address being deposited into on the other side. + * param _tokenId ERC721 being deposited. + */ + function _handleInitiateDeposit( + address, // _from, + address, // _to, + uint256 // _tokenId + ) + internal + virtual + { + revert("Implement me in child contracts"); + } + + /** + * @dev Overridable getter for the gas limit on the other side, in the case it may be + * dynamic, and the above public constant does not suffice. + * + */ + + function getFinalizeDepositGas() + public + view + returns( + uint32 + ) + { + return DEFAULT_FINALIZE_DEPOSIT_GAS; + } + + /************** + * Depositing * + **************/ + + /** + * @dev deposit an ERC721 to the caller's balance on the other side + * @param _tokenId ERC721 token to deposit + */ + function deposit( + uint _tokenId + ) + public + override + { + _initiateDeposit(msg.sender, msg.sender, _tokenId); + } + + /** + * @dev deposit an ERC721 to a recipient's balance on the other side + * @param _to address to receive the ERC721 token + * @param _tokenId ERC721 token to deposit + */ + function depositTo( + address _to, + uint _tokenId + ) + public + override + { + _initiateDeposit(msg.sender, _to, _tokenId); + } + + /** + * @dev deposit received ERC721s to the other side + * @param _operator the address calling the contract + * @param _from the address that the ERC721 is being transferred from + * @param _tokenId the token being transferred + * @param _data Additional data with no specified format + */ + function onERC721Received( + address _operator, + address _from, + uint256 _tokenId, + bytes memory _data + ) + external + override + returns ( + bytes4 + ) + { + _sendDepositMessage(_from, _from, _tokenId); + return IERC721Receiver.onERC721Received.selector; + } + + /** + * @dev Sends a message to deposit a token on the other side + * + * @param _from Account depositing from + * @param _to Account to give the deposit to + * @param _tokenId ERC721 token being deposited + */ + function _sendDepositMessage( + address _from, + address _to, + uint _tokenId + ) + internal + { + + // Construct calldata for depositedERC721.finalizeDeposit(_to, _tokenId, _tokenURI) + bytes memory data = abi.encodeWithSelector( + iOVM_DepositedERC721.finalizeDeposit.selector, + _to, + _tokenId, + IERC721Metadata(originalToken).tokenURI(_tokenId) + ); + + // Send calldata into L2 + sendCrossDomainMessage( + depositedToken, + data, + getFinalizeDepositGas() + ); + + emit DepositInitiated(_from, _to, _tokenId); + } + + /** + * @dev Performs the logic for deposits by informing the Deposited Token + * contract on the other side of the deposit and calling a handler to lock the funds. (e.g. transferFrom) + * + * @param _from Account to pull the deposit from on L1 + * @param _to Account to give the deposit to on L2 + * @param _tokenId ERC721 token to deposit + */ + function _initiateDeposit( + address _from, + address _to, + uint _tokenId + ) + internal + { + // Call our deposit accounting handler implemented by child contracts. + _handleInitiateDeposit( + _from, + _to, + _tokenId + ); + + _sendDepositMessage( + _from, + _to, + _tokenId + ); + + } + + /************************* + * Withdrawing * + *************************/ + + /** + * @dev Complete a withdrawal the other side, and credit the ERC721 token to the + * recipient. + * This call will fail if the initialized withdrawal from has not been finalized. + * + * @param _to L1 address to credit the withdrawal to + * @param _tokenId ERC721 token to withdraw + */ + function finalizeWithdrawal( + address _to, + uint _tokenId + ) + external + override + onlyFromCrossDomainAccount(depositedToken) + { + // Call our withdrawal accounting handler implemented by child contracts. + _handleFinalizeWithdrawal( + _to, + _tokenId + ); + + emit WithdrawalFinalized(_to, _tokenId); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_L1TokenGateway.sol b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_L1TokenGateway.sol index 0ea573f76..c0f15dfc9 100644 --- a/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_L1TokenGateway.sol +++ b/contracts/optimistic-ethereum/OVM/bridge/tokens/Abs_L1TokenGateway.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// @unsupported: ovm +// @unsupported: ovm pragma solidity >0.5.0 <0.8.0; pragma experimental ABIEncoderV2; @@ -16,7 +16,7 @@ import { OVM_CrossDomainEnabled } from "../../../libraries/bridge/OVM_CrossDomai * It synchronizes a corresponding L2 representation of the "deposited token", informing it * of new deposits and releasing L1 funds when there are newly finalized withdrawals. * - * NOTE: This abstract contract gives all the core functionality of an L1 token gateway, + * NOTE: This abstract contract gives all the core functionality of an L1 token gateway, * but provides easy hooks in case developers need extensions in child contracts. * In many cases, the default OVM_L1ERC20Gateway will suffice. * @@ -41,7 +41,7 @@ abstract contract Abs_L1TokenGateway is iOVM_L1TokenGateway, OVM_CrossDomainEnab */ constructor( address _l2DepositedToken, - address _l1messenger + address _l1messenger ) OVM_CrossDomainEnabled(_l1messenger) { @@ -74,7 +74,7 @@ abstract contract Abs_L1TokenGateway is iOVM_L1TokenGateway, OVM_CrossDomainEnab /** * @dev Core logic to be performed when a deposit is initiated on L1. - * In most cases, this will simply send locked funds to the withdrawer. + * In most cases, this will simply send funds to the Gateway contract. * * param _from Address being deposited from on L1. * param _to Address being deposited into on L2. @@ -183,9 +183,9 @@ abstract contract Abs_L1TokenGateway is iOVM_L1TokenGateway, OVM_CrossDomainEnab *************************/ /** - * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the - * L1 ERC20 token. - * This call will fail if the initialized withdrawal from L2 has not been finalized. + * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the + * L1 ERC20 token. + * This call will fail if the initialized withdrawal from L2 has not been finalized. * * @param _to L1 address to credit the withdrawal to * @param _amount Amount of the ERC20 to withdraw @@ -195,7 +195,7 @@ abstract contract Abs_L1TokenGateway is iOVM_L1TokenGateway, OVM_CrossDomainEnab uint _amount ) external - override + override onlyFromCrossDomainAccount(l2DepositedToken) { // Call our withdrawal accounting handler implemented by child contracts. diff --git a/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_DepositedERC721.sol b/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_DepositedERC721.sol new file mode 100644 index 000000000..66f2c9b39 --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_DepositedERC721.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_ERC721Gateway } from "../../../iOVM/bridge/tokens/iOVM_ERC721Gateway.sol"; + +/* Contract Imports */ +import { ERC721URIStorage } from "../../../libraries/standards/ERC721/extensions/ERC721URIStorage.sol"; +import { ERC721 } from "../../../libraries/standards/ERC721/ERC721.sol"; + +/* Library Imports */ +import { Abs_DepositedERC721 } from "./Abs_DepositedERC721.sol"; + +/** + * @title OVM_DepositedERC721 + * @dev The Deposited ERC721 is an ERC721 implementation which represents assets deposited on the other side of an Optimistic bridge. + * This contract mints new tokens when it hears about deposits into the corresponding gateway. + * This contract also burns the tokens intended for withdrawal, informing the gateway to release funds. + * + * NOTE: This contract implements the Abs_DepositedERC721 contract using OpenZeppelin's ERC20 as the implementation. + * Alternative implementations can be used in this similar manner. + * + * Compiler used: optimistic-solc + * Runtime target: OVM, EVM + */ +contract OVM_DepositedERC721 is Abs_DepositedERC721, ERC721URIStorage { + + /*************** + * Constructor * + ***************/ + + /** + * @param _messenger Cross-domain messenger used by this contract. + * @param _name ERC721 name + * @param _symbol ERC721 symbol + */ + constructor( + address _messenger, + string memory _name, + string memory _symbol + ) + Abs_DepositedERC721(_messenger) + ERC721(_name, _symbol) + {} + + // When a withdrawal is initiated, we burn the withdrawer's token to prevent subsequent usage. + function _handleInitiateWithdrawal( + address, // _to, + uint _tokenId + ) + internal + override + { + _burn(_tokenId); + } + + // When a deposit is finalized, we mint a new token to the designated account + function _handleFinalizeDeposit( + address _to, + uint _tokenId, + string memory _tokenURI + ) + internal + override + { + _mint(_to, _tokenId); + _setTokenURI(_tokenId, _tokenURI); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_ERC721Gateway.sol b/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_ERC721Gateway.sol new file mode 100644 index 000000000..50a419375 --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_ERC721Gateway.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_ERC721Gateway } from "../../../iOVM/bridge/tokens/iOVM_ERC721Gateway.sol"; +import { Abs_ERC721Gateway } from "./Abs_ERC721Gateway.sol"; +import { IERC721 } from "../../../libraries/standards/ERC721/IERC721.sol"; + +/** +* @title OVM_ERC721Gateway +* @dev An ERC721 Gateway is a contract which stores deposited ERC721 tokens that +* are in use on the other side of the bridge. +* It synchronizes a corresponding representation of the "deposited token" on +* the other side, informing it of new deposits and releasing tokens when there +* are newly finalized withdrawals. +* +* NOTE: This contract extends Abs_ERC721Gateway, which is where we +* takes care of most of the initialization and the cross-chain logic. +* If you are looking to implement your own deposit/withdrawal contracts, you +* may also want to extend the abstract contract in a similar manner. +* +* Compiler used: solc, optimistic-solc +* Runtime target: EVM or OVM + */ +contract OVM_ERC721Gateway is Abs_ERC721Gateway { + + /*************** + * Constructor * + ***************/ + + /** + * @param _ERC721 ERC721 address this gateway stores deposits for + * @param _depositedERC721 iOVM_DepositedERC721-compatible address on the chain being deposited into. + * @param _messenger messenger address being used for cross-chain communications. + */ + constructor( + address _ERC721, + address _depositedERC721, + address _messenger + ) + Abs_ERC721Gateway( + _ERC721, + _depositedERC721, + _messenger + ) + {} + + + /************** + * Accounting * + **************/ + + /** + * @dev When a deposit is initiated, the Gateway + * transfers the funds to itself for future withdrawals + * + * @param _from address the ERC721 is being deposited from + * param _to address that the ERC721 is being deposited to + * @param _tokenId the ERC721 being deposited + */ + function _handleInitiateDeposit( + address _from, + address, // _to, + uint256 _tokenId + ) + internal + override + { + // Hold on to the newly deposited funds + IERC721(originalToken).transferFrom( + _from, + address(this), + _tokenId + ); + } + + /** + * @dev When a withdrawal is finalized, the Gateway + * transfers the funds to the withdrawer + * + * @param _to address that the ERC721 is being withdrawn to + * @param _tokenId the ERC721 being withdrawn + */ + function _handleFinalizeWithdrawal( + address _to, + uint _tokenId + ) + internal + override + { + // Transfer withdrawn funds out to withdrawer + IERC721(originalToken).transferFrom(address(this), _to, _tokenId); + } +} diff --git a/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_DepositedERC721.sol b/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_DepositedERC721.sol new file mode 100644 index 000000000..766f79731 --- /dev/null +++ b/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_DepositedERC721.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0; +pragma experimental ABIEncoderV2; + +/** + * @title iOVM_DepositedERC721 + */ +interface iOVM_DepositedERC721 { + + /********** + * Events * + **********/ + + event WithdrawalInitiated( + address indexed _from, + address _to, + uint256 _tokenId + ); + + event DepositFinalized( + address indexed _to, + uint256 _tokenId, + string _tokenURI + ); + + + /******************** + * Public Functions * + ********************/ + + function withdraw( + uint _tokenId + ) + external; + + function withdrawTo( + address _to, + uint _tokenId + ) + external; + + + /************************* + * Cross-chain Functions * + *************************/ + + function finalizeDeposit( + address _to, + uint _tokenId, + string memory _tokenURI + ) + external; + +} diff --git a/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_ERC721Gateway.sol b/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_ERC721Gateway.sol new file mode 100644 index 000000000..5f7a12f2b --- /dev/null +++ b/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_ERC721Gateway.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0; +pragma experimental ABIEncoderV2; + +/** + * @title iOVM_ERC721Gateway + */ +interface iOVM_ERC721Gateway { + + /********** + * Events * + **********/ + + event DepositInitiated( + address indexed _from, + address _to, + uint256 _tokenId + ); + + event WithdrawalFinalized( + address indexed _to, + uint256 _tokenId + ); + + + /******************** + * Public Functions * + ********************/ + + function deposit( + uint _tokenId + ) + external; + + function depositTo( + address _to, + uint _tokenId + ) + external; + + + /************************* + * Cross-chain Functions * + *************************/ + + function finalizeWithdrawal( + address _to, + uint _tokenId + ) + external; +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC165.sol b/contracts/optimistic-ethereum/libraries/standards/ERC165.sol new file mode 100644 index 000000000..9f4d034cb --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC165.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +import "./IERC165.sol"; + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/ERC721.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/ERC721.sol new file mode 100644 index 000000000..0dc0f6443 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/ERC721.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./extensions/IERC721Metadata.sol"; +import "./utils/Address.sol"; +import "./utils/Context.sol"; +import "./utils/Strings.sol"; +import "../ERC165.sol"; + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension, but not including the Enumerable extension, which is available separately as + * {ERC721Enumerable}. + */ +contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { + using Address for address; + using Strings for uint256; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to owner address + mapping (uint256 => address) private _owners; + + // Mapping owner address to token count + mapping (address => uint256) private _balances; + + // Mapping from token ID to approved address + mapping (uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping (address => mapping (address => bool)) private _operatorApprovals; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor (string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC721).interfaceId + || interfaceId == type(IERC721Metadata).interfaceId + || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + require(owner != address(0), "ERC721: balance query for the zero address"); + return _balances[owner]; + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + address owner = _owners[tokenId]; + require(owner != address(0), "ERC721: owner query for nonexistent token"); + return owner; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 + ? string(abi.encodePacked(baseURI, tokenId.toString())) + : ''; + } + + /** + * @dev Base URI for computing {tokenURI}. Empty by default, can be overriden + * in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721.ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require(_msgSender() == owner || ERC721.isApprovedForAll(owner, _msgSender()), + "ERC721: approve caller is not owner nor approved for all" + ); + + _approve(to, tokenId); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + require(operator != _msgSender(), "ERC721: approve to caller"); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + //solhint-disable-next-line max-line-length + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); + + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); + _safeTransfer(from, to, tokenId, _data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * `_data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual { + _transfer(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _owners[tokenId] != address(0); + } + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { + require(_exists(tokenId), "ERC721: operator query for nonexistent token"); + address owner = ERC721.ownerOf(tokenId); + return (spender == owner || getApproved(tokenId) == spender || ERC721.isApprovedForAll(owner, spender)); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual { + _mint(to, tokenId); + require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + address owner = ERC721.ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId); + + // Clear approvals + _approve(address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) internal virtual { + require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own"); + require(to != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _tokenApprovals[tokenId] = to; + emit Approval(ERC721.ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) + private returns (bool) + { + if (to.isContract()) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721.sol new file mode 100644 index 000000000..c99136ba1 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +import "../IERC165.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721Receiver.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721Receiver.sol new file mode 100644 index 000000000..9176f1419 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/IERC721Receiver.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. + */ + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/ERC721URIStorage.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/ERC721URIStorage.sol new file mode 100644 index 000000000..822dfc82a --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/ERC721URIStorage.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +import "../ERC721.sol"; + +/** + * @dev ERC721 token with storage based token uri management. + */ +abstract contract ERC721URIStorage is ERC721 { + using Strings for uint256; + + // Optional mapping for token URIs + mapping (uint256 => string) private _tokenURIs; + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token"); + + string memory _tokenURI = _tokenURIs[tokenId]; + string memory base = _baseURI(); + + // If there is no base URI, return the token URI. + if (bytes(base).length == 0) { + return _tokenURI; + } + // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + if (bytes(_tokenURI).length > 0) { + return string(abi.encodePacked(base, _tokenURI)); + } + + return super.tokenURI(tokenId); + } + + /** + * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { + require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); + _tokenURIs[tokenId] = _tokenURI; + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + if (bytes(_tokenURIs[tokenId]).length != 0) { + delete _tokenURIs[tokenId]; + } + } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/IERC721Metadata.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/IERC721Metadata.sol new file mode 100644 index 000000000..de4669d95 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/extensions/IERC721Metadata.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +import "../IERC721.sol"; + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721Metadata is IERC721 { + + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Address.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Address.sol new file mode 100644 index 000000000..8902716af --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Address.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +/** + * @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) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { size := extcodesize(account) } + return size > 0; + } + + /** + * @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"); + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: value }(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.staticcall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.delegatecall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { + 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); + } + } + } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Context.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Context.sol new file mode 100644 index 000000000..5da1f7801 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Context.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.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 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) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Strings.sol b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Strings.sol new file mode 100644 index 000000000..18b7f6950 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/ERC721/utils/Strings.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.0; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant alphabet = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = alphabet[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + +} diff --git a/contracts/optimistic-ethereum/libraries/standards/IERC165.sol b/contracts/optimistic-ethereum/libraries/standards/IERC165.sol new file mode 100644 index 000000000..7f54c1ccb --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/IERC165.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.8.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. + * + * 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); +} diff --git a/contracts/test-helpers/TestERC721.sol b/contracts/test-helpers/TestERC721.sol new file mode 100644 index 000000000..98996d944 --- /dev/null +++ b/contracts/test-helpers/TestERC721.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Contract Imports */ +import { ERC721URIStorage } from "../optimistic-ethereum/libraries/standards/ERC721/extensions/ERC721URIStorage.sol"; +import { ERC721 } from "../optimistic-ethereum/libraries/standards/ERC721/ERC721.sol"; + +/** + * @title TestERC721 + * @dev A test ERC721 with tokenURI storage with an open mint function for testing + */ +contract TestERC721 is ERC721URIStorage { + + /*************** + * Constructor * + ***************/ + + /** + * @param _name ERC721 name + * @param _symbol ERC721 symbol + */ + constructor( + string memory _name, + string memory _symbol + ) + ERC721(_name, _symbol) + {} + + function mint(address _to, uint256 _tokenId, string memory _tokenURI) public { + _mint(_to, _tokenId); + _setTokenURI(_tokenId, _tokenURI); + } +} diff --git a/test/contracts/OVM/bridge/assets/OVM_DepositedERC721.spec.ts b/test/contracts/OVM/bridge/assets/OVM_DepositedERC721.spec.ts new file mode 100644 index 000000000..fb8efe31b --- /dev/null +++ b/test/contracts/OVM/bridge/assets/OVM_DepositedERC721.spec.ts @@ -0,0 +1,220 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract, constants } from 'ethers' +import { smockit, MockContract } from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS } from '../../../../helpers' + +const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' +const ERR_INVALID_X_DOMAIN_MSG_SENDER = + 'OVM_XCHAIN: wrong sender of cross-domain message' +const MOCK_GATEWAY_ADDRESS: string = + '0x1234123412341234123412341234123412341234' +const ERR_NON_EXISTENT_TOKEN = 'ERC721: owner query for nonexistent token' +const ERR_NOT_YET_INITIALISED = 'Contract has not yet been initialized' +const ERR_ALREADY_INITIALISED = 'Contract has already been initialized' + +describe('OVM_DepositedERC721', () => { + let alice: Signer + let bob: Signer + let Factory__OVM_ERC721Gateway: ContractFactory + before(async () => { + ;[alice, bob] = await ethers.getSigners() + Factory__OVM_ERC721Gateway = await ethers.getContractFactory( + 'OVM_ERC721Gateway' + ) + }) + + let OVM_DepositedERC721: Contract + let Mock__OVM_L2CrossDomainMessenger: MockContract + let finalizeWithdrawalGasLimit: number + beforeEach(async () => { + // Create a special signer which will enable us to send messages from the L2Messenger contract + let messengerImpersonator: Signer + ;[messengerImpersonator] = await ethers.getSigners() + + // Get a new mock L2 messenger + Mock__OVM_L2CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L2CrossDomainMessenger'), + // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls + { address: await messengerImpersonator.getAddress() } + ) + + // Deploy the contract under test + OVM_DepositedERC721 = await ( + await ethers.getContractFactory('OVM_DepositedERC721') + ).deploy(Mock__OVM_L2CrossDomainMessenger.address, 'OptimisticPunks', 'OP') + + // initialize the L2 Gateway with the L1G ateway addrss + await OVM_DepositedERC721.init(MOCK_GATEWAY_ADDRESS) + + finalizeWithdrawalGasLimit = await OVM_DepositedERC721.getFinalizeWithdrawalGas() + }) + + // test the transfer flow of moving a token from L2 to L1 + describe('finalizeDeposit', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_DepositedERC721 = await ( + await ethers.getContractFactory('OVM_DepositedERC721') + ).deploy(NON_ZERO_ADDRESS, 'OptimisticPunks', 'OP') + await OVM_DepositedERC721.init(NON_ZERO_ADDRESS) + + await expect( + OVM_DepositedERC721.finalizeDeposit(constants.AddressZero, 0, 'abc') + ).to.be.revertedWith(ERR_INVALID_MESSENGER) + }) + + it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L1ERC20Gateway)', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + NON_ZERO_ADDRESS + ) + + await expect( + OVM_DepositedERC721.finalizeDeposit(constants.AddressZero, 0, { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should mint the specified token to the depositor', async () => { + const depositToken = 123 + const depositTokenURI = 'test-token-uri' + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_GATEWAY_ADDRESS + ) + + await OVM_DepositedERC721.finalizeDeposit( + await alice.getAddress(), + depositToken, + depositTokenURI, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const aliceBalance = await OVM_DepositedERC721.balanceOf( + await alice.getAddress() + ) + aliceBalance.should.equal(1) + + // Assert that Alice is now the owner of the deposited token + const tokenOwner = await OVM_DepositedERC721.ownerOf(depositToken) + tokenOwner.should.equal(await alice.getAddress()) + + // Assert that deposited token has URI set + const tokenURI = await OVM_DepositedERC721.tokenURI(depositToken) + tokenURI.should.equal(depositTokenURI) + }) + }) + + describe('withdrawals', () => { + //const ALICE_INITIAL_BALANCE = 2 + const depositToken = 123 + const depositTokenURI = 'test-token-uri' + + beforeEach(async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_GATEWAY_ADDRESS + ) + + await OVM_DepositedERC721.finalizeDeposit( + await alice.getAddress(), + depositToken, + depositTokenURI, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + }) + + it('withdraw() burns and sends the correct withdrawal message', async () => { + await OVM_DepositedERC721.withdraw(depositToken) + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert Alice's balance went down + const aliceBalance = await OVM_DepositedERC721.balanceOf( + await alice.getAddress() + ) + expect(aliceBalance).to.deep.equal(ethers.BigNumber.from(0)) + + // Assert that the withdrawn token no longer exists + await expect( + OVM_DepositedERC721.ownerOf(depositToken) + ).to.be.revertedWith(ERR_NON_EXISTENT_TOKEN) + + // Assert the correct cross-chain call was sent: + // Message should be sent to the L1ERC20Gateway on L1 + expect(withdrawalCallToMessenger._target).to.equal(MOCK_GATEWAY_ADDRESS) + // Message data should be a call telling the L1ERC20Gateway to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + await Factory__OVM_ERC721Gateway.interface.encodeFunctionData( + 'finalizeWithdrawal', + [await alice.getAddress(), depositToken] + ) + ) + // Hardcoded gaslimit should be correct + expect(withdrawalCallToMessenger._gasLimit).to.equal( + finalizeWithdrawalGasLimit + ) + }) + + it('withdrawTo() burns and sends the correct withdrawal message', async () => { + await OVM_DepositedERC721.withdrawTo(await bob.getAddress(), depositToken) + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert Alice's balance went down + const aliceBalance = await OVM_DepositedERC721.balanceOf( + await alice.getAddress() + ) + expect(aliceBalance).to.deep.equal(ethers.BigNumber.from(0)) + + // Assert that the withdrawn token no longer exists + await expect( + OVM_DepositedERC721.ownerOf(depositToken) + ).to.be.revertedWith(ERR_NON_EXISTENT_TOKEN) + + // Assert the correct cross-chain call was sent: + // Message should be sent to the L1ERC20Gateway on L1 + expect(withdrawalCallToMessenger._target).to.equal(MOCK_GATEWAY_ADDRESS) + // Message data should be a call telling the L1ERC20Gateway to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + await Factory__OVM_ERC721Gateway.interface.encodeFunctionData( + 'finalizeWithdrawal', + [await bob.getAddress(), depositToken] + ) + ) + // Hardcoded gaslimit should be correct + expect(withdrawalCallToMessenger._gasLimit).to.equal( + finalizeWithdrawalGasLimit + ) + }) + }) + + describe('Initialization logic', () => { + it('should not allow calls to onlyInitialized functions', async () => { + OVM_DepositedERC721 = await ( + await ethers.getContractFactory('OVM_DepositedERC721') + ).deploy(NON_ZERO_ADDRESS, 'OptimisticPunks', 'OP') + + await expect( + OVM_DepositedERC721.finalizeDeposit(constants.AddressZero, 0, 'abc') + ).to.be.revertedWith(ERR_NOT_YET_INITIALISED) + }) + + it('should only allow initialization once and emits initialized event', async () => { + OVM_DepositedERC721 = await ( + await ethers.getContractFactory('OVM_DepositedERC721') + ).deploy(NON_ZERO_ADDRESS, 'OptimisticPunks', 'OP') + await expect(OVM_DepositedERC721.init(NON_ZERO_ADDRESS)).to.emit( + OVM_DepositedERC721, + 'Initialized' + ) + + await expect( + OVM_DepositedERC721.init(constants.AddressZero) + ).to.be.revertedWith(ERR_ALREADY_INITIALISED) + }) + }) +}) diff --git a/test/contracts/OVM/bridge/assets/OVM_ERC721Gateway.spec.ts b/test/contracts/OVM/bridge/assets/OVM_ERC721Gateway.spec.ts new file mode 100644 index 000000000..95950ee9d --- /dev/null +++ b/test/contracts/OVM/bridge/assets/OVM_ERC721Gateway.spec.ts @@ -0,0 +1,241 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract, constants } from 'ethers' +import { smockit, MockContract, smoddit } from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS } from '../../../../helpers' + +const TEST_TOKEN_URI = 'test-uri-goes-here' + +const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' +const ERR_INVALID_X_DOMAIN_MSG_SENDER = + 'OVM_XCHAIN: wrong sender of cross-domain message' + +describe('OVM_ERC721Gateway', () => { + // init signers + let alice: Signer + + // we can just make up this string since it's on the "other" Layer + let Mock__OVM_DepositedERC721: MockContract + let Factory__ERC721: ContractFactory + let ERC721: Contract + before(async () => { + Mock__OVM_DepositedERC721 = await smockit( + await ethers.getContractFactory('OVM_DepositedERC721') + ) + + // deploy an ERC20 contract on L1 + Factory__ERC721 = await smoddit('TestERC721') + + ERC721 = await Factory__ERC721.deploy('TestERC721', 'ERC') + }) + + let OVM_ERC721Gateway: Contract + let Mock__OVM_L1CrossDomainMessenger: MockContract + let finalizeDepositGasLimit: number + beforeEach(async () => { + // Create a special signer which will enable us to send messages from the Messenger contract + let l1MessengerImpersonator: Signer + ;[l1MessengerImpersonator, alice] = await ethers.getSigners() + // Get a new mock messenger + Mock__OVM_L1CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L1CrossDomainMessenger'), + { address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls + ) + + // Deploy the contract under test + OVM_ERC721Gateway = await ( + await ethers.getContractFactory('OVM_ERC721Gateway') + ).deploy( + ERC721.address, + Mock__OVM_DepositedERC721.address, + Mock__OVM_L1CrossDomainMessenger.address + ) + + finalizeDepositGasLimit = await OVM_ERC721Gateway.DEFAULT_FINALIZE_DEPOSIT_GAS() + }) + + describe('finalizeWithdrawal', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_ERC721Gateway = await ( + await ethers.getContractFactory('OVM_ERC721Gateway') + ).deploy( + ERC721.address, + Mock__OVM_DepositedERC721.address, + NON_ZERO_ADDRESS + ) + + await expect( + OVM_ERC721Gateway.finalizeWithdrawal(constants.AddressZero, 1) + ).to.be.revertedWith(ERR_INVALID_MESSENGER) + }) + + it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2ERC20Gateway)', async () => { + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => NON_ZERO_ADDRESS + ) + + await expect( + OVM_ERC721Gateway.finalizeWithdrawal(constants.AddressZero, 1, { + from: Mock__OVM_L1CrossDomainMessenger.address, + }) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should credit funds to the withdrawer and not use too much gas', async () => { + // make sure no balance at start of test + await expect(await ERC721.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) + + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => Mock__OVM_DepositedERC721.address + ) + + const testTokenId = 123 + ERC721.mint(OVM_ERC721Gateway.address, testTokenId, TEST_TOKEN_URI) + + const res = await OVM_ERC721Gateway.finalizeWithdrawal( + NON_ZERO_ADDRESS, + testTokenId, + { from: Mock__OVM_L1CrossDomainMessenger.address } + ) + + await expect(await ERC721.ownerOf(testTokenId)).to.be.equal( + NON_ZERO_ADDRESS + ) + + const gasUsed = ( + await OVM_ERC721Gateway.provider.getTransactionReceipt(res.hash) + ).gasUsed + + const OVM_DepositedERC721 = await ( + await ethers.getContractFactory('OVM_DepositedERC721') + ).deploy(constants.AddressZero, '', '') + const defaultFinalizeWithdrawalGas = await OVM_DepositedERC721.getFinalizeWithdrawalGas() + await expect(gasUsed.gt((defaultFinalizeWithdrawalGas * 11) / 10)) + }) + + it.skip('finalizeWithdrawalAndCall(): should should credit funds to the withdrawer, and forward from and data', async () => { + // TODO: implement this functionality in a future update + expect.fail() + }) + }) + + describe('deposits', () => { + let depositer: string + const depositTokenId = 321 + + beforeEach(async () => { + // Deploy the ERC721 token + ERC721 = await Factory__ERC721.deploy('TestERC721', 'ERC') + + // get a new mock messenger + Mock__OVM_L1CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L1CrossDomainMessenger') + ) + + // Deploy the contract under test: + OVM_ERC721Gateway = await ( + await ethers.getContractFactory('OVM_ERC721Gateway') + ).deploy( + ERC721.address, + Mock__OVM_DepositedERC721.address, + Mock__OVM_L1CrossDomainMessenger.address + ) + + depositer = await ERC721.signer.getAddress() + + await ERC721.mint(depositer, depositTokenId, TEST_TOKEN_URI) + + // the Signer sets approve for the Gateway + await ERC721.approve(OVM_ERC721Gateway.address, depositTokenId) + }) + + it('deposit() escrows the deposit token and sends the correct deposit message', async () => { + // expect depositer to be initial owner + const initialTokenOwner = await ERC721.ownerOf(depositTokenId) + expect(initialTokenOwner).to.equal(depositer) + + // depositer calls deposit on the gateway and the gateway calls transferFrom on the token + await OVM_ERC721Gateway.deposit(depositTokenId) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + // expect the gateway to be the new owner of the token + const newTokenOwner = await ERC721.ownerOf(depositTokenId) + expect(newTokenOwner).to.equal(OVM_ERC721Gateway.address) + + // Check the correct cross-chain call was sent: + // Message should be sent to the Deposited ERC721 + expect(depositCallToMessenger._target).to.equal( + Mock__OVM_DepositedERC721.address + ) + // Message data should be a call telling the ERC721Gateway to finalize the deposit + + // the gateway sends the correct message to the messenger, including TokenURI + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_DepositedERC721.interface.encodeFunctionData( + 'finalizeDeposit', + [depositer, depositTokenId, TEST_TOKEN_URI] + ) + ) + expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) + }) + + it('depositTo() escrows the deposit token and sends the correct deposit message', async () => { + // depositor calls deposit on the gateway and the gateway calls transferFrom on the token + const aliceAddress = await alice.getAddress() + + // depositer calls deposit on the gateway and the gateway calls transferFrom on the token + await OVM_ERC721Gateway.depositTo(aliceAddress, depositTokenId) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Message data should be a call telling the ERC721Gateway to finalize the deposit + // the gateway sends the correct message to the messenger, including TokenURI + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_DepositedERC721.interface.encodeFunctionData( + 'finalizeDeposit', + [aliceAddress, depositTokenId, TEST_TOKEN_URI] + ) + ) + }) + + it('safeTransfer of a token to the gateway escrows it and initiates a deposit', async () => { + const initialTokenOwner = await ERC721.ownerOf(depositTokenId) + + // depositer safeTransfers a token to the gateway, which leads to a call onERC721Received which initiates a deposit + await expect( + ERC721['safeTransferFrom(address,address,uint256)']( + initialTokenOwner, + OVM_ERC721Gateway.address, + depositTokenId + ) + ).to.emit(OVM_ERC721Gateway, 'DepositInitiated') + + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Message should be sent to the L2ERC721Gateway + expect(depositCallToMessenger._target).to.equal( + Mock__OVM_DepositedERC721.address + ) + + const newTokenOwner = await ERC721.ownerOf(depositTokenId) + expect(newTokenOwner).to.equal(OVM_ERC721Gateway.address) + // Message data should be a call telling the ERC721Gateway to finalize the deposit + + // the gateway sends the correct message to the messenger, including TokenURI + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_DepositedERC721.interface.encodeFunctionData( + 'finalizeDeposit', + [initialTokenOwner, depositTokenId, TEST_TOKEN_URI] + ) + ) + expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) + }) + }) +}) diff --git a/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts b/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts index 2ba568d30..e7997f6d4 100644 --- a/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts +++ b/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts @@ -2,7 +2,7 @@ import { expect } from '../../../../setup' /* External Imports */ import { ethers } from 'hardhat' -import { Signer, ContractFactory, Contract } from 'ethers' +import { Signer, ContractFactory, Contract, constants } from 'ethers' import { smockit, MockContract, @@ -11,13 +11,15 @@ import { } from '@eth-optimism/smock' /* Internal Imports */ -import { NON_ZERO_ADDRESS, ZERO_ADDRESS } from '../../../../helpers' +import { NON_ZERO_ADDRESS } from '../../../../helpers' const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' const ERR_INVALID_X_DOMAIN_MSG_SENDER = 'OVM_XCHAIN: wrong sender of cross-domain message' const MOCK_L1GATEWAY_ADDRESS: string = '0x1234123412341234123412341234123412341234' +const ERR_NOT_YET_INITIALISED = 'Contract has not yet been initialized' +const ERR_ALREADY_INITIALISED = 'Contract has already been initialized' describe('OVM_L2DepositedERC20', () => { let alice: Signer @@ -66,7 +68,7 @@ describe('OVM_L2DepositedERC20', () => { await OVM_L2DepositedERC20.init(NON_ZERO_ADDRESS) await expect( - OVM_L2DepositedERC20.finalizeDeposit(ZERO_ADDRESS, 0) + OVM_L2DepositedERC20.finalizeDeposit(constants.AddressZero, 0) ).to.be.revertedWith(ERR_INVALID_MESSENGER) }) @@ -76,7 +78,7 @@ describe('OVM_L2DepositedERC20', () => { ) await expect( - OVM_L2DepositedERC20.finalizeDeposit(ZERO_ADDRESS, 0, { + OVM_L2DepositedERC20.finalizeDeposit(constants.AddressZero, 0, { from: Mock__OVM_L2CrossDomainMessenger.address, }) ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) @@ -195,13 +197,29 @@ describe('OVM_L2DepositedERC20', () => { }) // low priority todos: see question in contract - describe.skip('Initialization logic', () => { - it('should not allow calls to onlyInitialized functions', async () => { - // TODO + describe('Initialization logic', () => { + it('should not allow calls to onlyInitialized functions before initialization', async () => { + OVM_L2DepositedERC20 = await ( + await ethers.getContractFactory('OVM_L2DepositedERC20') + ).deploy(NON_ZERO_ADDRESS, 'ovmWETH', 'oWETH') + + await expect( + OVM_L2DepositedERC20.finalizeDeposit(constants.AddressZero, 1) + ).to.be.revertedWith(ERR_NOT_YET_INITIALISED) }) it('should only allow initialization once and emits initialized event', async () => { - // TODO + OVM_L2DepositedERC20 = await ( + await ethers.getContractFactory('OVM_L2DepositedERC20') + ).deploy(NON_ZERO_ADDRESS, 'ovmWETH', 'oWETH') + + await expect(OVM_L2DepositedERC20.init(NON_ZERO_ADDRESS)).to.emit( + OVM_L2DepositedERC20, + 'Initialized' + ) + await expect( + OVM_L2DepositedERC20.init(constants.AddressZero) + ).to.be.revertedWith(ERR_ALREADY_INITIALISED) }) }) })