Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(token-handler)!: added automatic token migration #317

Merged
merged 35 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5b1b544
Added the basic changes.
Foivos Dec 23, 2024
3be2fda
made lint happy
Foivos Jan 2, 2025
8e0c290
Added some functions to migrate tokens
Foivos Jan 6, 2025
58e956e
added automatic token migration
Foivos Jan 6, 2025
af995b8
Merge branch 'main' into feat/token-manager-mint-interchain-tokens-2
milapsheth Jan 7, 2025
4c7e551
Merge branch 'feat/token-manager-mint-interchain-tokens-2' into feat/…
milapsheth Jan 7, 2025
6cd5926
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
milapsheth Jan 7, 2025
a833844
Update contracts/InterchainTokenService.sol
Foivos Jan 9, 2025
123c9c1
removed migrate legacy token and added tests
Foivos Jan 9, 2025
3c63b22
added a docstring
Foivos Jan 9, 2025
70a0796
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
Foivos Jan 9, 2025
b2b367b
fixed tests
Foivos Jan 9, 2025
716fb46
made lint happy
Foivos Jan 9, 2025
de53714
addressed comments
Foivos Jan 10, 2025
5bcb97a
prettier
Foivos Jan 10, 2025
e4f3152
Merge branch 'feat/token-manager-mint-interchain-tokens-2' into feat/…
Foivos Jan 10, 2025
1f6eb3f
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
Foivos Jan 10, 2025
9c9f5f2
fix tests
Foivos Jan 10, 2025
2a606ec
Apply suggestions from code review
milapsheth Jan 10, 2025
09de417
Merge branch 'main' into feat/token-manager-mint-interchain-tokens-2
milapsheth Jan 13, 2025
cd4341f
Merge branch 'main' into feat/token-manager-mint-interchain-tokens-2
milapsheth Jan 15, 2025
bc6d739
Merge branch 'feat/token-manager-mint-interchain-tokens-2' into feat/…
milapsheth Jan 15, 2025
69f8dc6
cleanup
milapsheth Jan 15, 2025
d7e6c39
cs
milapsheth Jan 15, 2025
ac912cf
fmt
milapsheth Jan 15, 2025
a21d366
stash
Foivos Jan 15, 2025
7edef20
Merge branch 'main' into feat/manual-migrate-tokens
Foivos Jan 16, 2025
379a08c
Fixed tests
Foivos Jan 16, 2025
cd3826a
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
Foivos Jan 16, 2025
c3ae998
made lint happy
Foivos Jan 16, 2025
9a729bd
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
Foivos Jan 16, 2025
a5ad995
fixed tests
Foivos Jan 16, 2025
ba0334d
prettier
Foivos Jan 16, 2025
0d7dbfc
Merge branch 'feat/manual-migrate-tokens' into feat/automcatic-migrat…
Foivos Jan 16, 2025
593c674
Merge branch 'main' into feat/automcatic-migrate-tokens
milapsheth Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-tigers-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@axelar-network/interchain-token-service': minor
---

Interchain tokens now get minted/burnt by the token manager.
5 changes: 5 additions & 0 deletions .changeset/quick-ants-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@axelar-network/interchain-token-service': minor
---

Add auto-migration of minter for native interchain tokens
2 changes: 2 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Most current bridge designs aim to transfer a pre-existing, popular token to dif

We designed an [interface](./contracts/interfaces/IInterchainTokenStandard.sol) along with an [example implementation](./contracts/interchain-token/InterchainTokenStandard.sol) of an ERC20 that can use the `InterchainTokenService` internally. This has the main benefit that for `TokenManagers` that require user approval (Lock/Unlock, Lock/Unlock Fee and Mint/BurnFrom), the token can provide this approval within the same call, providing better UX for users, and saving them some gas.

Interchain Tokens function the same as mint/burn tokens do: The `tokenManager` that manages them will ask them to `burn` tokens on the sending chain, and to `mint` tokens on the receiving chain.

## Interchain Communication Spec

