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 29 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/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
11 changes: 11 additions & 0 deletions contracts/InterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { IInterchainTokenExpressExecutable } from './interfaces/IInterchainToken
import { ITokenManager } from './interfaces/ITokenManager.sol';
import { IERC20Named } from './interfaces/IERC20Named.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 @@ -647,6 +648,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
32 changes: 22 additions & 10 deletions contracts/TokenHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ 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

Expand All @@ -44,20 +46,15 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)
) {
_mintToken(ITokenManager(tokenManager), tokenAddress, to, amount);
return (amount, tokenAddress);
}

if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) {
} 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 @@ -75,6 +72,8 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea

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

_migrateToken(tokenManager, tokenAddress, tokenManagerType);

if (
tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN)
) {
Expand Down Expand Up @@ -176,4 +175,17 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
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 @@ -267,4 +267,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;
}
32 changes: 32 additions & 0 deletions test/InterchainTokenFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,23 @@
);
});

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 tokenFactory.interchainTokenId(wallet.address, salt);

await tokenFactory.deployInterchainToken(salt, name, symbol, decimals, 0, wallet.address).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('Custom Token Manager Deployment', () => {
const tokenName = 'Token Name';
const tokenSymbol = 'TN';
Expand Down Expand Up @@ -761,6 +778,21 @@
);
});

it('Should revert when deploying a custom token when the service is paused', async () => {
const salt = getRandomBytes32();
const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
const deploySalt = await tokenFactory.linkedTokenDeploySalt(wallet.address, salt);

Check failure on line 784 in test/InterchainTokenFactory.js

View workflow job for this annotation

GitHub Actions / lint

'deploySalt' is assigned a value but never used
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);

Check failure on line 785 in test/InterchainTokenFactory.js

View workflow job for this annotation

GitHub Actions / lint

'tokenManagerAddress' is assigned a value but never used
const gasValue = 1;
const params = defaultAbiCoder.encode(['bytes', 'address'], ['0x', token.address]);

Check failure on line 787 in test/InterchainTokenFactory.js

View workflow job for this annotation

GitHub Actions / lint

'params' is assigned a value but never used

await service.setPauseStatus(true).then((tx) => tx.wait);

await expectRevert((gasOptions) => tokenFactory.registerCustomToken(salt, token.address, LOCK_UNLOCK, AddressZero, gasValue, { value: gasValue, ...gasOptions }), service, 'Pause');

await service.setPauseStatus(false).then((tx) => tx.wait);
});

it('Should register a custom token with no operator', async () => {
const salt = getRandomBytes32();
const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
Expand Down
70 changes: 66 additions & 4 deletions test/InterchainTokenService.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ITS_HUB_CHAIN_NAME,
ITS_HUB_ROUTING_IDENTIFIER,
ITS_HUB_ADDRESS,
MINTER_ROLE,
MESSAGE_TYPE_REGISTER_TOKEN_METADATA,
} = require('./constants');

Expand Down Expand Up @@ -213,19 +214,18 @@
return [token, tokenManager, tokenId];
};

async function deployNewInterchainToken(service, tokenName, tokenSymbol, tokenDecimals, mintAmount = 0, skipApprove = false) {
async function deployNewInterchainToken(service, tokenName, tokenSymbol, tokenDecimals, minter = null, mintAmount = 0, skipApprove = false) {
const salt = getRandomBytes32();
const tokenId = await service.interchainTokenId(wallet.address, salt);
const sourceAddress = service.address;

const tokenManagerAddress = await service.tokenManagerAddress(tokenId);
const minter = wallet.address;
const operator = '0x';
const tokenAddress = await service.interchainTokenAddress(tokenId);
const params = defaultAbiCoder.encode(['bytes', 'address'], [minter, tokenAddress]);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, tokenAddress]);
const payload = defaultAbiCoder.encode(
['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes', 'bytes'],
[MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, tokenName, tokenSymbol, tokenDecimals, minter, operator],
[MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, tokenName, tokenSymbol, tokenDecimals, wallet.address, operator],
);
const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload);

Expand All @@ -244,6 +244,9 @@
await token.mint(wallet.address, mintAmount).then((tx) => tx.wait);
if (!skipApprove) await token.approve(service.address, mintAmount).then((tx) => tx.wait);
}
if (minter) {

Check failure on line 247 in test/InterchainTokenService.js

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
await token.transferMintership(minter).then((tx) => tx.wait);
}

return [token, tokenManager, tokenId, salt];
}
Expand Down Expand Up @@ -1175,6 +1178,20 @@
);
});

it('Should revert on initiate an interchain token transfer to the ITS HUB', async () => {
const [, , tokenId] = await deployFunctions.lockUnlockFee(
service,
'Test Token lockUnlockFee',
'TT',
12,
amount,
false,
'free',
);

await expectRevert((gasOptions) => service.interchainTransfer(tokenId, ITS_HUB_CHAIN_NAME, destAddress, amount, '0x', gasValue, { value: gasValue, ...gasOptions }), service, 'UntrustedChain');
});

it('Should revert on initiate interchain token transfer when service is paused', async () => {
await service.setPauseStatus(true).then((tx) => tx.wait);

Expand Down Expand Up @@ -2908,6 +2925,51 @@
});
});

describe('Interchain Token Migration', () => {
it('Should migrate a token succesfully', async () => {
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;

const [token, tokenManager, tokenId] = await deployFunctions.interchainToken(service, name, symbol, decimals, service.address);

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

it('Should not be able to migrate a token twice', async () => {
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;

const [token, tokenManager, tokenId] = await deployFunctions.interchainToken(service, name, symbol, decimals, service.address);

await expect(service.migrateInterchainToken(tokenId))
.to.emit(token, 'RolesRemoved')
.withArgs(service.address, 1 << MINTER_ROLE)
.to.emit(token, 'RolesAdded')
.withArgs(tokenManager.address, 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 as not the owner', async () => {
const name = 'migrated token';
const symbol = 'MT';
const decimals = 53;

const [, , tokenId] = await deployFunctions.interchainToken(service, name, symbol, decimals, service.address);

await expectRevert((gasOptions) => service.connect(otherWallet).migrateInterchainToken(tokenId, gasOptions), service, 'NotOwner');
})
});

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