diff --git a/contracts/v0.7/bridge/README.md b/contracts/v0.7/bridge/README.md index e703e70..b64d150 100644 --- a/contracts/v0.7/bridge/README.md +++ b/contracts/v0.7/bridge/README.md @@ -1,3 +1,6 @@ # LINK Token Bridge v0.7 -- `./token/LinkTokenChild.sol`: A mintable & burnable child LinkToken contract to be used on child networks. +- `./token/LinkTokenChild.sol`: A generalized mintable & burnable child LinkToken contract to be used on child networks. +- `./token/arbitrum/`: Documentation for the Arbitrum bridge. +- `./token/avalanche/`: Documentation and token implementation for the Avalanche bridge v2. +- `./token/optimism/`: Documentation and token implementation for the Optimism bridge v2. diff --git a/contracts/v0.7/bridge/token/LinkTokenChild.sol b/contracts/v0.7/bridge/token/LinkTokenChild.sol index d6e75d4..3fb7a76 100644 --- a/contracts/v0.7/bridge/token/LinkTokenChild.sol +++ b/contracts/v0.7/bridge/token/LinkTokenChild.sol @@ -11,17 +11,31 @@ import { ERC20Burnable } from "../../../../vendor/OpenZeppelin/openzeppelin-cont import { SimpleWriteAccessController } from "../../../../vendor/smartcontractkit/chainlink/evm-contracts/src/v0.6/SimpleWriteAccessController.sol"; import { LinkToken } from "../../../v0.6/LinkToken.sol"; -/// @dev Access controlled mintable & burnable LinkToken, for use on sidechains and L2 networks. +/** + * @dev Access controlled mintable & burnable LinkToken, for use on sidechains and L2 networks. + * + * NOTICE: Current implementation of LinkTokenChild contract requires some additional consideration: + * - Supporting more than one gateway (multiple bridges minting the same token) leaves room for accounting issues. + * If we have more than one gateway supported, an additional check needs to exist that limits withdrawals per + * gateway to an amount locked on L1, for the specific gateway. Otherwise one can accidentally "burn" tokens + * by withdrawing more than locked in L1 (tx will fail on L1). When there is a 1:1 relationship between the + * gateway and token, the token itself is an accounting mechanism. For a potential N:1 relationship, a more + * sophisticated type of accounting needs to exist. + * - Every bridge is unique in the amount of risk it bears, so tokens bridged by different bridges are not 1:1 + * the same token, and shouldn't be forced as such. + * - Bridges often require an unique interface to be supported by the child network tokens + * (e.g. mint` vs. `deposit`, `burn` vs. `withdraw/unwrap`, etc.). + * - Bridges often assume that the child contract they are bridging to is the ERC20 token itself, not a gateway + * (intermediate contract) that could help us map from the specific bridge interface to our standard + * LinkTokenChild interface. + * - Chainlink often needs to launch on a new network before the native bridge interface is defined. + * - To support early (before the bridge is defined) Chainlink network launch, we could make an upgradeable + * LinkTokenChild contract which would enable us to slightly update the contract interface after the bridge + * gets defined, and once online transfer the ownership (bridge gateway role) to the new bridge. + * + * TODO: Make upgradeable and limit gateway support to only one (owner)! + */ contract LinkTokenChild is ITypeAndVersion, IERC20Child, SimpleWriteAccessController, ERC20Burnable, LinkToken { - /** - * @dev Overrides parent contract so no tokens are minted on deployment. - * @inheritdoc LinkToken - */ - function _onCreate() - internal - override - {} - /** * @notice versions: * @@ -86,6 +100,15 @@ contract LinkTokenChild is ITypeAndVersion, IERC20Child, SimpleWriteAccessContro super.burnFrom(account, amount); } + /** + * @dev Overrides parent contract so no tokens are minted on deployment. + * @inheritdoc LinkToken + */ + function _onCreate() + internal + override + {} + /// @inheritdoc LinkToken function _transfer( address sender, diff --git a/contracts/v0.7/bridge/token/arbitrum/README.md b/contracts/v0.7/bridge/token/arbitrum/README.md new file mode 100644 index 0000000..af37539 --- /dev/null +++ b/contracts/v0.7/bridge/token/arbitrum/README.md @@ -0,0 +1,8 @@ +# LINK Token on Arbitrum + +Arbitrum included the ERC677 protocol support into their standard bridge token implementation, so **custom integration is not necessary**. + +- Arbitrum ERC20/ERC677 token implementation: [StandardArbERC20.sol](https://github.com/OffchainLabs/arbitrum/blob/042bcafbccf09a23c149822436fb5ec0f4f6fe57/packages/arb-bridge-peripherals/contracts/tokenbridge/arbitrum/StandardArbERC20.sol) +- _Ethereum Rinkeby Arbitrum_ LINK token address: [0x615fBe6372676474d9e6933d310469c9b68e9726](https://rinkeby-explorer.arbitrum.io/address/0x615fBe6372676474d9e6933d310469c9b68e9726) +- _Ethereum Mainnet Arbitrum One_ LINK token address: [0xf97f4df75117a78c1A5a0DBb814Af92458539FB4](https://explorer.arbitrum.io/address/0xf97f4df75117a78c1A5a0DBb814Af92458539FB4) +- Arbitrum bridge UI: [https://bridge.arbitrum.io/](https://bridge.arbitrum.io/) diff --git a/contracts/v0.7/bridge/token/avalanche/IERC20Avalanche.sol b/contracts/v0.7/bridge/token/avalanche/IERC20Avalanche.sol new file mode 100644 index 0000000..c477419 --- /dev/null +++ b/contracts/v0.7/bridge/token/avalanche/IERC20Avalanche.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.6.0 <0.8.0; + +/* Interface Imports */ +import { IERC20 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +/// @dev Interface for the bridged ERC20 token expected by the Avalanche standard bridge. +interface IERC20Avalanche is IERC20 { + + function mint( + address to, + uint256 amount, + address fee_address, + uint256 fee_amount, + bytes32 origin_tx_id + ) + external; + + function chain_ids( + uint256 id + ) + external + view + returns (bool); + + function add_supported_chain_id( + uint256 chain_id + ) + external; + + /** + * @dev Destroys `amount` tokens from `msg.sender. This function is monitored by the Avalanche bridge. + * @notice Call this when withdrawing tokens from Avalanche (NOT the direct `burn/burnFrom` method!). + * @param amount Number of tokens to unwrap. + * @param chain_id Id of the chain/network where to withdraw tokens. + */ + function unwrap( + uint256 amount, + uint256 chain_id + ) + external; + + /** + * @dev Transfers bridge role from `msg.sender` to `new_bridge_role_address`. + * @param new_bridge_role_address Address of the new bridge operator. + */ + function migrate_bridge_role( + address new_bridge_role_address + ) + external; + + function add_swap_token( + address contract_address, + uint256 supply_increment + ) + external; + + function remove_swap_token( + address contract_address, + uint256 supply_decrement + ) + external; + + /** + * @dev Creates an L2 token connected to a specific L2 bridge gateway & L1 token + * @param token Address of the token to swap. + * @param amount Number of tokens to swap. + */ + function swap( + address token, + uint256 amount + ) + external; +} diff --git a/contracts/v0.7/bridge/token/avalanche/LinkTokenAvalanche.sol b/contracts/v0.7/bridge/token/avalanche/LinkTokenAvalanche.sol new file mode 100644 index 0000000..252c844 --- /dev/null +++ b/contracts/v0.7/bridge/token/avalanche/LinkTokenAvalanche.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.6.0 <0.8.0; + +/* Interface Imports */ +import { ITypeAndVersion } from "../../../../v0.6/ITypeAndVersion.sol"; +import { IERC20Avalanche } from "./IERC20Avalanche.sol"; + +/* Library Imports */ +import { Address } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/utils/Address.sol"; +import { SafeMath } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol"; + +/* Contract Imports */ +import { Ownable } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/access/Ownable.sol"; +import { ERC20 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { ERC20Burnable } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20Burnable.sol"; +import { LinkToken } from "../../../../v0.6/LinkToken.sol"; + +/// @dev Access controlled mintable & burnable LinkToken, for use on Avalanche network. +contract LinkTokenAvalanche is ITypeAndVersion, IERC20Avalanche, ERC20Burnable, LinkToken, Ownable { + using SafeMath for uint256; + + struct SwapToken { + address tokenContract; + uint256 supply; + } + + mapping(address => SwapToken) s_swapTokens; + mapping(uint256 => bool) s_chainIds; + + /** + * @notice versions: + * + * - LinkTokenAvalanche 0.0.1: initial release + * + * @inheritdoc ITypeAndVersion + */ + function typeAndVersion() + external + pure + override(ITypeAndVersion, LinkToken) + virtual + returns (string memory) + { + return "LinkTokenAvalanche 0.0.1"; + } + + /// @inheritdoc IERC20Avalanche + function chain_ids( + uint256 id + ) + public + view + override + returns (bool) + { + return s_chainIds[id]; + } + + /// @inheritdoc IERC20Avalanche + function mint( + address to, + uint256 amount, + address fee_address, + uint256 fee_amount, + bytes32 /* origin_tx_id */ + ) + public + override + { + require(owner() == _msgSender(), "DOES_NOT_HAVE_BRIDGE_ROLE"); + _mint(to, amount); + if (fee_amount > 0) { + _mint(fee_address, fee_amount); + } + } + + /// @inheritdoc IERC20Avalanche + function add_supported_chain_id( + uint256 chain_id + ) + public + override + { + require(owner() == _msgSender(), "DOES_NOT_HAVE_BRIDGE_ROLE"); + s_chainIds[chain_id] = true; + } + + /// @inheritdoc IERC20Avalanche + function unwrap( + uint256 amount, + uint256 chain_id + ) + public + override + { + require(s_chainIds[chain_id] == true, "CHAIN_ID_NOT_SUPPORTED"); + _burn(_msgSender(), amount); + } + + /// @inheritdoc IERC20Avalanche + function migrate_bridge_role( + address new_bridge_role_address + ) + public + override + { + require(owner() == _msgSender(), "DOES_NOT_HAVE_BRIDGE_ROLE"); + transferOwnership(new_bridge_role_address); + } + + /// @inheritdoc IERC20Avalanche + function add_swap_token( + address contract_address, + uint256 supply_increment + ) + public + override + { + require(owner() == _msgSender(), "DOES_NOT_HAVE_BRIDGE_ROLE"); + require(Address.isContract(contract_address), "ADDRESS_IS_NOT_CONTRACT"); + + // If the swap token is not already supported, add it with the total supply of supply_increment + // Otherwise, increment the current supply. + if (s_swapTokens[contract_address].tokenContract == address(0)) { + s_swapTokens[contract_address] = SwapToken({ + tokenContract: contract_address, + supply: supply_increment + }); + } else { + s_swapTokens[contract_address].supply = + s_swapTokens[contract_address].supply.add(supply_increment); + } + } + + /// @inheritdoc IERC20Avalanche + function remove_swap_token( + address contract_address, + uint256 supply_decrement + ) + public + override + { + require(owner() == _msgSender(), "DOES_NOT_HAVE_BRIDGE_ROLE"); + require(Address.isContract(contract_address), "ADDRESS_IS_NOT_CONTRACT"); + require(s_swapTokens[contract_address].tokenContract != address(0), "SWAP_TOKEN_IS_NOT_SUPPORTED"); + + // If the decrement is less than the current supply, decrement it from the current supply. + // Otherwise, if the decrement is greater than or equal to the current supply, delete the mapping value. + if (s_swapTokens[contract_address].supply > supply_decrement) { + s_swapTokens[contract_address].supply = + s_swapTokens[contract_address].supply.sub(supply_decrement); + } else { + delete s_swapTokens[contract_address]; + } + } + + /// @inheritdoc IERC20Avalanche + function swap( + address token, + uint256 amount + ) + public + override + { + require(Address.isContract(token), "TOKEN_IS_NOT_CONTRACT"); + require(s_swapTokens[token].tokenContract != address(0), "SWAP_TOKEN_IS_NOT_SUPPORTED"); + require(amount <= s_swapTokens[token].supply, "SWAP_AMOUNT_MORE_THAN_ALLOWED_SUPPLY"); + + // Update the allowed swap amount. + s_swapTokens[token].supply = s_swapTokens[token].supply.sub(amount); + + // Burn the old token. + ERC20Burnable swapToken = ERC20Burnable(s_swapTokens[token].tokenContract); + swapToken.burnFrom(_msgSender(), amount); + + // Mint the new token. + _mint(_msgSender(), amount); + } + + /** + * @notice WARNING: Will burn tokens, without withdrawing them to the origin chain! + * To withdraw tokens use the `unwrap` method, which is monitored by the bridge + * + * @inheritdoc ERC20Burnable + */ + function burn( + uint256 amount + ) + public + override + virtual + { + super.burn(amount); + } + + /** + * @notice WARNING: Will burn tokens, without withdrawing them to the origin chain! + * To withdraw tokens use the `unwrap` method, which is monitored by the bridge + * + * @inheritdoc ERC20Burnable + */ + function burnFrom( + address account, + uint256 amount + ) + public + override + virtual + { + super.burnFrom(account, amount); + } + + /** + * @dev Overrides parent contract so no tokens are minted on deployment. + * @inheritdoc LinkToken + */ + function _onCreate() + internal + override + { + s_chainIds[0] = true; + } + + /// @inheritdoc LinkToken + function _transfer( + address sender, + address recipient, + uint256 amount + ) + internal + override(ERC20, LinkToken) + virtual + { + super._transfer(sender, recipient, amount); + } + + /// @inheritdoc LinkToken + function _approve( + address owner, + address spender, + uint256 amount + ) + internal + override(ERC20, LinkToken) + virtual + { + super._approve(owner, spender, amount); + } +} diff --git a/contracts/v0.7/bridge/token/avalanche/README.md b/contracts/v0.7/bridge/token/avalanche/README.md new file mode 100644 index 0000000..3127b42 --- /dev/null +++ b/contracts/v0.7/bridge/token/avalanche/README.md @@ -0,0 +1,13 @@ +# LINK Token on Avalanche + +- `./token/IERC20Avalanche.sol`: Interface for the bridged ERC20 token expected by the Avalanche standard bridge. +- `./token/LinkTokenAvalanche.sol`: Access controlled mintable & burnable LinkToken, for use on Avalanche network. + +`LinkTokenAvalanche.sol` is a slightly modified version of Avalanche's standard bridged ERC20 token and will be connected to the bridge operator once the bridge is online and operational. Modifications include: + +- Contract versioning via `ITypeAndVersion` interface +- ERC677 support by extending the `LinkToken` contract +- Transfers & approves to the contract itself blocked (provided by `LinkToken` contract) +- Using OZ's `Ownable` contract to express the bridge operator role instead the original custom `Roles` contract + +The public bridge contracts source code and addresses are still TBA by the Avalanche team. diff --git a/contracts/v0.7/bridge/token/optimism/IERC20Optimism.sol b/contracts/v0.7/bridge/token/optimism/IERC20Optimism.sol new file mode 100644 index 0000000..2b15021 --- /dev/null +++ b/contracts/v0.7/bridge/token/optimism/IERC20Optimism.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.6.0 <0.8.0; + +/* Interface Imports */ +import { IERC20 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { IERC165 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/introspection/IERC165.sol"; + +/// @dev Interface for the bridged ERC20 token expected by the Optimism standard bridge L2 gateway. +interface IERC20Optimism is IERC20, IERC165 { + /// @dev Returns the address of an L1 token contract linked to this L2 token contract + function l1Token() + external + returns (address); + + /** + * @dev Creates `_amount` tokens `_to` account. + * @notice Called by L2 gateway to deposit tokens. + * @param _to Address of the recipient. + * @param _amount Number of tokens to mint. + */ + function mint( + address _to, + uint256 _amount + ) + external; + + /** + * @dev Destroys `_amount` tokens `_from` account. + * @notice Called by L2 gateway to withdraw tokens. + * @param _from Address of the account holding the tokens to be burnt. + * @param _amount Number of tokens to burn. + */ + function burn( + address _from, + uint256 _amount + ) + external; + + /// @dev Emitted when `_amount` tokens are deposited from L1 to L2. + event Mint( + address indexed _account, + uint256 _amount + ); + + /// @dev Emitted when `_amount` tokens are withdrawn from L2 to L1. + event Burn( + address indexed _account, + uint256 _amount + ); +} diff --git a/contracts/v0.7/bridge/token/optimism/LinkTokenOptimism.sol b/contracts/v0.7/bridge/token/optimism/LinkTokenOptimism.sol new file mode 100644 index 0000000..48f9df2 --- /dev/null +++ b/contracts/v0.7/bridge/token/optimism/LinkTokenOptimism.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.6.0 <0.8.0; + +/* Interface Imports */ +import { IERC165 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/introspection/IERC165.sol"; +import { ITypeAndVersion } from "../../../../v0.6/ITypeAndVersion.sol"; +import { IERC20Optimism } from "./IERC20Optimism.sol"; + +/* Contract Imports */ +import { ERC20 } from "../../../../../vendor/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { LinkToken } from "../../../../v0.6/LinkToken.sol"; + +/// @dev Access controlled mintable & burnable LinkToken, for use on Optimism L2 network. +contract LinkTokenOptimism is ITypeAndVersion, IERC20Optimism, LinkToken { + /// @dev Returns the address of an L2 bridge contract that has access to mint & burn + address public immutable l2Bridge; + /// @inheritdoc IERC20Optimism + address public immutable override l1Token; + + /** + * @dev Creates an L2 token connected to a specific L2 bridge gateway & L1 token + * @param l2BridgeAddr Address of the corresponding L2 bridge gateway. + * @param l1TokenAddr Address of the corresponding L1 token. + */ + constructor( + address l2BridgeAddr, + address l1TokenAddr + ) { + l2Bridge = l2BridgeAddr; + l1Token = l1TokenAddr; + } + + /** + * @notice versions: + * + * - LinkTokenOptimism 0.0.1: initial release + * + * @inheritdoc ITypeAndVersion + */ + function typeAndVersion() + external + pure + override(ITypeAndVersion, LinkToken) + virtual + returns (string memory) + { + return "LinkTokenOptimism 0.0.1"; + } + + /// @dev Checks that message sender is the L2 bridge contract (locked access to mint & burn) + modifier onlyL2Bridge { + require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); + _; + } + + /** + * @dev Optimism standard bridge L2 gateway uses ERC165 to confirm the required interface + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) + public + override + pure + returns (bool) + { + bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 + bytes4 secondSupportedInterface = IERC20Optimism.l1Token.selector + ^ IERC20Optimism.mint.selector + ^ IERC20Optimism.burn.selector; + return interfaceId == firstSupportedInterface || interfaceId == secondSupportedInterface; + } + + /// @inheritdoc IERC20Optimism + function mint( + address _to, + uint256 _amount + ) + public + override + onlyL2Bridge() + { + _mint(_to, _amount); + emit Mint(_to, _amount); + } + + /// @inheritdoc IERC20Optimism + function burn( + address _from, + uint256 _amount + ) + public + override + onlyL2Bridge() + { + _burn(_from, _amount); + emit Burn(_from, _amount); + } + + /** + * @dev Overrides parent contract so no tokens are minted on deployment. + * @inheritdoc LinkToken + */ + function _onCreate() + internal + override + {} +} diff --git a/contracts/v0.7/bridge/token/optimism/README.md b/contracts/v0.7/bridge/token/optimism/README.md new file mode 100644 index 0000000..7513550 --- /dev/null +++ b/contracts/v0.7/bridge/token/optimism/README.md @@ -0,0 +1,13 @@ +# LINK Token on Optimism + +- `./token/IERC20Optimism.sol`: Interface for the bridged ERC20 token expected by the Optimism standard bridge L2 gateway. +- `./token/LinkTokenOptimism.sol`: Access controlled mintable & burnable LinkToken, for use on Optimism L2 network. + +`LinkTokenOptimism.sol` is a slightly modified version of Optimism's [`L2StandardERC20.sol`](https://github.com/ethereum-optimism/optimism/blob/master/packages/contracts/contracts/optimistic-ethereum/libraries/standards/L2StandardERC20.sol) and will be connected to the [`OVM_L2StandardBridge.sol`](https://github.com/ethereum-optimism/optimism/blob/master/packages/contracts/contracts/optimistic-ethereum/OVM/bridge/tokens/OVM_L2StandardBridge.sol). Modifications include: + +- Contract versioning via `ITypeAndVersion` interface +- ERC677 support by extending the `LinkToken` contract +- Transfers & approves to the contract itself blocked (provided by `LinkToken` contract) +- `l2Bridge` & `l1Token` were changed from storage vars to immutable vars, which provides some gas savings + +The [Optimism Gateway](https://gateway.optimism.io) bridge UI can be used to move the tokens between networks.