The messages going through the Axelar Network between `InterchainTokenServices` need to have a consistent format to be understood properly. We chose to use `abi` encoding because it is easy to use in EVM chains, which are at the front and center of programmable blockchains, and because it is easy to implement in other ecosystems which tend to be more gas efficient. There are currently three supported message types: `INTERCHAIN_TRANSFER`, `DEPLOY_INTERCHAIN_TOKEN`, `DEPLOY_TOKEN_MANAGER`.
Expand Down
11 changes: 11 additions & 0 deletions contracts/InterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IInterchainTokenExecutable } from './interfaces/IInterchainTokenExecuta
import { IInterchainTokenExpressExecutable } from './interfaces/IInterchainTokenExpressExecutable.sol';
import { ITokenManager } from './interfaces/ITokenManager.sol';
import { IGatewayCaller } from './interfaces/IGatewayCaller.sol';
import { IMinter } from './interfaces/IMinter.sol';
import { Create3AddressFixed } from './utils/Create3AddressFixed.sol';
import { Operator } from './utils/Operator.sol';

Expand Down Expand Up @@ -643,6 +644,16 @@ contract InterchainTokenService is
}
}

/**
* @notice Allows the owner to migrate minter of native interchain tokens from ITS to the corresponding token manager.
* @param tokenId the tokenId of the registered token.
*/
function migrateInterchainToken(bytes32 tokenId) external onlyOwner {
ITokenManager tokenManager_ = deployedTokenManager(tokenId);
address tokenAddress = tokenManager_.tokenAddress();
IMinter(tokenAddress).transferMintership(address(tokenManager_));
}

/****************\
INTERNAL FUNCTIONS
\****************/
Expand Down
85 changes: 47 additions & 38 deletions contracts/TokenHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { Create3AddressFixed } from './utils/Create3AddressFixed.sol';
import { ITokenManagerType } from './interfaces/ITokenManagerType.sol';
import { ITokenManager } from './interfaces/ITokenManager.sol';
import { ITokenManagerProxy } from './interfaces/ITokenManagerProxy.sol';
import { IERC20MintableBurnable } from './interfaces/IERC20MintableBurnable.sol';
import { IERC20BurnableFrom } from './interfaces/IERC20BurnableFrom.sol';
import { IMinter } from './interfaces/IMinter.sol';

/**
* @title TokenHandler
Expand All @@ -35,30 +35,26 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea

(uint256 tokenManagerType, address tokenAddress) = ITokenManagerProxy(tokenManager).getImplementationTypeAndTokenAddress();

_migrateToken(tokenManager, tokenAddress, tokenManagerType);

/// @dev Track the flow amount being received via the message
ITokenManager(tokenManager).addFlowIn(amount);
milapsheth marked this conversation as resolved.
Show resolved Hide resolved

if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) {
_giveInterchainToken(tokenAddress, to, amount);
return (amount, tokenAddress);
}

if (tokenManagerType == uint256(TokenManagerType.MINT_BURN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) {
_mintToken(tokenManager, tokenAddress, to, amount);
return (amount, tokenAddress);
}

if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) {
if (
tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) ||
tokenManagerType == uint256(TokenManagerType.MINT_BURN) ||
tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)
) {
_mintToken(ITokenManager(tokenManager), tokenAddress, to, amount);
} else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) {
_transferTokenFrom(tokenAddress, tokenManager, to, amount);
return (amount, tokenAddress);
}

if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) {
} else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) {
amount = _transferTokenFromWithFee(tokenAddress, tokenManager, to, amount);
return (amount, tokenAddress);
} else {
revert UnsupportedTokenManagerType(tokenManagerType);
}

revert UnsupportedTokenManagerType(tokenManagerType);
return (amount, tokenAddress);
}

/**
Expand All @@ -76,10 +72,12 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea

if (tokenOnly && msg.sender != tokenAddress) revert NotToken(msg.sender, tokenAddress);

if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) {
_takeInterchainToken(tokenAddress, from, amount);
} else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN)) {
_burnToken(tokenManager, tokenAddress, from, amount);
_migrateToken(tokenManager, tokenAddress, tokenManagerType);

if (
tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN)
) {
_burnToken(ITokenManager(tokenManager), tokenAddress, from, amount);
} else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) {
_burnTokenFrom(tokenAddress, from, amount);
} else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) {
Expand Down Expand Up @@ -133,10 +131,16 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
* @param tokenManager The address of the token manager.
*/
// slither-disable-next-line locked-ether
function postTokenManagerDeploy(uint256 tokenManagerType, address tokenManager) external payable {
// For lock/unlock token managers, the ITS contract needs an approval from the token manager to transfer tokens on its behalf
if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) {
ITokenManager(tokenManager).approveService();
function postTokenManagerDeploy(uint256 tokenManagerType, ITokenManager tokenManager) external payable {
// For native interhcain tokens we transfer mintership to the token manager.
// This is done here because InterchainToken bytecode needs to be fixed.
if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) {
IMinter(tokenManager.tokenAddress()).transferMintership(address(tokenManager));
// For lock/unlock token managers, the ITS contract needs an approval from the token manager to transfer tokens on its behalf.
} else if (
tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)
) {
tokenManager.approveService();
}
}

