From 4e6b5482207cc7aac30aa779b92381dc2fb51376 Mon Sep 17 00:00:00 2001 From: TuDo1403 Date: Mon, 13 Jan 2025 15:31:20 +0700 Subject: [PATCH] feat(LegacyTokenMigrator): add impl --- foundry.toml | 2 +- src/interfaces/IWBTC.sol | 52 ++++++++ src/ronin/migration/LegacyTokenMigrator.sol | 124 ++++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/IWBTC.sol create mode 100644 src/ronin/migration/LegacyTokenMigrator.sol diff --git a/foundry.toml b/foundry.toml index 7035c36a..c24039be 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ ffi = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options -solc = '0.8.23' +solc = '0.8.27' extra_output = ["devdoc", "userdoc", "storagelayout"] fs_permissions = [{ access = "read-write", path = "./" }] evm_version = 'shanghai' diff --git a/src/interfaces/IWBTC.sol b/src/interfaces/IWBTC.sol new file mode 100644 index 00000000..ea5cc639 --- /dev/null +++ b/src/interfaces/IWBTC.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IWBTC { + event Approval(address indexed owner, address indexed spender, uint256 value); + event Paused(address account); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + event Transfer(address indexed from, address indexed to, uint256 value); + event Unpaused(address account); + + function BURNER_ROLE() external view returns (bytes32); + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + function MINTER_ROLE() external view returns (bytes32); + function PAUSER_ROLE() external view returns (bytes32); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf( + address account + ) external view returns (uint256); + function burn( + uint256 amount + ) external; + function burnFrom(address account, uint256 amount) external; + function decimals() external view returns (uint8); + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); + function getRoleAdmin( + bytes32 role + ) external view returns (bytes32); + function getRoleMember(bytes32 role, uint256 index) external view returns (address); + function getRoleMemberCount( + bytes32 role + ) external view returns (uint256); + function grantRole(bytes32 role, address account) external; + function hasRole(bytes32 role, address account) external view returns (bool); + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + function mint(address to, uint256 amount) external; + function name() external view returns (string memory); + function pause() external; + function paused() external view returns (bool); + function renounceRole(bytes32 role, address account) external; + function revokeRole(bytes32 role, address account) external; + function supportsInterface( + bytes4 interfaceId + ) external view returns (bool); + function symbol() external view returns (string memory); + function totalSupply() external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function unpause() external; +} diff --git a/src/ronin/migration/LegacyTokenMigrator.sol b/src/ronin/migration/LegacyTokenMigrator.sol new file mode 100644 index 00000000..80647ae5 --- /dev/null +++ b/src/ronin/migration/LegacyTokenMigrator.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IERC20MintBurn { + function mint(address to, uint256 amount) external; + + function burn( + uint256 amount + ) external; + + function burnFrom(address account, uint256 amount) external; +} + +contract LegacyTokenMigrator is Initializable, AccessControlEnumerable { + using SafeERC20 for IERC20; + + mapping(address legacyToken => address newToken) internal _tokenMap; + + error MintTokenFailed(address token, uint256 amount); + error NullAmount(); + error TokenNotMapped(); + error LengthMismatch(); + error DecimalsMismatch(uint8 legacyDecimals, uint8 newDecimals); + + event TokenMapped(address indexed by, address indexed legacyToken, address indexed newToken); + event Converted(address indexed user, address indexed fromToken, address indexed toToken, uint256 amount); + event LegacyTokenBurned(address indexed by, address indexed token, uint256 amount); + event LegacyTokenLocked(address indexed by, address indexed token, uint256 amount); + + constructor() { + _disableInitializers(); + } + + function initialize(address admin, address[] calldata legacyTokens, address[] calldata newTokens) external initializer { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + + require(legacyTokens.length == newTokens.length, LengthMismatch()); + + for (uint256 i; i < legacyTokens.length; ++i) { + _mapToken(legacyTokens[i], newTokens[i]); + } + } + + /** + * @dev See {_mapToken}. + */ + function mapToken(address legacyToken, address newToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + _mapToken(legacyToken, newToken); + } + + /** + * @dev Converts a legacy token to a new token. + * + * Requirements: + * - The amount must be non-zero. + * - `msg.sender` must approve the contract to spend the legacy token. + * + * @param legacyToken Address of the legacy token. + * @param amount Amount of the legacy token to convert. + */ + function convert(address legacyToken, uint256 amount) external { + address newToken = getTokenMap(legacyToken); + + require(amount != 0, NullAmount()); + require(newToken != address(0), TokenNotMapped()); + + // Attempt to burn the legacy token, otherwise lock it in the contract + try IERC20MintBurn(legacyToken).burnFrom(msg.sender, amount) { + emit LegacyTokenBurned(msg.sender, legacyToken, amount); + } catch { + IERC20(legacyToken).safeTransferFrom(msg.sender, address(this), amount); + emit LegacyTokenLocked(msg.sender, legacyToken, amount); + } + + // Mint the new token if the contract balance is insufficient + uint256 selfBalance = IERC20(newToken).balanceOf(address(this)); + if (selfBalance < amount) { + try IERC20MintBurn(newToken).mint(address(this), amount - selfBalance) { } + catch { + revert MintTokenFailed(newToken, amount - selfBalance); + } + } + + // Transfer the new token to the user + IERC20(newToken).safeTransfer(msg.sender, amount); + + emit Converted(msg.sender, legacyToken, newToken, amount); + } + + /** + * @dev Returns the new token address for a given legacy token. + */ + function getTokenMap( + address legacyToken + ) public view returns (address) { + return _tokenMap[legacyToken]; + } + + /** + * @dev Maps a legacy token to a new token. + * + * Requirements: + * - Caller must have the `DEFAULT_ADMIN_ROLE`. + * - Decimals of the legacy token and the new token must match. + * + * @param legacyToken Address of the legacy token. + * @param newToken Address of the new token. + */ + function _mapToken(address legacyToken, address newToken) internal { + _tokenMap[legacyToken] = newToken; + + uint8 legacyDecimals = IERC20Metadata(legacyToken).decimals(); + uint8 newDecimals = IERC20Metadata(newToken).decimals(); + require(legacyDecimals == newDecimals, DecimalsMismatch(legacyDecimals, newDecimals)); + + emit TokenMapped(msg.sender, legacyToken, newToken); + } +}