Skip to content

Commit

Permalink
feat(LegacyTokenMigrator): add impl
Browse files Browse the repository at this point in the history
  • Loading branch information
TuDo1403 committed Jan 13, 2025
1 parent 150ab55 commit 4e6b548
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 1 deletion.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
52 changes: 52 additions & 0 deletions src/interfaces/IWBTC.sol
Original file line number Diff line number Diff line change
@@ -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;
}
124 changes: 124 additions & 0 deletions src/ronin/migration/LegacyTokenMigrator.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 4e6b548

Please sign in to comment.