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)!: give interchain token mint/burn permission to token manager #314

Merged
merged 11 commits into from
Jan 15, 2025
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

milapsheth marked this conversation as resolved.
Show resolved Hide resolved
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.
milapsheth marked this conversation as resolved.
Show resolved Hide resolved

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
29 changes: 11 additions & 18 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 @@ -38,12 +38,11 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
/// @dev Track the flow amount being received via the message
ITokenManager(tokenManager).addFlowIn(amount);

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)) {
if (
tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) ||
tokenManagerType == uint256(TokenManagerType.MINT_BURN) ||
tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)
) {
_mintToken(tokenManager, tokenAddress, to, amount);
return (amount, tokenAddress);
}
Expand Down Expand Up @@ -76,9 +75,9 @@ 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)) {
if (
tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN)
) {
_burnToken(tokenManager, tokenAddress, from, amount);
} else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) {
_burnTokenFrom(tokenAddress, from, amount);
Expand Down Expand Up @@ -137,6 +136,8 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea
// For lock/unlock token managers, the ITS contract needs an approval from the token manager to transfer tokens on its behalf
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) {
ITokenManager(tokenManager).approveService();
} else if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) {
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
IMinter(ITokenManager(tokenManager).tokenAddress()).transferMintership(tokenManager);
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -160,14 +161,6 @@ 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 _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);
}
Expand Down
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
2 changes: 1 addition & 1 deletion test/InterchainTokenService.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,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