-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cross-chain Tokenized Threshold BTC: Generic L2 ERC20 implementation (#…
…550) Closes #543 # The contract `L2TBTC` is a canonical L2/sidechain token implementation. tBTC token is minted on L1 and locked there to be moved to L2/sidechain. By deploying a canonical token on each L2/sidechain, we can ensure the supply of tBTC remains sacrosanct, while enabling quick, interoperable cross-chain bridges and localizing ecosystem risk. This contract is flexible enough to: - Delegate minting authority to a native bridge on the chain, if present. - Delegate minting authority to a short list of ecosystem bridges. - Have mints and burns paused by any one of n guardians, allowing avoidance of contagion in case of a chain- or bridge-specific incident. - Be governed and upgradeable. The token is burnable by the token holder and supports EIP2612 permits. Token holder can authorize a transfer of their token with a signature conforming EIP712 standard instead of an on-chain transaction from their address. Anyone can submit this signature on the user's behalf by calling the permit function, paying gas fees, and possibly performing other actions in the same transaction. The governance can recover ERC20 and ERC721 tokens sent mistakenly to `L2TBTC` token contract. # Testing All functions defined in `L2TBTC` contract are fully covered with tests. `L2TBTC` contract inherits from OpenZeppelin contracts and we do not want to test the framework. At the same time, we need to make sure all the declared functionalities are exposed by the contract and that they work. The tests must fail if the contract initialization gets broken or if one of the OpenZeppelin extensions is dropped from the inheritance. To make it happen, the tests cover really basic scenarios for the code implemented in OpenZeppelin ERC20 and extensions. The tests use `helpers.upgrades.deployProxy` to deploy `L2TBTC` and to test it as a contract deployed behind a proxy. There is a [small complication](https://github.com/keep-network/tbtc-v2/pull/550/files/7ae7122334a75cbbcd3a2b117082ad4e5ddde3d3#diff-35dc35185324e88cdeb94aa85d52542cd9bf3fb6b3bddddb2e2fcbf7a42c4ebd) with this approach that led to opening an issue in our hardhat plugins repo: keep-network/hardhat-helpers#38.
- Loading branch information
Showing
4 changed files
with
1,467 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
|
||
// ██████████████ ▐████▌ ██████████████ | ||
// ██████████████ ▐████▌ ██████████████ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
// ██████████████ ▐████▌ ██████████████ | ||
// ██████████████ ▐████▌ ██████████████ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
// ▐████▌ ▐████▌ | ||
|
||
pragma solidity ^0.8.17; | ||
|
||
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; | ||
|
||
/// @title L2TBTC | ||
/// @notice Canonical L2/sidechain token implementation. tBTC token is minted on | ||
/// L1 and locked there to be moved to L2/sidechain. By deploying | ||
/// a canonical token on each L2/sidechain, we can ensure the supply of | ||
/// tBTC remains sacrosanct, while enabling quick, interoperable | ||
/// cross-chain bridges and localizing ecosystem risk. | ||
/// | ||
/// This contract is flexible enough to: | ||
/// - Delegate minting authority to a native bridge on the chain, if | ||
/// present. | ||
/// - Delegate minting authority to a short list of ecosystem bridges. | ||
/// - Have mints and burns paused by any one of n guardians, allowing | ||
/// avoidance of contagion in case of a chain- or bridge-specific | ||
/// incident. | ||
/// - Be governed and upgradeable. | ||
/// | ||
/// The token is burnable by the token holder and supports EIP2612 | ||
/// permits. Token holder can authorize a transfer of their token with | ||
/// a signature conforming EIP712 standard instead of an on-chain | ||
/// transaction from their address. Anyone can submit this signature on | ||
/// the user's behalf by calling the permit function, paying gas fees, | ||
/// and possibly performing other actions in the same transaction. | ||
/// The governance can recover ERC20 and ERC721 tokens sent mistakenly | ||
/// to L2TBTC token contract. | ||
contract L2TBTC is | ||
ERC20Upgradeable, | ||
ERC20BurnableUpgradeable, | ||
ERC20PermitUpgradeable, | ||
OwnableUpgradeable, | ||
PausableUpgradeable | ||
{ | ||
using SafeERC20Upgradeable for IERC20Upgradeable; | ||
|
||
/// @notice Indicates if the given address is a minter. Only minters can | ||
/// mint the token. | ||
mapping(address => bool) public isMinter; | ||
|
||
/// @notice List of all minters. | ||
address[] public minters; | ||
|
||
/// @notice Indicates if the given address is a guardian. Only guardians can | ||
/// pause token mints and burns. | ||
mapping(address => bool) public isGuardian; | ||
|
||
/// @notice List of all guardians. | ||
address[] public guardians; | ||
|
||
event MinterAdded(address indexed minter); | ||
event MinterRemoved(address indexed minter); | ||
|
||
event GuardianAdded(address indexed guardian); | ||
event GuardianRemoved(address indexed guardian); | ||
|
||
modifier onlyMinter() { | ||
require(isMinter[msg.sender], "Caller is not a minter"); | ||
_; | ||
} | ||
|
||
modifier onlyGuardian() { | ||
require(isGuardian[msg.sender], "Caller is not a guardian"); | ||
_; | ||
} | ||
|
||
/// @notice Initializes the token contract. | ||
/// @param _name The name of the token. | ||
/// @param _symbol The symbol of the token, usually a shorter version of the | ||
/// name. | ||
function initialize(string memory _name, string memory _symbol) | ||
external | ||
initializer | ||
{ | ||
// OpenZeppelin upgradeable contracts documentation says: | ||
// | ||
// "Use with multiple inheritance requires special care. Initializer | ||
// functions are not linearized by the compiler like constructors. | ||
// Because of this, each __{ContractName}_init function embeds the | ||
// linearized calls to all parent initializers. As a consequence, | ||
// calling two of these init functions can potentially initialize the | ||
// same contract twice." | ||
// | ||
// Note that ERC20 extensions do not linearize calls to ERC20Upgradeable | ||
// initializer so we call all extension initializers individually. At | ||
// the same time, ERC20PermitUpgradeable does linearize the call to | ||
// EIP712Upgradeable so we are not using the unchained initializer | ||
// versions. | ||
__ERC20_init(_name, _symbol); | ||
__ERC20Burnable_init(); | ||
__ERC20Permit_init(_name); | ||
__Ownable_init(); | ||
__Pausable_init(); | ||
} | ||
|
||
/// @notice Adds the address to the minters list. | ||
/// @dev Requirements: | ||
/// - The caller must be the contract owner. | ||
/// - `minter` must not be a minter address already. | ||
/// @param minter The address to be added as a minter. | ||
function addMinter(address minter) external onlyOwner { | ||
require(!isMinter[minter], "This address is already a minter"); | ||
isMinter[minter] = true; | ||
minters.push(minter); | ||
emit MinterAdded(minter); | ||
} | ||
|
||
/// @notice Removes the address from the minters list. | ||
/// @dev Requirements: | ||
/// - The caller must be the contract owner. | ||
/// - `minter` must be a minter address. | ||
/// @param minter The address to be removed from the minters list. | ||
function removeMinter(address minter) external onlyOwner { | ||
require(isMinter[minter], "This address is not a minter"); | ||
delete isMinter[minter]; | ||
|
||
// We do not expect too many minters so a simple loop is safe. | ||
for (uint256 i = 0; i < minters.length; i++) { | ||
if (minters[i] == minter) { | ||
minters[i] = minters[minters.length - 1]; | ||
// slither-disable-next-line costly-loop | ||
minters.pop(); | ||
break; | ||
} | ||
} | ||
|
||
emit MinterRemoved(minter); | ||
} | ||
|
||
/// @notice Adds the address to the guardians list. | ||
/// @dev Requirements: | ||
/// - The caller must be the contract owner. | ||
/// - `guardian` must not be a guardian address already. | ||
/// @param guardian The address to be added as a guardian. | ||
function addGuardian(address guardian) external onlyOwner { | ||
require(!isGuardian[guardian], "This address is already a guardian"); | ||
isGuardian[guardian] = true; | ||
guardians.push(guardian); | ||
emit GuardianAdded(guardian); | ||
} | ||
|
||
/// @notice Removes the address from the guardians list. | ||
/// @dev Requirements: | ||
/// - The caller must be the contract owner. | ||
/// - `guardian` must be a guardian address. | ||
/// @param guardian The address to be removed from the guardians list. | ||
function removeGuardian(address guardian) external onlyOwner { | ||
require(isGuardian[guardian], "This address is not a guardian"); | ||
delete isGuardian[guardian]; | ||
|
||
// We do not expect too many guardians so a simple loop is safe. | ||
for (uint256 i = 0; i < guardians.length; i++) { | ||
if (guardians[i] == guardian) { | ||
guardians[i] = guardians[guardians.length - 1]; | ||
// slither-disable-next-line costly-loop | ||
guardians.pop(); | ||
break; | ||
} | ||
} | ||
|
||
emit GuardianRemoved(guardian); | ||
} | ||
|
||
/// @notice Allows the governance of the token contract to recover any ERC20 | ||
/// sent mistakenly to the token contract address. | ||
/// @param token The address of the token to be recovered. | ||
/// @param recipient The token recipient address that will receive recovered | ||
/// tokens. | ||
/// @param amount The amount to be recovered. | ||
function recoverERC20( | ||
IERC20Upgradeable token, | ||
address recipient, | ||
uint256 amount | ||
) external onlyOwner { | ||
token.safeTransfer(recipient, amount); | ||
} | ||
|
||
/// @notice Allows the governance of the token contract to recover any | ||
/// ERC721 sent mistakenly to the token contract address. | ||
/// @param token The address of the token to be recovered. | ||
/// @param recipient The token recipient address that will receive the | ||
/// recovered token. | ||
/// @param tokenId The ID of the ERC721 token to be recovered. | ||
function recoverERC721( | ||
IERC721Upgradeable token, | ||
address recipient, | ||
uint256 tokenId, | ||
bytes calldata data | ||
) external onlyOwner { | ||
token.safeTransferFrom(address(this), recipient, tokenId, data); | ||
} | ||
|
||
/// @notice Allows one of the guardians to pause mints and burns allowing | ||
/// avoidance of contagion in case of a chain- or bridge-specific | ||
/// incident. | ||
/// @dev Requirements: | ||
/// - The caller must be a guardian. | ||
/// - The contract must not be already paused. | ||
function pause() external onlyGuardian { | ||
_pause(); | ||
} | ||
|
||
/// @notice Allows the governance to unpause mints and burns previously | ||
/// paused by one of the guardians. | ||
/// @dev Requirements: | ||
/// - The caller must be the contract owner. | ||
/// - The contract must be paused. | ||
function unpause() external onlyOwner { | ||
_unpause(); | ||
} | ||
|
||
/// @notice Allows one of the minters to mint `amount` tokens and assign | ||
/// them to `account`, increasing the total supply. Emits | ||
/// a `Transfer` event with `from` set to the zero address. | ||
/// @dev Requirements: | ||
/// - The caller must be a minter. | ||
/// - `account` must not be the zero address. | ||
/// @param account The address to receive tokens. | ||
/// @param amount The amount of token to be minted. | ||
function mint(address account, uint256 amount) | ||
external | ||
whenNotPaused | ||
onlyMinter | ||
{ | ||
_mint(account, amount); | ||
} | ||
|
||
/// @notice Destroys `amount` tokens from the caller. Emits a `Transfer` | ||
/// event with `to` set to the zero address. | ||
/// @dev Requirements: | ||
/// - The caller must have at least `amount` tokens. | ||
/// @param amount The amount of token to be burned. | ||
function burn(uint256 amount) public override whenNotPaused { | ||
super.burn(amount); | ||
} | ||
|
||
/// @notice Destroys `amount` tokens from `account`, deducting from the | ||
/// caller's allowance. Emits a `Transfer` event with `to` set to | ||
/// the zero address. | ||
/// @dev Requirements: | ||
/// - The che caller must have allowance for `accounts`'s tokens of at | ||
/// least `amount`. | ||
/// - `account` must not be the zero address. | ||
/// - `account` must have at least `amount` tokens. | ||
/// @param account The address owning tokens to be burned. | ||
/// @param amount The amount of token to be burned. | ||
function burnFrom(address account, uint256 amount) | ||
public | ||
override | ||
whenNotPaused | ||
{ | ||
super.burnFrom(account, amount); | ||
} | ||
|
||
/// @notice Allows to fetch a list of all minters. | ||
function getMinters() external view returns (address[] memory) { | ||
return minters; | ||
} | ||
|
||
/// @notice Allows to fetch a list of all guardians. | ||
function getGuardians() external view returns (address[] memory) { | ||
return guardians; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.