Expand All @@ -160,23 +164,28 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
return diff < amount ? diff : amount;
}

function _giveInterchainToken(address tokenAddress, address to, uint256 amount) internal {
IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.mint.selector, to, amount));
function _mintToken(ITokenManager tokenManager, address tokenAddress, address to, uint256 amount) internal {
tokenManager.mintToken(tokenAddress, to, amount);
}

function _takeInterchainToken(address tokenAddress, address from, uint256 amount) internal {
IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.burn.selector, from, amount));
}

function _mintToken(address tokenManager, address tokenAddress, address to, uint256 amount) internal {
ITokenManager(tokenManager).mintToken(tokenAddress, to, amount);
}

function _burnToken(address tokenManager, address tokenAddress, address from, uint256 amount) internal {
ITokenManager(tokenManager).burnToken(tokenAddress, from, amount);
function _burnToken(ITokenManager tokenManager, address tokenAddress, address from, uint256 amount) internal {
tokenManager.burnToken(tokenAddress, from, amount);
}

function _burnTokenFrom(address tokenAddress, address from, uint256 amount) internal {
IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20BurnableFrom.burnFrom.selector, from, amount));
}

/**
* @notice This transfers mintership of a native Interchain token to the tokenManager if ITS is still its minter.
* It does nothing if ITS is not the minter. This ensures that interchain tokens are auto-migrated without requiring a downtime for ITS.
* @param tokenManager The token manager address to transfer mintership to.
* @param tokenAddress The address of the token to transfer mintership of.
* @param tokenManagerType The token manager type for the token.
*/
function _migrateToken(address tokenManager, address tokenAddress, uint256 tokenManagerType) internal {
if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) && IMinter(tokenAddress).isMinter(address(this))) {
IMinter(tokenAddress).transferMintership(tokenManager);
}
}
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 6 additions & 0 deletions contracts/interfaces/IInterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,10 @@ interface IInterchainTokenService is
* @param paused whether to pause or unpause.
*/
function setPauseStatus(bool paused) external;

/**
* @notice Allows the owner to migrate legacy tokens that cannot be migrated automatically.
* @param tokenId the tokenId of the registered token.
*/
function migrateInterchainToken(bytes32 tokenId) external;
}
4 changes: 3 additions & 1 deletion contracts/interfaces/ITokenHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

pragma solidity ^0.8.0;

import { ITokenManager } from './ITokenManager.sol';

