diff --git a/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L1TokenBridge.sol b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L1TokenBridge.sol new file mode 100644 index 000000000..ec1c9754c --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L1TokenBridge.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +// @unsupported: ovm +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_L1TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L1TokenBridge.sol"; +import { iOVM_L2TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol"; +import { iOVM_ERC20 } from "../../../iOVM/precompiles/iOVM_ERC20.sol"; + +/* Library Imports */ +import { OVM_CrossDomainEnabled } from "../../../libraries/bridge/OVM_CrossDomainEnabled.sol"; +import { UniSafeERC20Namer } from "../../../libraries/standards/UniSafeERC20Namer.sol"; + +/** + * @title OVM_L1TokenBridge + * @dev The L1 Token Bridge is a contract which stores deposited L1 funds that are in use on L2. + * It synchronizes a corresponding L2 ERC20 Bridge, informing it of deposits, and listening to it + * for newly finalized withdrawals. + * + * Compiler used: solc + * Runtime target: EVM + */ +contract OVM_L1TokenBridge is iOVM_L1TokenBridge, OVM_CrossDomainEnabled { + /******************************** + * External Contract References * + ********************************/ + + address public immutable l2Bridge; + bytes32 public immutable l2ERC20BytecodeHash; + bytes32 public immutable l2ERC777BytecodeHash; + + /*************** + * Constructor * + ***************/ + + /** + * @param _l2Bridge L2 bridge contract address + * @param _l1messenger L1 Messenger address being used for cross-chain communications. + * @param _l2ERC20BytecodeHash Hash of the L2 ERC20 contract bytecode, used to calculate token addresses + * @param _l2ERC777BytecodeHash Hash of the L2 ERC777 contract bytecode, used to calculate token addresses + */ + constructor( + address _l2Bridge, + address _l1messenger, + bytes32 _l2ERC20BytecodeHash, + bytes32 _l2ERC777BytecodeHash + ) + OVM_CrossDomainEnabled(_l1messenger) + { + l2Bridge = _l2Bridge; + l2ERC20BytecodeHash = _l2ERC20BytecodeHash; + l2ERC777BytecodeHash = _l2ERC777BytecodeHash; + } + + /********************** + * L2 Token Addresses * + **********************/ + + /** + * Calculates the addres of a bridged ERC777 on L2 + * @param _l1Token The ERC20 token on L1 + * @return calculatedAddress The address of the bridged ERC777 on L2 + */ + function calculateL2ERC777Address(address _l1Token) public view returns (address calculatedAddress) { + calculatedAddress = address(uint(keccak256(abi.encodePacked( + byte(0xff), + l2Bridge, + bytes32(uint(_l1Token)), + l2ERC777BytecodeHash + )))); + } + + /** + * Calculates the addres of a bridged ERC20 on L2 + * @param _l1Token The ERC20 token on L1 + * @return calculatedAddress The address of the bridged ERC20 on L2 + */ + function calculateL2ERC20Address(address _l1Token) public view returns (address calculatedAddress) { + calculatedAddress = address(uint(keccak256(abi.encodePacked( + byte(0xff), + l2Bridge, + bytes32(uint(_l1Token)), + l2ERC20BytecodeHash + )))); + } + + /************** + * Depositing * + **************/ + + /** + * @dev deposit an amount of ERC20 to a recipients's balance on L2 + * @param _to L2 address to credit the withdrawal to + * @param _amount Amount of the ERC20 to deposit + */ + function depositAsERC20( + address token, + address _to, + uint _amount + ) + external + override + { + _initiateDeposit(iOVM_L2TokenBridge.depositAsERC20.selector, token, msg.sender, _to, _amount); + } + + /** + * @dev deposit an amount of ERC20 to a recipients's balance on L2 + * @param _to L2 address to credit the withdrawal to + * @param _amount Amount of the ERC20 to deposit + */ + function depositAsERC777( + address _token, + address _to, + uint _amount + ) + external + override + { + require(iOVM_ERC20(_token).decimals() <= 18, "Only "); + _initiateDeposit(iOVM_L2TokenBridge.depositAsERC777.selector, _token, msg.sender, _to, _amount); + } + + /** + * @dev Performs the logic for deposits by storing the ERC20 and informing the L2 Deposited ERC20 contract of the deposit. + * + * @param _from Account to pull the deposit from on L1 + * @param _to Account to give the deposit to on L2 + * @param _amount Amount of the ERC20 to deposit. + */ + function _initiateDeposit( + bytes4 _selector, + address _token, + address _from, + address _to, + uint _amount + ) + internal + { + // Hold on to the newly deposited funds + iOVM_ERC20(_token).transferFrom( + _from, + address(this), + _amount + ); + + uint8 _decimals = iOVM_ERC20(_token).decimals(); + + // Construct calldata for l2Bridge.finalizeDeposit(_to, _amount) + bytes memory data = abi.encodeWithSelector(_selector, _token, _to, _amount, _decimals); + + // Send calldata into L2 + sendCrossDomainMessage( + l2Bridge, + data, + DEFAULT_FINALIZE_DEPOSIT_L2_GAS + ); + + emit DepositInitiated(_token, _from, _to, _amount); + } + + /** + * @dev L2 tokens have no name or symbol by default. This function passes that data to L2. + * @param _l1Token Address of the L1 token + */ + function updateTokenInfo(address _l1Token) external { + bytes memory data = abi.encodeWithSelector( + iOVM_L2TokenBridge.updateTokenInfo.selector, + _l1Token, + UniSafeERC20Namer.tokenName(_l1Token), + UniSafeERC20Namer.tokenSymbol(_l1Token) + ); + sendCrossDomainMessage(l2Bridge, data, DEFAULT_FINALIZE_DEPOSIT_L2_GAS); + } + + /************************************* + * Cross-chain Function: Withdrawing * + *************************************/ + + /** + * @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 + */ + function finalizeWithdrawal( + address _token, + address _to, + uint _amount + ) + external + override + onlyFromCrossDomainAccount(l2Bridge) + { + iOVM_ERC20(_token).transfer(_to, _amount); + + emit WithdrawalFinalized(_token, _to, _amount); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC20.sol b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC20.sol new file mode 100644 index 000000000..ca3f52c47 --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC20.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_L2TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol"; + +/* Contract Imports */ +import { UniswapV2ERC20 } from "../../../libraries/standards/UniswapV2ERC20.sol"; + +/** + * @title OVM_L2ERC20 + * @dev The L2 Deposited ERC20 is an ERC20 implementation which represents L1 assets deposited into L2. + * This contract mints new tokens when the token bridge receives deposit messages. + * This contract also burns the tokens intended for withdrawal and calls the bridge contract. + * The name & symbol will be empty by default, and can be set by calling updateTokenInfo on the L1 bridge. + * + * Compiler used: optimistic-solc + * Runtime target: OVM + */ +contract OVM_L2ERC20 is UniswapV2ERC20 { + /******************************** + * External Contract References * + ********************************/ + + address public immutable bridge; + address public l1Address; + + /******************************** + * Constructor & Initialization * + ********************************/ + + constructor() public UniswapV2ERC20(0, "", "") { + bridge = msg.sender; + } + + /** + * @dev Initialize the contract immediately after construction, passing in the + * L1 token address and the number of decimals. + * + * @param _l1Address Address of the corresponding token on L1 + * @param _decimals Number of decimal places of the token + */ + function initialize(address _l1Address, uint8 _decimals) external onlyBridge { + l1Address = _l1Address; + decimals = _decimals; + } + + /********************** + * Function Modifiers * + **********************/ + + modifier onlyBridge { + require(msg.sender == bridge, "May only be called by the bridge"); + _; + } + + /****************** + * User Functions * + ******************/ + + /** + * @dev Initiate a withdraw of some ERC20 to a recipient's account on L1 + * @param _destination L1 adress to credit the withdrawal to + * @param _amount Amount of the ERC20 to withdraw + */ + function withdraw(address _destination, uint256 _amount) external { + _burn(msg.sender, _amount); + iOVM_L2TokenBridge(bridge).withdraw(l1Address, _destination, _amount); + } + + /** + * @dev Migrate tokens from ERC20 to ERC777 + * @param _amount Amount of the ERC20 to migrate + * @param _target The address of the ERC777 token + */ + function migrate(uint256 _amount, address _target) external { + _burn(msg.sender, _amount); + iOVM_L2TokenBridge(bridge).migrate(l1Address, _target, msg.sender, _amount); + } + + /******************** + * Bridge functions * + ********************/ + + /** + * @dev Receives the name & symbol of the token from the bridge. + * + * @param _newName The token's name + * @param _newSymbol The token's symbol + */ + function updateInfo(string memory _newName, string memory _newSymbol) external onlyBridge { + name = _newName; + symbol = _newSymbol; + } + + /** + * @dev Mints new tokens to a user. + * + * @param _recipient The address to receive the tokens. + * @param _amount The amount of tokens to mint. + */ + function mint(address _recipient, uint256 _amount) external onlyBridge { + _mint(_recipient, _amount); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC777.sol b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC777.sol new file mode 100644 index 000000000..a4fd6ce49 --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2ERC777.sol @@ -0,0 +1,537 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >0.5.0 <0.8.0; + +/* Interface Imports */ +import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol"; +import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; +import { IERC777Sender } from "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { iOVM_L2TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol"; + +/* Library Imports */ +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC1820Registry } from "@openzeppelin/contracts/introspection/IERC1820Registry.sol"; + +/** + * @title OVM_L2ERC777 + * @dev The L2 Deposited ERC777 is an ERC777 implementation which represents L1 assets deposited into L2. + * This contract mints new tokens when the token bridge receives deposit messages. + * This contract also burns the tokens intended for withdrawal and calls the bridge contract. + * The name & symbol will be empty by default, and can be set by calling updateTokenInfo on the L1 bridge. + * + * This contract is a modified version of the OpenZeppelin ERC777 contract, which removes unnecessary features. + * + * Compiler used: optimistic-solc + * Runtime target: OVM + */ +contract OVM_L2ERC777 is IERC777, IERC20 { + using Address for address; + + + /******************************** + * External Contract References * + ********************************/ + + address public immutable bridge; + address public l1Address; + uint8 public l1Decimals; + + IERC1820Registry constant internal _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); + + + /********************** + * Contract Variables * + **********************/ + + mapping(address => uint256) private _balances; + + uint256 private _totalSupply; + string private _name; + string private _symbol; + uint256 private _granularity; + + // For each account, a mapping of its operators and revoked bridge operators. + mapping(address => mapping(address => bool)) private _operators; + + // ERC20-allowances + mapping (address => mapping (address => uint256)) private _allowances; + + bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); + bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); + + + /******************************** + * Constructor & Initialization * + ********************************/ + + constructor() public { + bridge = msg.sender; + + // register interfaces + _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this)); + _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this)); + } + + function initialize(address _l1Address, uint8 _decimals) external onlyBridge { + require(_decimals <= 18); + l1Address = _l1Address; + l1Decimals = _decimals; + _granularity = 10 ** uint256(18 - _decimals); + } + + /********************** + * Function Modifiers * + **********************/ + + modifier onlyBridge { + require(msg.sender == bridge, "May only be called by the bridge"); + _; + } + + /****************** + * View Functions * + ******************/ + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {ERC20-decimals}. + * + * Always returns 18, as per the + * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility). + */ + function decimals() public pure virtual returns (uint8) { + return 18; + } + + /** + * @dev The smallest unit that can be transferred or minted. Derived from the number of decimals. + */ + function granularity() public view virtual override returns (uint256) { + return _granularity; + } + + /** + * @dev See {IERC777-totalSupply}. + */ + function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) { + return _totalSupply; + } + + /** + * @dev Returns the amount of tokens owned by an account (`tokenHolder`). + */ + function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) { + return _balances[tokenHolder]; + } + + /******************* + * Token Transfers * + *******************/ + + /** + * @dev See {IERC777-send}. + * + * Also emits a {IERC20-Transfer} event for ERC20 compatibility. + */ + function send(address recipient, uint256 amount, bytes memory data) public virtual override { + _send(msg.sender, recipient, amount, data, "", true); + } + + /** + * @dev See {IERC20-transfer}. + * + * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient} + * interface if it is a contract. + * + * Also emits a {Sent} event. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + require(recipient != address(0), "ERC777: transfer to the zero address"); + + address from = msg.sender; + + _callTokensToSend(from, from, recipient, amount, "", ""); + + _move(from, from, recipient, amount, "", ""); + + _callTokensReceived(from, from, recipient, amount, "", "", false); + + return true; + } + + /************* + * Operators * + *************/ + + /** + * @dev See {IERC777-isOperatorFor}. + */ + function isOperatorFor(address operator, address tokenHolder) public view virtual override returns (bool) { + return operator == tokenHolder || _operators[tokenHolder][operator]; + } + + /** + * @dev See {IERC777-authorizeOperator}. + */ + function authorizeOperator(address operator) public virtual override { + require(msg.sender != operator, "ERC777: authorizing self as operator"); + + _operators[msg.sender][operator] = true; + + emit AuthorizedOperator(operator, msg.sender); + } + + /** + * @dev See {IERC777-revokeOperator}. + */ + function revokeOperator(address operator) public virtual override { + require(operator != msg.sender, "ERC777: revoking self as operator"); + + delete _operators[msg.sender][operator]; + + emit RevokedOperator(operator, msg.sender); + } + + /** + * @dev See {IERC777-defaultOperators}. + */ + function defaultOperators() public view virtual override returns (address[] memory _defaultOperators) { + _defaultOperators = new address[](0); + } + + /** + * @dev See {IERC777-operatorSend}. + * + * Emits {Sent} and {IERC20-Transfer} events. + */ + function operatorSend( + address sender, + address recipient, + uint256 amount, + bytes memory data, + bytes memory operatorData + ) + public + virtual + override + { + require(isOperatorFor(msg.sender, sender), "ERC777: caller is not an operator for holder"); + _send(sender, recipient, amount, data, operatorData, true); + } + + /** + * @dev See {IERC777-operatorBurn}. + * + * Emits {Burned} and {IERC20-Transfer} events. + */ + function operatorBurn(address account, uint256 amount, bytes memory data, bytes memory operatorData) public virtual override { + require(isOperatorFor(msg.sender, account), "ERC777: caller is not an operator for holder"); + _burn(account, amount, data, operatorData); + } + + /************** + * Allowances * + **************/ + + /** + * @dev See {IERC20-allowance}. + * + * Note that operator and allowance concepts are orthogonal: operators may + * not have allowance, and accounts with allowance may not be operators + * themselves. + */ + function allowance(address holder, address spender) public view virtual override returns (uint256) { + return _allowances[holder][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Note that accounts cannot have allowance issued by their operators. + */ + function approve(address spender, uint256 value) public virtual override returns (bool) { + address holder = msg.sender; + _approve(holder, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Note that operator and allowance concepts are orthogonal: operators cannot + * call `transferFrom` (unless they have allowance), and accounts with + * allowance cannot call `operatorSend` (unless they are operators). + * + * Emits {Sent}, {IERC20-Transfer} and {IERC20-Approval} events. + */ + function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) { + require(recipient != address(0), "ERC777: transfer to the zero address"); + require(holder != address(0), "ERC777: transfer from the zero address"); + + address spender = msg.sender; + + _callTokensToSend(spender, holder, recipient, amount, "", ""); + + _move(spender, holder, recipient, amount, "", ""); + + uint256 currentAllowance = _allowances[holder][spender]; + require(currentAllowance >= amount, "ERC777: transfer amount exceeds allowance"); + _approve(holder, spender, currentAllowance - amount); + + _callTokensReceived(spender, holder, recipient, amount, "", "", false); + + return true; + } + + /************************* + * User Bridge functions * + *************************/ + + /** + * @dev initiate a withdraw of some ERC20 to a recipient's account on L1 + * @param _destination L1 address to credit the withdrawal to + * @param _amount Amount of tokens to withdraw (with ERC777 decimals) + */ + function withdraw(address _destination, uint256 _amount) external { + _burn(msg.sender, _amount, "", ""); + iOVM_L2TokenBridge(bridge).withdraw(l1Address, _destination, from777to20(l1Decimals, _amount)); + } + + /** + * @dev initiate a withdraw of some ERC20 to a recipient's account on L1 + * @param _amount Amount of tokens to migrate (with ERC777 decimals) + * @param _target The token to migrate to (should be the ERC20 version) + */ + function migrate(uint256 _amount, address _target) external { + _burn(msg.sender, _amount, "", ""); + iOVM_L2TokenBridge(bridge).migrate(l1Address, _target, msg.sender, from777to20(l1Decimals, _amount)); + } + + /******************** + * Bridge functions * + ********************/ + + /** + * @dev Called by the bridge to mint new tokens + * @param _destination address to mint tokens to + * @param _amount Amount of tokens to mint (ERC20 units) + */ + function mint(address _destination, uint256 _amount) external onlyBridge { + _mint(_destination, from20to777(l1Decimals, _amount), '', ''); + } + + function updateInfo(string memory _newName, string memory _newSymbol) external onlyBridge { + _name = _newName; + _symbol = _newSymbol; + } + + /********************* + * Private functions * + *********************/ + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * If a send hook is registered for `account`, the corresponding function + * will be called with `operator`, `data` and `operatorData`. + * + * See {IERC777Sender} and {IERC777Recipient}. + * + * Emits {Minted} and {IERC20-Transfer} events. + * + */ + function _mint( + address account, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) + internal + virtual + { + require(account != address(0), "ERC777: mint to the zero address"); + + address operator = msg.sender; + + // Update state variables + _totalSupply += amount; + _balances[account] += amount; + + // Note: The ERC777 specification & OpenZeppelin implementation set requireReceptionAck + // to true for _mint. However, here it has been changed to false to prevent deposit failures. + _callTokensReceived(operator, address(0), account, amount, userData, operatorData, true); + + emit Minted(operator, account, amount, userData, operatorData); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Send tokens + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient + */ + function _send( + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData, + bool requireReceptionAck + ) + internal + virtual + { + require(from != address(0), "ERC777: send from the zero address"); + require(to != address(0), "ERC777: send to the zero address"); + + address operator = msg.sender; + + _callTokensToSend(operator, from, to, amount, userData, operatorData); + + _move(operator, from, to, amount, userData, operatorData); + + _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck); + } + + /** + * @dev Burn tokens + * @param from address token holder address + * @param amount uint256 amount of tokens to burn + * @param data bytes extra information provided by the token holder + * @param operatorData bytes extra information provided by the operator (if any) + */ + function _burn( + address from, + uint256 amount, + bytes memory data, + bytes memory operatorData + ) + internal + virtual + { + require(from != address(0), "ERC777: burn from the zero address"); + + address operator = msg.sender; + + _callTokensToSend(operator, from, address(0), amount, data, operatorData); + + // Update state variables + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC777: burn amount exceeds balance"); + _balances[from] = fromBalance - amount; + _totalSupply -= amount; + + emit Burned(operator, from, amount, data, operatorData); + emit Transfer(from, address(0), amount); + } + + function _move( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) + private + { + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC777: transfer amount exceeds balance"); + _balances[from] = fromBalance - amount; + _balances[to] += amount; + + emit Sent(operator, from, to, amount, userData, operatorData); + emit Transfer(from, to, amount); + } + + /** + * @dev See {ERC20-_approve}. + * + * Note that accounts cannot have allowance issued by their operators. + */ + function _approve(address holder, address spender, uint256 value) internal { + require(holder != address(0), "ERC777: approve from the zero address"); + require(spender != address(0), "ERC777: approve to the zero address"); + + _allowances[holder][spender] = value; + emit Approval(holder, spender, value); + } + + /** + * @dev Call from.tokensToSend() if the interface is registered + * @param operator address operator requesting the transfer + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + */ + function _callTokensToSend( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) + private + { + address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH); + if (implementer != address(0)) { + IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData); + } + } + + /** + * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but + * tokensReceived() was not registered for the recipient + * @param operator address operator requesting the transfer + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient + */ + function _callTokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData, + bool requireReceptionAck + ) + private + { + address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH); + if (implementer != address(0)) { + IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData); + } else if (requireReceptionAck) { + require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient"); + } + } + + function from777to20(uint8 _decimals, uint amount) internal pure returns (uint256) { + require(_decimals <= 18, 'DEC'); + return amount / (10 ** uint256(18 - _decimals)); + } + + function from20to777(uint8 _decimals, uint amount) internal pure returns (uint256) { + require(_decimals <= 18, 'DEC'); + return amount * (10 ** uint256(18 - _decimals)); + } +} diff --git a/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2TokenBridge.sol b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2TokenBridge.sol new file mode 100644 index 000000000..f7fb6e078 --- /dev/null +++ b/contracts/optimistic-ethereum/OVM/bridge/unibridge/OVM_L2TokenBridge.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +// @unsupported: ovm +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iOVM_L1TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L1TokenBridge.sol"; +import { iOVM_L2TokenBridge } from "../../../iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol"; +import { iOVM_L2Token } from "../../../iOVM/bridge/unibridge/iOVM_L2Token.sol"; + +/* Contract Imports */ +import { OVM_L2ERC20 } from "./OVM_L2ERC20.sol"; +import { OVM_L2ERC777 } from "./OVM_L2ERC777.sol"; + +/* Library Imports */ +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { OVM_CrossDomainEnabled } from "../../../libraries/bridge/OVM_CrossDomainEnabled.sol"; + +/** + * @title OVM_L2TokenBridge + * @dev The L1 ERC20 Gateway is a contract which stores deposited L1 funds that are in use on L2. + * It synchronizes a corresponding L2 ERC20 Gateway, informing it of deposits, and listening to it + * for newly finalized withdrawals. + * + * Compiler used: solc + * Runtime _target: EVM + */ +contract OVM_L2TokenBridge is iOVM_L2TokenBridge, OVM_CrossDomainEnabled { + using Address for address; + + /******************* + * Contract Events * + *******************/ + + event Initialized(iOVM_L1TokenBridge _l1TokenBridge); + + /******************************** + * External Contract References * + ********************************/ + + iOVM_L1TokenBridge l1TokenBridge; + + bytes32 public constant ERC777_BYTECODE_HASH = keccak256(type(OVM_L2ERC777).creationCode); + bytes32 public constant ERC20_BYTECODE_HASH = keccak256(type(OVM_L2ERC20).creationCode); + + /******************************** + * Constructor & Initialization * + ********************************/ + + /** + * @param _l2CrossDomainMessenger L1 Messenger address being used for cross-chain communications. + */ + constructor(address _l2CrossDomainMessenger) + public + OVM_CrossDomainEnabled(_l2CrossDomainMessenger) + {} + + /** + * @dev Initialize this gateway with the L1 gateway address + * The assumed flow is that this contract is deployed on L2, then the L1 + * gateway is dpeloyed, and its address passed here to init. + * + * @param _l1TokenBridge Address of the corresponding L1 gateway deployed to the main chain + */ + function init( + iOVM_L1TokenBridge _l1TokenBridge + ) + external + { + require(address(l1TokenBridge) == address(0), "Contract has already been initialized"); + + l1TokenBridge = _l1TokenBridge; + + emit Initialized(l1TokenBridge); + } + + /********************** + * Function Modifiers * + **********************/ + + modifier onlyInitialized() { + require(address(l1TokenBridge) != address(0), "Contract has not yet been initialized"); + _; + } + + function calculateL2ERC777Address(address _l1Token) public view returns (address calculatedAddress) { + calculatedAddress = address(uint(keccak256(abi.encodePacked( + byte(0xff), + address(this), + bytes32(uint(_l1Token)), + ERC777_BYTECODE_HASH + )))); + } + + function calculateL2ERC20Address(address _l1Token) public view returns (address calculatedAddress) { + calculatedAddress = address(uint(keccak256(abi.encodePacked( + byte(0xff), + address(this), + bytes32(uint(_l1Token)), + ERC20_BYTECODE_HASH + )))); + } + + /*************** + * Withdrawing * + ***************/ + + /** + * @dev Called by a L2 token to withdraw tokens back to L1 + * @param _l1Token Address of the token on L1 + * @param _destination Address to receive tokens on L1 + * @param _amount Amount of the ERC20 to withdraw + */ + function withdraw( + address _l1Token, + address _destination, + uint _amount + ) + external + override + { + require(msg.sender == calculateL2ERC777Address(_l1Token) + || msg.sender == calculateL2ERC20Address(_l1Token), + "Must be called by a bridged token"); + + // Construct calldata for bridge.finalizeWithdrawal(_to, _amount) + bytes memory data = abi.encodeWithSelector( + iOVM_L1TokenBridge.finalizeWithdrawal.selector, + _l1Token, + _destination, + _amount + ); + + // Send message up to L1 gateway + sendCrossDomainMessage( + address(l1TokenBridge), + data, + DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS + ); + + emit WithdrawalInitiated(_l1Token, msg.sender, _destination, _amount); + } + + /** + * @dev Allows converting betwen ERC20 & ERC777 tokens. Must be called by token. + * @param _l1Token Address of the token on L1 + * @param _recipient Address to receive tokens + * @param _target The token to migrate to + * @param _amount Amount of the ERC20 to migrate (in ERC20 decimals) + */ + function migrate( + address _l1Token, + address _target, + address _recipient, + uint256 _amount + ) external override { + address l2ERC777 = calculateL2ERC777Address(_l1Token); + address l2ERC20 = calculateL2ERC20Address(_l1Token); + + require(msg.sender == l2ERC777 || msg.sender == l2ERC20, "Must be called by token"); + require(_target == l2ERC777 || _target == l2ERC20, "Can only migrate to ERC20 or ERC777"); + + iOVM_L2Token(_target).mint(_recipient, _amount); + } + + /************************************ + * Cross-chain Function: Depositing * + ************************************/ + + /** + * @dev Complete a deposit from L1 to L2, and credits funds to the recipient's balance of this + * L2 ERC20 token. + * This call will fail if it did not originate from a corresponding deposit in OVM_L1ERC20Gateway. + * + * @param _to Address to receive the withdrawal at + * @param _amount Amount of the ERC20 to withdraw + */ + function depositAsERC20( + address _token, + address _to, + uint _amount, + uint8 _decimals + ) external override onlyInitialized onlyFromCrossDomainAccount(address(l1TokenBridge)) + { + iOVM_L2Token l2Token = getToken(_token, _decimals, false); + l2Token.mint(_to, _amount); + emit DepositFinalized(_token, _to, _amount); + } + + function depositAsERC777( + address _token, + address _to, + uint _amount, + uint8 _decimals + ) external override onlyInitialized onlyFromCrossDomainAccount(address(l1TokenBridge)) + { + iOVM_L2Token l2Token = getToken(_token, _decimals, true); + l2Token.mint(_to, _amount); + emit DepositFinalized(_token, _to, _amount); + } + + function updateTokenInfo( + address l1ERC20, + string calldata name, + string calldata symbol + ) external override onlyInitialized onlyFromCrossDomainAccount(address(l1TokenBridge)) { + address erc777 = calculateL2ERC777Address(l1ERC20); + address erc20 = calculateL2ERC20Address(l1ERC20); + + if (erc777.isContract()) { + iOVM_L2Token(erc777).updateInfo(name, symbol); + } + if (erc20.isContract()) { + iOVM_L2Token(erc20).updateInfo(name, symbol); + } + } + + function getToken(address _l1Token, uint8 decimals, bool isERC777) private returns (iOVM_L2Token) { + address calculatedAddress = isERC777 + ? calculateL2ERC777Address(_l1Token) + : calculateL2ERC20Address(_l1Token); + + if (!calculatedAddress.isContract()) { + if (isERC777) { + new OVM_L2ERC777{ salt: bytes32(uint(_l1Token)) }(); + } else { + new OVM_L2ERC20{ salt: bytes32(uint(_l1Token)) }(); + } + iOVM_L2Token(calculatedAddress).initialize(_l1Token, decimals); + } + + return iOVM_L2Token(calculatedAddress); + } +} diff --git a/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L1TokenBridge.sol b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L1TokenBridge.sol new file mode 100644 index 000000000..ef7f734b6 --- /dev/null +++ b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L1TokenBridge.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0; +pragma experimental ABIEncoderV2; + +/** + * @title iOVM_L1TokenBridge + */ +interface iOVM_L1TokenBridge { + + /********** + * Events * + **********/ + + event DepositInitiated( + address indexed _token, + address indexed _from, + address _to, + uint256 _amount + ); + + event WithdrawalFinalized( + address indexed _token, + address indexed _to, + uint256 _amount + ); + + + /******************** + * Public Functions * + ********************/ + + function depositAsERC20( + address token, + address _to, + uint _amount + ) external; + + function depositAsERC777( + address _token, + address _to, + uint _amount + ) external; + + /************************* + * Cross-chain Functions * + *************************/ + + function finalizeWithdrawal( + address _token, + address _to, + uint _amount + ) + external; +} diff --git a/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2Token.sol b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2Token.sol new file mode 100644 index 000000000..b23ac08c8 --- /dev/null +++ b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2Token.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0; +pragma experimental ABIEncoderV2; + +/** + * @title iOVM_L2Token + */ +interface iOVM_L2Token { + function initialize(address _l1Address, uint8 _decimals) external; + + function updateInfo(string memory newName, string memory newSymbol) external; + + function withdraw(address _destination, uint256 _amount) external; + + function migrate(uint256 amount, address target) external; + + function mint(address recipient, uint256 amount) external; +} diff --git a/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol new file mode 100644 index 000000000..dbd229d6f --- /dev/null +++ b/contracts/optimistic-ethereum/iOVM/bridge/unibridge/iOVM_L2TokenBridge.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0; +pragma experimental ABIEncoderV2; + +/** + * @title iOVM_L2TokenBridge + */ +interface iOVM_L2TokenBridge { + + /********** + * Events * + **********/ + + event WithdrawalInitiated( + address indexed _token, + address indexed _from, + address _to, + uint256 _amount + ); + + event DepositFinalized( + address indexed _token, + address indexed _to, + uint256 _amount + ); + + + /******************** + * Public Functions * + ********************/ + + function depositAsERC20( + address _token, + address _to, + uint _amount, + uint8 _decimals + ) + external; + + function depositAsERC777( + address _token, + address _to, + uint _amount, + uint8 _decimals + ) + external; + + + function withdraw( + address _l1Token, + address _destination, + uint _amount + ) + external; + + function migrate( + address _l1Token, + address _target, + address _recipient, + uint256 _amount + ) + external; + + function updateTokenInfo( + address l1ERC20, + string calldata name, + string calldata symbol + ) + external; + +} diff --git a/contracts/optimistic-ethereum/iOVM/precompiles/iOVM_ERC20.sol b/contracts/optimistic-ethereum/iOVM/precompiles/iOVM_ERC20.sol index 1c765799c..7727de33f 100644 --- a/contracts/optimistic-ethereum/iOVM/precompiles/iOVM_ERC20.sol +++ b/contracts/optimistic-ethereum/iOVM/precompiles/iOVM_ERC20.sol @@ -17,6 +17,9 @@ interface iOVM_ERC20 { /// total amount of tokens function totalSupply() external view returns (uint256); + /// @return How many decimal places the token has + function decimals() external view returns (uint8); + /// @param _owner The address from which the balance will be retrieved /// @return balance The balance function balanceOf(address _owner) external view returns (uint256 balance); diff --git a/contracts/optimistic-ethereum/libraries/standards/UniSafeERC20Namer.sol b/contracts/optimistic-ethereum/libraries/standards/UniSafeERC20Namer.sol new file mode 100644 index 000000000..41534dcf6 --- /dev/null +++ b/contracts/optimistic-ethereum/libraries/standards/UniSafeERC20Namer.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.5.0; + +// Copied from https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/SafeERC20Namer.sol + +library AddressStringUtil { + // converts an address to the uppercase hex string, extracting only len bytes (up to 20, multiple of 2) + function toAsciiString(address addr, uint256 len) internal pure returns (string memory) { + require(len % 2 == 0 && len > 0 && len <= 40, 'AddressStringUtil: INVALID_LEN'); + + bytes memory s = new bytes(len); + uint256 addrNum = uint256(addr); + for (uint256 i = 0; i < len / 2; i++) { + // shift right and truncate all but the least significant byte to extract the byte at position 19-i + uint8 b = uint8(addrNum >> (8 * (19 - i))); + // first hex character is the most significant 4 bits + uint8 hi = b >> 4; + // second hex character is the least significant 4 bits + uint8 lo = b - (hi << 4); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + // hi and lo are only 4 bits and between 0 and 16 + // this method converts those values to the unicode/ascii code point for the hex representation + // uses upper case for the characters + function char(uint8 b) private pure returns (bytes1 c) { + if (b < 10) { + return bytes1(b + 0x30); + } else { + return bytes1(b + 0x37); + } + } +} +// produces token descriptors from inconsistent or absent ERC20 symbol implementations that can return string or bytes32 +// this library will always produce a string symbol to represent the token +library UniSafeERC20Namer { + function bytes32ToString(bytes32 x) private pure returns (string memory) { + bytes memory bytesString = new bytes(32); + uint256 charCount = 0; + for (uint256 j = 0; j < 32; j++) { + bytes1 char = x[j]; + if (char != 0) { + bytesString[charCount] = char; + charCount++; + } + } + bytes memory bytesStringTrimmed = new bytes(charCount); + for (uint256 j = 0; j < charCount; j++) { + bytesStringTrimmed[j] = bytesString[j]; + } + return string(bytesStringTrimmed); + } + + // assumes the data is in position 2 + function parseStringData(bytes memory b) private pure returns (string memory) { + uint256 charCount = 0; + // first parse the charCount out of the data + for (uint256 i = 32; i < 64; i++) { + charCount <<= 8; + charCount += uint8(b[i]); + } + + bytes memory bytesStringTrimmed = new bytes(charCount); + for (uint256 i = 0; i < charCount; i++) { + bytesStringTrimmed[i] = b[i + 64]; + } + + return string(bytesStringTrimmed); + } + + // uses a heuristic to produce a token name from the address + // the heuristic returns the full hex of the address string in upper case + function addressToName(address token) private pure returns (string memory) { + return AddressStringUtil.toAsciiString(token, 40); + } + + // uses a heuristic to produce a token symbol from the address + // the heuristic returns the first 6 hex of the address string in upper case + function addressToSymbol(address token) private pure returns (string memory) { + return AddressStringUtil.toAsciiString(token, 6); + } + + // calls an external view token contract method that returns a symbol or name, and parses the output into a string + function callAndParseStringReturn(address token, bytes4 selector) private view returns (string memory) { + (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(selector)); + // if not implemented, or returns empty data, return empty string + if (!success || data.length == 0) { + return ''; + } + // bytes32 data always has length 32 + if (data.length == 32) { + bytes32 decoded = abi.decode(data, (bytes32)); + return bytes32ToString(decoded); + } else if (data.length > 64) { + return abi.decode(data, (string)); + } + return ''; + } + + // attempts to extract the token symbol. if it does not implement symbol, returns a symbol derived from the address + function tokenSymbol(address token) internal view returns (string memory) { + // 0x95d89b41 = bytes4(keccak256("symbol()")) + string memory symbol = callAndParseStringReturn(token, 0x95d89b41); + if (bytes(symbol).length == 0) { + // fallback to 6 uppercase hex of address + return addressToSymbol(token); + } + return symbol; + } + + // attempts to extract the token name. if it does not implement name, returns a name derived from the address + function tokenName(address token) internal view returns (string memory) { + // 0x06fdde03 = bytes4(keccak256("name()")) + string memory name = callAndParseStringReturn(token, 0x06fdde03); + if (bytes(name).length == 0) { + // fallback to full hex of address + return addressToName(token); + } + return name; + } +} diff --git a/contracts/optimistic-ethereum/libraries/standards/UniswapV2ERC20.sol b/contracts/optimistic-ethereum/libraries/standards/UniswapV2ERC20.sol index 605a9401e..1578731d0 100644 --- a/contracts/optimistic-ethereum/libraries/standards/UniswapV2ERC20.sol +++ b/contracts/optimistic-ethereum/libraries/standards/UniswapV2ERC20.sol @@ -8,7 +8,7 @@ contract UniswapV2ERC20 is IUniswapV2ERC20 { string public override name; string public override symbol; - uint8 public override immutable decimals; + uint8 public override decimals; uint public override totalSupply; mapping(address => uint) public override balanceOf; mapping(address => mapping(address => uint)) public override allowance; diff --git a/hardhat.config.ts b/hardhat.config.ts index 37365517a..308636171 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -9,6 +9,7 @@ import { import '@nomiclabs/hardhat-ethers' import '@nomiclabs/hardhat-waffle' import 'hardhat-typechain' +import 'hardhat-erc1820' import '@eth-optimism/plugins/hardhat/compiler' import '@eth-optimism/smock/build/src/plugins/hardhat-storagelayout' diff --git a/package.json b/package.json index d47b4c97f..6a3926b84 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "@ethersproject/hardware-wallets": "^5.0.8", "@openzeppelin/contracts": "^3.3.0", "ganache-core": "^2.12.1", - "glob": "^7.1.6" + "glob": "^7.1.6", + "hardhat-erc1820": "^0.1.0" }, "devDependencies": { "@eth-optimism/plugins": "^0.0.16", diff --git a/test/contracts/OVM/bridge/unibridge/OVM_L1TokenBridge.spec.ts b/test/contracts/OVM/bridge/unibridge/OVM_L1TokenBridge.spec.ts new file mode 100644 index 000000000..23062cc65 --- /dev/null +++ b/test/contracts/OVM/bridge/unibridge/OVM_L1TokenBridge.spec.ts @@ -0,0 +1,278 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract, BigNumber } from 'ethers' +import { + smockit, + MockContract, + smoddit, + ModifiableContract, +} from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS, ZERO_ADDRESS } from '../../../../helpers' + +const INITIAL_TOTAL_L1_SUPPLY = 3000 + +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 ERC20_BYTECODE_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ERC777_BYTECODE_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' + +describe('OVM_L1TokenBridge', () => { + // init signers + let alice: Signer + let bob: Signer + + // we can just make up this string since it's on the "other" Layer + let Mock__OVM_L2TokenBridge: MockContract + let Factory__L1ERC20: ContractFactory + let L1ERC20: Contract + const initialSupply = 1_000 + before(async () => { + ;[alice, bob] = await ethers.getSigners() + + Mock__OVM_L2TokenBridge = await smockit( + await ethers.getContractFactory('OVM_L2TokenBridge') + ) + + // deploy an ERC20 contract on L1 + Factory__L1ERC20 = await smoddit('UniswapV2ERC20') + + L1ERC20 = await Factory__L1ERC20.deploy(18, 'L1ERC20', 'ERC') + + const aliceAddress = await alice.getAddress() + L1ERC20.smodify.put({ + totalSupply: INITIAL_TOTAL_L1_SUPPLY, + balanceOf: { + [aliceAddress]: INITIAL_TOTAL_L1_SUPPLY, + }, + }) + }) + + let OVM_L1TokenBridge: Contract + let Mock__OVM_L1CrossDomainMessenger: MockContract + let finalizeDepositGasLimit: number + beforeEach(async () => { + // Create a special signer which will enable us to send messages from the L1Messenger contract + let l1MessengerImpersonator: Signer + ;[l1MessengerImpersonator, alice, bob] = await ethers.getSigners() + // Get a new mock L1 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_L1TokenBridge = await ( + await ethers.getContractFactory('OVM_L1TokenBridge') + ).deploy( + Mock__OVM_L2TokenBridge.address, + Mock__OVM_L1CrossDomainMessenger.address, + ERC20_BYTECODE_HASH, + ERC777_BYTECODE_HASH + ) + + finalizeDepositGasLimit = await OVM_L1TokenBridge.DEFAULT_FINALIZE_DEPOSIT_L2_GAS() + }) + + it('should calculate L2 token addresses') + + it('should update L2 token info', async () => { + await OVM_L1TokenBridge.updateTokenInfo(L1ERC20.address) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2ERC20Gateway on L2 + expect(depositCallToMessenger._target).to.equal( + Mock__OVM_L2TokenBridge.address + ) + // Message data should be a call telling the L2ERC20Gateway to finalize the deposit + + // the L1 gateway sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_L2TokenBridge.interface.encodeFunctionData( + 'updateTokenInfo', + [L1ERC20.address, 'L1ERC20', 'ERC'] + ) + ) + expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) + }) + + describe('finalizeWithdrawal', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_L1TokenBridge = await ( + await ethers.getContractFactory('OVM_L1TokenBridge') + ).deploy( + Mock__OVM_L2TokenBridge.address, + NON_ZERO_ADDRESS, + ERC20_BYTECODE_HASH, + ERC777_BYTECODE_HASH + ) + + await expect( + OVM_L1TokenBridge.finalizeWithdrawal(L1ERC20.address, ZERO_ADDRESS, 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_L1TokenBridge.finalizeWithdrawal(L1ERC20.address, ZERO_ADDRESS, 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 L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) + + const withdrawalAmount = 100 + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => Mock__OVM_L2TokenBridge.address + ) + + await L1ERC20.transfer(OVM_L1TokenBridge.address, withdrawalAmount) + + const res = await OVM_L1TokenBridge.finalizeWithdrawal( + L1ERC20.address, + NON_ZERO_ADDRESS, + withdrawalAmount, + { from: Mock__OVM_L1CrossDomainMessenger.address } + ) + + await expect(await L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal( + withdrawalAmount + ) + + const gasUsed = ( + await OVM_L1TokenBridge.provider.getTransactionReceipt(res.hash) + ).gasUsed + + await expect( + gasUsed.gt( + ((await OVM_L1TokenBridge.DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS()) * + 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', () => { + const INITIAL_DEPOSITER_BALANCE = 100_000 + let depositer: string + const depositAmount = 1_000 + let L1ERC20: Contract + + beforeEach(async () => { + // Deploy the L1 ERC20 token, Alice will receive the full initialSupply + L1ERC20 = await Factory__L1ERC20.deploy(18, 'L1ERC20', 'ERC') + + // get a new mock L1 messenger + Mock__OVM_L1CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L1CrossDomainMessenger') + ) + + // Deploy the contract under test: + OVM_L1TokenBridge = await ( + await ethers.getContractFactory('OVM_L1TokenBridge') + ).deploy( + Mock__OVM_L2TokenBridge.address, + Mock__OVM_L1CrossDomainMessenger.address, + ERC20_BYTECODE_HASH, + ERC777_BYTECODE_HASH + ) + + // the Signer sets approve for the L1 Gateway + await L1ERC20.approve(OVM_L1TokenBridge.address, depositAmount) + depositer = await L1ERC20.signer.getAddress() + + await L1ERC20.smodify.put({ + balanceOf: { + [depositer]: INITIAL_DEPOSITER_BALANCE, + }, + }) + }) + + it('depositAsERC20() escrows the deposit amount and sends the correct deposit message', async () => { + // alice calls deposit on the gateway and the L1 gateway calls transferFrom on the token + const bobsAddress = await bob.getAddress() + await OVM_L1TokenBridge.depositAsERC20(L1ERC20.address, bobsAddress, depositAmount) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await L1ERC20.balanceOf(depositer) + expect(depositerBalance).to.equal( + INITIAL_DEPOSITER_BALANCE - depositAmount + ) + + // gateway's balance is increased + const gatewayBalance = await L1ERC20.balanceOf(OVM_L1TokenBridge.address) + expect(gatewayBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2ERC20Gateway on L2 + expect(depositCallToMessenger._target).to.equal( + Mock__OVM_L2TokenBridge.address + ) + // Message data should be a call telling the L2ERC20Gateway to finalize the deposit + + // the L1 gateway sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_L2TokenBridge.interface.encodeFunctionData( + 'depositAsERC20', + [L1ERC20.address, bobsAddress, depositAmount, 18] + ) + ) + expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) + }) + + it('depositAsERC777() escrows the deposit amount and sends the correct deposit message', async () => { + // depositor calls deposit on the gateway and the L1 gateway calls transferFrom on the token + const bobsAddress = await bob.getAddress() + await OVM_L1TokenBridge.depositAsERC777(L1ERC20.address, bobsAddress, depositAmount) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await L1ERC20.balanceOf(depositer) + expect(depositerBalance).to.equal( + INITIAL_DEPOSITER_BALANCE - depositAmount + ) + + // gateway's balance is increased + const gatewayBalance = await L1ERC20.balanceOf(OVM_L1TokenBridge.address) + expect(gatewayBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2ERC20Gateway on L2 + expect(depositCallToMessenger._target).to.equal( + Mock__OVM_L2TokenBridge.address + ) + // Message data should be a call telling the L2ERC20Gateway to finalize the deposit + + // the L1 gateway sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + await Mock__OVM_L2TokenBridge.interface.encodeFunctionData( + 'depositAsERC777', + [L1ERC20.address, bobsAddress, depositAmount, 18] + ) + ) + expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) + }) + }) +}) diff --git a/test/contracts/OVM/bridge/unibridge/OVM_L2ERC20.spec.ts b/test/contracts/OVM/bridge/unibridge/OVM_L2ERC20.spec.ts new file mode 100644 index 000000000..2282b9c29 --- /dev/null +++ b/test/contracts/OVM/bridge/unibridge/OVM_L2ERC20.spec.ts @@ -0,0 +1,76 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, Contract } from 'ethers' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS } from '../../../../helpers' + +const ERR_ONLY_BRIDGE = 'May only be called by the bridge' + +// Note: withdraw & migrate are tested in OVM_L2TokenBridge.spec.ts +describe('OVM_L2ERC20', () => { + // init signers + let bridge: Signer + let bob: Signer + + before(async () => { + ;[bridge, bob] = await ethers.getSigners() + }) + + let OVM_L2ERC20: Contract + beforeEach(async () => { + // Deploy the contract under test + OVM_L2ERC20 = await ( + await ethers.getContractFactory('OVM_L2ERC20') + ).deploy() + }) + + describe('initialize', () => { + it('onlyBridge: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC20.connect(bob).initialize(NON_ZERO_ADDRESS, 18) + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('initialize() should set the metadata', async () => { + // make sure no balance at start of test + await OVM_L2ERC20.initialize(NON_ZERO_ADDRESS, 18) + + await expect(await OVM_L2ERC20.decimals()).to.be.equal(18) + await expect(await OVM_L2ERC20.l1Address()).to.be.equal(NON_ZERO_ADDRESS) + }) + }) + + describe('updateInfo', () => { + it('updateInfo: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC20.connect(bob).updateInfo('Test Name', 'TEST') + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('updateInfo() should set the token metadata', async () => { + await OVM_L2ERC20.updateInfo('Test Name', 'TEST') + + await expect(await OVM_L2ERC20.name()).to.be.equal('Test Name') + await expect(await OVM_L2ERC20.symbol()).to.be.equal('TEST') + }) + }) + + describe('mint', () => { + it('mint: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC20.connect(bob).mint(NON_ZERO_ADDRESS, 100) + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('mint: should mint tokens to an address', async () => { + await expect(await OVM_L2ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) + + await OVM_L2ERC20.mint(NON_ZERO_ADDRESS, 100) + + await expect(await OVM_L2ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(100) + }) + }) +}) diff --git a/test/contracts/OVM/bridge/unibridge/OVM_L2ERC777.spec.ts b/test/contracts/OVM/bridge/unibridge/OVM_L2ERC777.spec.ts new file mode 100644 index 000000000..45e657f2c --- /dev/null +++ b/test/contracts/OVM/bridge/unibridge/OVM_L2ERC777.spec.ts @@ -0,0 +1,83 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, Contract } from 'ethers' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS } from '../../../../helpers' + +const ERR_ONLY_BRIDGE = 'May only be called by the bridge' + +// Note: withdraw & migrate are tested in OVM_L2TokenBridge.spec.ts +describe('OVM_L2ERC777', () => { + // init signers + let bridge: Signer + let bob: Signer + + before(async () => { + ;[bridge, bob] = await ethers.getSigners() + }) + + let OVM_L2ERC777: Contract + beforeEach(async () => { + // Deploy the contract under test + OVM_L2ERC777 = await ( + await ethers.getContractFactory('OVM_L2ERC777') + ).deploy() + }) + + describe('initialize', () => { + it('onlyBridge: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC777.connect(bob).initialize(NON_ZERO_ADDRESS, 18) + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('initialize() should set the metadata', async () => { + await OVM_L2ERC777.initialize(NON_ZERO_ADDRESS, 18) + + await expect(await OVM_L2ERC777.decimals()).to.be.equal(18) + await expect(await OVM_L2ERC777.l1Address()).to.be.equal(NON_ZERO_ADDRESS) + }) + + it('initialize() should set the granularity if less than 18 decimals', async () => { + await OVM_L2ERC777.initialize(NON_ZERO_ADDRESS, 16) + + await expect(await OVM_L2ERC777.decimals()).to.be.equal(18) + await expect(await OVM_L2ERC777.granularity()).to.be.equal(100) + }) + }) + + describe('updateInfo', () => { + it('updateInfo: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC777.connect(bob).updateInfo('Test Name', 'TEST') + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('updateInfo() should set the token metadata', async () => { + await OVM_L2ERC777.updateInfo('Test Name', 'TEST') + + await expect(await OVM_L2ERC777.name()).to.be.equal('Test Name') + await expect(await OVM_L2ERC777.symbol()).to.be.equal('TEST') + }) + }) + + describe('mint', () => { + it('mint: should revert on calls from accounts other than the bridge', async () => { + await expect( + OVM_L2ERC777.connect(bob).mint(NON_ZERO_ADDRESS, 100) + ).to.be.revertedWith(ERR_ONLY_BRIDGE) + }) + + it('mint: should mint tokens to an address', async () => { + await expect(await OVM_L2ERC777.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) + + await OVM_L2ERC777.initialize(NON_ZERO_ADDRESS, 18) + await OVM_L2ERC777.mint(NON_ZERO_ADDRESS, 100) + + await expect(await OVM_L2ERC777.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(100) + }) + }) +}) diff --git a/test/contracts/OVM/bridge/unibridge/OVM_L2TokenBridge.spec.ts b/test/contracts/OVM/bridge/unibridge/OVM_L2TokenBridge.spec.ts new file mode 100644 index 000000000..7580ac467 --- /dev/null +++ b/test/contracts/OVM/bridge/unibridge/OVM_L2TokenBridge.spec.ts @@ -0,0 +1,427 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract, BigNumber } from 'ethers' +import { + smockit, + MockContract, + smoddit, + ModifiableContract, +} from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_ZERO_ADDRESS, ZERO_ADDRESS } from '../../../../helpers' + +const decimals = 1 + +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 MOCK_L1ERC20_ADDRESS = '0x4242424242424242424242424242424242424242' + +describe('OVM_L2TokenBridge', () => { + let alice: Signer + let bob: Signer + let Factory__OVM_L1TokenBridge: ContractFactory + let Factory__OVM_L2ERC20: ContractFactory + let Factory__OVM_L2ERC777: ContractFactory + before(async () => { + ;[alice, bob] = await ethers.getSigners() + Factory__OVM_L1TokenBridge = await ethers.getContractFactory('OVM_L1TokenBridge') + Factory__OVM_L2ERC777 = await ethers.getContractFactory('OVM_L2ERC777') + Factory__OVM_L2ERC20 = await ethers.getContractFactory('OVM_L2ERC20') + }) + + let OVM_L2TokenBridge: 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 l2MessengerImpersonator: Signer + ;[l2MessengerImpersonator] = 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 l2MessengerImpersonator.getAddress() } + ) + + // Deploy the contract under test + OVM_L2TokenBridge = await ( + await ethers.getContractFactory('OVM_L2TokenBridge') + ).deploy(Mock__OVM_L2CrossDomainMessenger.address) + + // initialize the L2 Gateway with the L1G ateway addrss + await OVM_L2TokenBridge.init(MOCK_L1GATEWAY_ADDRESS) + + finalizeWithdrawalGasLimit = await OVM_L2TokenBridge.DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS() + }) + + describe('deposits', () => { + describe('ERC777', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_L2TokenBridge = await ( + await ethers.getContractFactory('OVM_L2TokenBridge') + ).deploy(NON_ZERO_ADDRESS) + await OVM_L2TokenBridge.init(NON_ZERO_ADDRESS) + + await expect( + OVM_L2TokenBridge.depositAsERC777(MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 1000, 18) + ).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_L2TokenBridge.depositAsERC777(MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 1000, 18, { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should create an ERC777 token on L2 and deposit funds', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC20Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + + expect(await L2ERC20Token.balanceOf(NON_ZERO_ADDRESS)).to.equal(100) + expect(await L2ERC20Token.totalSupply()).to.equal(100) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(100) + expect(await L2ERC20Token.totalSupply()).to.equal(200) + }) + + it('should convert values for tokens with less than 18 decimals', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 16, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC20Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + + expect(await L2ERC20Token.decimals()).to.equal(18) + expect(await L2ERC20Token.granularity()).to.equal(100) + expect(await L2ERC20Token.balanceOf(NON_ZERO_ADDRESS)).to.equal(10000) + expect(await L2ERC20Token.totalSupply()).to.equal(10000) + }) + }) + + describe('ERC20', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_L2TokenBridge = await ( + await ethers.getContractFactory('OVM_L2TokenBridge') + ).deploy(NON_ZERO_ADDRESS) + await OVM_L2TokenBridge.init(NON_ZERO_ADDRESS) + + await expect( + OVM_L2TokenBridge.depositAsERC777(MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 1000, 18) + ).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_L2TokenBridge.depositAsERC777(MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 1000, 18, { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should create an ERC777 token on L2 and deposit funds', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC20Token = await Factory__OVM_L2ERC20.attach( + await OVM_L2TokenBridge.calculateL2ERC20Address(MOCK_L1ERC20_ADDRESS) + ) + + expect(await L2ERC20Token.balanceOf(NON_ZERO_ADDRESS)).to.equal(100) + expect(await L2ERC20Token.totalSupply()).to.equal(100) + + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(100) + expect(await L2ERC20Token.totalSupply()).to.equal(200) + }) + }) + }) + + describe('updateTokenInfo', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { + // Deploy new gateway, initialize with random messenger + OVM_L2TokenBridge = await ( + await ethers.getContractFactory('OVM_L2TokenBridge') + ).deploy(NON_ZERO_ADDRESS) + await OVM_L2TokenBridge.init(NON_ZERO_ADDRESS) + + await expect( + OVM_L2TokenBridge.updateTokenInfo(MOCK_L1ERC20_ADDRESS, 'Test Token', 'TEST') + ).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_L2TokenBridge.updateTokenInfo(MOCK_L1ERC20_ADDRESS, 'Test Token', 'TEST', { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should update the ERC777 & ERC20 token info', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC777Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + const L2ERC20Token = await Factory__OVM_L2ERC20.attach( + await OVM_L2TokenBridge.calculateL2ERC20Address(MOCK_L1ERC20_ADDRESS) + ) + expect(await L2ERC20Token.name()).to.equal('') + expect(await L2ERC20Token.symbol()).to.equal('') + expect(await L2ERC777Token.name()).to.equal('') + expect(await L2ERC777Token.symbol()).to.equal('') + + await OVM_L2TokenBridge.updateTokenInfo( + MOCK_L1ERC20_ADDRESS, + 'Test Token', + 'TEST', + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + expect(await L2ERC20Token.name()).to.equal('Test Token') + expect(await L2ERC20Token.symbol()).to.equal('TEST') + expect(await L2ERC777Token.name()).to.equal('Test Token') + expect(await L2ERC777Token.symbol()).to.equal('TEST') + }) + }) + + describe('migrations', () => { + it('should migrate ERC20 tokens to ERC777', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC777Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + const L2ERC20Token = await Factory__OVM_L2ERC20.attach( + await OVM_L2TokenBridge.calculateL2ERC20Address(MOCK_L1ERC20_ADDRESS) + ) + + expect(await L2ERC777Token.balanceOf(await bob.getAddress())).to.equal(0) + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(100) + + await L2ERC20Token.connect(bob).migrate(100, L2ERC777Token.address) + + expect(await L2ERC777Token.balanceOf(await bob.getAddress())).to.equal(100) + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(0) + }) + + it('should migrate ERC777 tokens to ERC20', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + NON_ZERO_ADDRESS, + 100, + 18, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC777Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + const L2ERC20Token = await Factory__OVM_L2ERC20.attach( + await OVM_L2TokenBridge.calculateL2ERC20Address(MOCK_L1ERC20_ADDRESS) + ) + + expect(await L2ERC777Token.balanceOf(await bob.getAddress())).to.equal(100) + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(0) + + await L2ERC777Token.connect(bob).migrate(100, L2ERC20Token.address) + + expect(await L2ERC777Token.balanceOf(await bob.getAddress())).to.equal(0) + expect(await L2ERC20Token.balanceOf(await bob.getAddress())).to.equal(100) + }) + }) + + describe('withdrawals', () => { + it('should let a ERC777 token call withdraw', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + const OVM_L2ERC777 = await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + + await OVM_L2TokenBridge.depositAsERC777( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 16, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC777Token = await Factory__OVM_L2ERC777.attach( + await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + ) + + // We created a 16-decimal ERC20. That means 10000 ERC777 should equal 100 ERC20 + await L2ERC777Token.connect(bob).withdraw(NON_ZERO_ADDRESS, 10000); + + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert the correct cross-chain call was sent: + // Message should be sent to the L1ERC20Gateway on L1 + expect(withdrawalCallToMessenger._target).to.equal(MOCK_L1GATEWAY_ADDRESS) + // Message data should be a call telling the L1ERC20Gateway to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + await Factory__OVM_L1TokenBridge.interface.encodeFunctionData( + 'finalizeWithdrawal', + [MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 100] + ) + ) + // Hardcoded gaslimit should be correct + expect(withdrawalCallToMessenger._gasLimit).to.equal(finalizeWithdrawalGasLimit) + }) + + it('should let a ERC20 token call withdraw', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => MOCK_L1GATEWAY_ADDRESS + ) + + const OVM_L2ERC20 = await OVM_L2TokenBridge.calculateL2ERC777Address(MOCK_L1ERC20_ADDRESS) + + await OVM_L2TokenBridge.depositAsERC20( + MOCK_L1ERC20_ADDRESS, + await bob.getAddress(), + 100, + 16, + { from: Mock__OVM_L2CrossDomainMessenger.address } + ) + + const L2ERC20Token = await Factory__OVM_L2ERC20.attach( + await OVM_L2TokenBridge.calculateL2ERC20Address(MOCK_L1ERC20_ADDRESS) + ) + + await L2ERC20Token.connect(bob).withdraw(NON_ZERO_ADDRESS, 100); + + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert the correct cross-chain call was sent: + // Message should be sent to the L1ERC20Gateway on L1 + expect(withdrawalCallToMessenger._target).to.equal(MOCK_L1GATEWAY_ADDRESS) + // Message data should be a call telling the L1ERC20Gateway to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + await Factory__OVM_L1TokenBridge.interface.encodeFunctionData( + 'finalizeWithdrawal', + [MOCK_L1ERC20_ADDRESS, NON_ZERO_ADDRESS, 100] + ) + ) + // Hardcoded gaslimit should be correct + expect(withdrawalCallToMessenger._gasLimit).to.equal(finalizeWithdrawalGasLimit) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 7aa6f867e..ac682cb59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4493,6 +4493,11 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" +hardhat-erc1820@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/hardhat-erc1820/-/hardhat-erc1820-0.1.0.tgz#9bc5a2acc9c9b84f3be9041c25fa349010cd3192" + integrity sha512-oQxe7Li8Ev6/Gs6PMcH9+IjaXS+xh6HyPBTGnlRVG4yfmkYF7ajVvzxfYY/FGlM9/j+F2uZjRhxsc//qisC82A== + hardhat-typechain@^0.3.4: version "0.3.5" resolved "https://registry.yarnpkg.com/hardhat-typechain/-/hardhat-typechain-0.3.5.tgz#8e50616a9da348b33bd001168c8fda9c66b7b4af"