/**
* @title ITokenHandler Interface
* @notice This interface is responsible for handling tokens before initiating an interchain token transfer, or after receiving one.
Expand Down Expand Up @@ -47,5 +49,5 @@ interface ITokenHandler {
* @param tokenManagerType The token manager type.
* @param tokenManager The address of the token manager.
*/
function postTokenManagerDeploy(uint256 tokenManagerType, address tokenManager) external payable;
function postTokenManagerDeploy(uint256 tokenManagerType, ITokenManager tokenManager) external payable;
}
12 changes: 8 additions & 4 deletions test/InterchainTokenFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('InterchainTokenFactory', () => {
const checkRoles = async (tokenManager, minter) => {
const token = await getContractAt('InterchainToken', await tokenManager.tokenAddress(), wallet);
expect(await token.isMinter(minter)).to.be.true;
expect(await token.isMinter(service.address)).to.be.true;
expect(await token.isMinter(tokenManager.address)).to.be.true;

expect(await tokenManager.isOperator(minter)).to.be.true;
expect(await tokenManager.isOperator(service.address)).to.be.true;
Expand Down Expand Up @@ -332,7 +332,11 @@ describe('InterchainTokenFactory', () => {
.and.to.emit(tokenManager, 'RolesRemoved')
.withArgs(tokenFactory.address, 1 << OPERATOR_ROLE)
.and.to.emit(tokenManager, 'RolesRemoved')
.withArgs(tokenFactory.address, 1 << FLOW_LIMITER_ROLE);
.withArgs(tokenFactory.address, 1 << FLOW_LIMITER_ROLE)
.and.to.emit(token, 'RolesRemoved')
.withArgs(service.address, 1 << MINTER_ROLE)
.and.to.emit(token, 'RolesAdded')
.withArgs(tokenManager.address, 1 << MINTER_ROLE);

const payload = defaultAbiCoder.encode(
['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes'],
Expand Down Expand Up @@ -388,7 +392,7 @@ describe('InterchainTokenFactory', () => {
},
),
tokenFactory,
'InvalidMinter',
'NotMinter',
[service.address],
);

Expand Down Expand Up @@ -441,7 +445,7 @@ describe('InterchainTokenFactory', () => {
},
),
tokenFactory,
'InvalidMinter',
'NotMinter',
[service.address],
);

Expand Down
77 changes: 76 additions & 1 deletion test/InterchainTokenService.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
ITS_HUB_CHAIN_NAME,
ITS_HUB_ROUTING_IDENTIFIER,
ITS_HUB_ADDRESS,
MINTER_ROLE,
} = require('./constants');

const reportGas = gasReporter('Interchain Token Service');
Expand Down Expand Up @@ -646,7 +647,7 @@ describe('Interchain Token Service', () => {

const token = await getContractAt('InterchainToken', tokenAddress, wallet);
expect(await token.isMinter(wallet.address)).to.be.true;
expect(await token.isMinter(service.address)).to.be.true;
expect(await token.isMinter(tokenManager.address)).to.be.true;
});

it('Should revert when registering an interchain token as a lock/unlock for a second time', async () => {
Expand Down Expand Up @@ -3235,6 +3236,80 @@ describe('Interchain Token Service', () => {
});
});

describe('Interchain Token Migration', () => {
it('Should migrate a token succesfully', async () => {
const salt = getRandomBytes32();
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;
const tokenId = await service.interchainTokenId(wallet.address, salt);
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);

await interchainTokenDeployer
.deployInterchainToken(salt, tokenId, service.address, name, symbol, decimals)
.then((tx) => tx.wait);
const tokenAddress = await interchainTokenDeployer.deployedAddress(salt);
const token = await getContractAt('InterchainToken', tokenAddress, wallet);

const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, tokenAddress]);

await service.deployTokenManager(salt, '', MINT_BURN, params, 0).then((tx) => tx.wait);

await expect(service.migrateInterchainToken(tokenId))
.to.emit(token, 'RolesRemoved')
.withArgs(service.address, 1 << MINTER_ROLE)
.to.emit(token, 'RolesAdded')
.withArgs(tokenManagerAddress, 1 << MINTER_ROLE);
});

it('Should not be able to migrate a token twice', async () => {
const salt = getRandomBytes32();
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;
const tokenId = await service.interchainTokenId(wallet.address, salt);
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);

await interchainTokenDeployer
.deployInterchainToken(salt, tokenId, service.address, name, symbol, decimals)
.then((tx) => tx.wait);
const tokenAddress = await interchainTokenDeployer.deployedAddress(salt);
const token = await getContractAt('InterchainToken', tokenAddress, wallet);

const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, tokenAddress]);

await service.deployTokenManager(salt, '', MINT_BURN, params, 0).then((tx) => tx.wait);

await expect(service.migrateInterchainToken(tokenId))
.to.emit(token, 'RolesRemoved')
.withArgs(service.address, 1 << MINTER_ROLE)
.to.emit(token, 'RolesAdded')
.withArgs(tokenManagerAddress, 1 << MINTER_ROLE);

await expectRevert((gasOptions) => service.migrateInterchainToken(tokenId, { gasOptions }), token, 'MissingRole', [
service.address,
MINTER_ROLE,
]);
});

it('Should not be able to migrate a token deployed after this upgrade', async () => {
const salt = getRandomBytes32();
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;
const tokenId = await service.interchainTokenId(wallet.address, salt);

await service.deployInterchainToken(salt, '', name, symbol, decimals, AddressZero, 0).then((tx) => tx.wait);
const tokenAddress = await service.interchainTokenAddress(tokenId);
const token = await getContractAt('InterchainToken', tokenAddress, wallet);

await expectRevert((gasOptions) => service.migrateInterchainToken(tokenId, { gasOptions }), token, 'MissingRole', [
service.address,
MINTER_ROLE,
]);
});
});

describe('Bytecode checks [ @skip-on-coverage ]', () => {
it('Should preserve the same proxy bytecode for each EVM', async () => {
const proxyFactory = await ethers.getContractFactory('InterchainProxy', wallet);
Expand Down
Loading