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(its): add custom token support with auto scaling #311

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
exit 0;
fi

- name: Set environment variable
run: export CHECK_CONTRACT_SIZE=true
# - name: Set environment variable
# run: export CHECK_CONTRACT_SIZE=true

- name: Test
run: npm run test
51 changes: 49 additions & 2 deletions contracts/InterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Multicall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/uti
import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol';
import { IInterchainTokenService } from './interfaces/IInterchainTokenService.sol';
import { IInterchainTokenFactory } from './interfaces/IInterchainTokenFactory.sol';
import { ITokenManagerType } from './interfaces/ITokenManagerType.sol';
import { ITokenManager } from './interfaces/ITokenManager.sol';
import { IInterchainToken } from './interfaces/IInterchainToken.sol';
import { IERC20Named } from './interfaces/IERC20Named.sol';
Expand All @@ -16,7 +15,7 @@ import { IERC20Named } from './interfaces/IERC20Named.sol';
* @title InterchainTokenFactory
* @notice This contract is responsible for deploying new interchain tokens and managing their token managers.
*/
contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, Multicall, Upgradable {
contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradable {
using AddressBytes for address;

/// @dev This slot contains the storage for this contract in an upgrade-compatible manner
Expand All @@ -26,6 +25,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
bytes32 private constant CONTRACT_ID = keccak256('interchain-token-factory');
bytes32 internal constant PREFIX_CANONICAL_TOKEN_SALT = keccak256('canonical-token-salt');
bytes32 internal constant PREFIX_INTERCHAIN_TOKEN_SALT = keccak256('interchain-token-salt');
bytes32 internal constant PREFIX_CUSTOM_TOKEN_SALT = keccak256('custom-token-salt');
bytes32 internal constant PREFIX_DEPLOY_APPROVAL = keccak256('deploy-approval');
address private constant TOKEN_FACTORY_DEPLOYER = address(0);

Expand Down Expand Up @@ -484,6 +484,53 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
tokenId = deployRemoteCanonicalInterchainToken(originalTokenAddress, destinationChain, gasValue);
}

function linkedTokenDeploySalt(address deployer, bytes32 salt) public view returns (bytes32 deploySalt) {
deploySalt = keccak256(abi.encode(PREFIX_CUSTOM_TOKEN_SALT, chainNameHash, deployer, salt));
}

function linkedTokenId(address deployer, bytes32 salt) external view returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(deployer, salt);
tokenId = _interchainTokenId(deploySalt);
}

function registerCustomToken(
bytes32 salt,
address tokenAddress,
TokenManagerType tokenManagerType,
address operator,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt);
bytes memory operatorBytes = '';
string memory currentChain = '';
if (operator != address(0)) {
operatorBytes = operator.toBytes();
}

tokenId = interchainTokenService.linkToken(deploySalt, currentChain, tokenAddress.toBytes(), tokenManagerType, operatorBytes, 0);

interchainTokenService.registerTokenMetadata{ value: gasValue }(tokenAddress, gasValue);
}

function linkToken(
bytes32 salt,
string calldata destinationChain,
bytes calldata destinationTokenAddress,
TokenManagerType tokenManagerType,
bytes calldata linkParams,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt);
tokenId = interchainTokenService.linkToken(
deploySalt,
destinationChain,
destinationTokenAddress,
tokenManagerType,
linkParams,
gasValue
);
}

/********************\
|* Pure Key Getters *|
\********************/
Expand Down
124 changes: 89 additions & 35 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 { IERC20Named } from './interfaces/IERC20Named.sol';
import { Create3AddressFixed } from './utils/Create3AddressFixed.sol';
import { Operator } from './utils/Operator.sol';

Expand Down Expand Up @@ -79,9 +80,12 @@ contract InterchainTokenService is

uint256 private constant MESSAGE_TYPE_INTERCHAIN_TRANSFER = 0;
uint256 private constant MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN = 1;
uint256 private constant MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER = 2;
// Deprecated
// uint256 private constant MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER = 2;
uint256 private constant MESSAGE_TYPE_SEND_TO_HUB = 3;
uint256 private constant MESSAGE_TYPE_RECEIVE_FROM_HUB = 4;
uint256 private constant MESSAGE_TYPE_LINK_TOKEN = 5;
uint256 private constant MESSAGE_TYPE_REGISTER_TOKEN_METADATA = 6;

/**
* @dev Tokens and token managers deployed via the Token Factory contract use a special deployer address.
Expand Down Expand Up @@ -279,6 +283,36 @@ contract InterchainTokenService is
USER FUNCTIONS
\************/

/**
* @notice Registers metadata for a token on the ITS Hub. This metadata is used for scaling linked tokens.
* @param tokenAddress The address of the token.
* @param gasValue The gas value for deployment.
*/
function registerTokenMetadata(address tokenAddress, uint256 gasValue) external payable {
if (tokenAddress == address(0)) revert ZeroAddress();

uint8 decimals = IERC20Named(tokenAddress).decimals();

bytes memory payload = abi.encode(MESSAGE_TYPE_REGISTER_TOKEN_METADATA, tokenAddress.toBytes(), decimals);

string memory destinationAddress = trustedAddress(ITS_HUB_CHAIN_NAME);
// Check whether no trusted address was set for ITS Hub chain
if (bytes(destinationAddress).length == 0) revert UntrustedChain();

(bool success, bytes memory returnData) = gatewayCaller.delegatecall(
abi.encodeWithSelector(
IGatewayCaller.callContract.selector,
ITS_HUB_CHAIN_NAME,
destinationAddress,
payload,
IGatewayCaller.MetadataVersion.CONTRACT_CALL,
gasValue
)
);

if (!success) revert GatewayCallFailed(returnData);
}

/**
* @notice Used to deploy remote custom TokenManagers.
* @dev At least the `gasValue` amount of native token must be passed to the function call. `gasValue` exists because this function can be
Expand All @@ -301,31 +335,46 @@ contract InterchainTokenService is
TokenManagerType tokenManagerType,
bytes calldata params,
uint256 gasValue
) external payable whenNotPaused returns (bytes32 tokenId) {
if (bytes(params).length == 0) revert EmptyParams();
) external payable returns (bytes32 tokenId) {
(bytes memory linkParams, address destinationTokenAddress) = abi.decode(params, (bytes, address));

tokenId = linkToken(salt, destinationChain, destinationTokenAddress.toBytes(), tokenManagerType, linkParams, gasValue);
}

function linkToken(
bytes32 salt,
string calldata destinationChain,
bytes memory destinationTokenAddress,
TokenManagerType tokenManagerType,
bytes memory linkParams,
uint256 gasValue
) public payable whenNotPaused returns (bytes32 tokenId) {
if (destinationTokenAddress.length == 0) revert EmptyDestinationAddress();

// TODO: Should we only allow mint/burn or lock/unlock for remote linking for simplicity? Makes it easier for external chains
// Custom token managers can't be deployed with native interchain token type, which is reserved for interchain tokens
if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType);

address deployer = msg.sender;

if (deployer == interchainTokenFactory) {
if (msg.sender == interchainTokenFactory) {
deployer = TOKEN_FACTORY_DEPLOYER;
} else if (bytes(destinationChain).length == 0 && trustedAddressHash(chainName()) == ITS_HUB_ROUTING_IDENTIFIER_HASH) {
// Restricted on ITS contracts deployed to Amplifier chains until ITS Hub adds support
revert NotSupported();
} else if (bytes(destinationChain).length == 0) {
// TODO: Only support linking new tokens via ITS factory, to include chain name in token id derivation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to already deployed custom tokens?

// Custom token usage needs to be moved to ITS factory tests
// revert NotSupported();
}

tokenId = interchainTokenId(deployer, salt);

emit InterchainTokenIdClaimed(tokenId, deployer, salt);

if (bytes(destinationChain).length == 0) {
_deployTokenManager(tokenId, tokenManagerType, params);
_deployTokenManager(tokenId, tokenManagerType, destinationTokenAddress.toAddress(), linkParams);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linkParams are just the operator for local deployments it seems.

} else {
if (chainNameHash == keccak256(bytes(destinationChain))) revert CannotDeployRemotelyToSelf();

_deployRemoteTokenManager(tokenId, destinationChain, gasValue, tokenManagerType, params);
_linkToken(tokenId, destinationChain, destinationTokenAddress, tokenManagerType, linkParams, gasValue);
}
}

Expand Down Expand Up @@ -370,7 +419,7 @@ contract InterchainTokenService is
if (bytes(destinationChain).length == 0) {
address tokenAddress = _deployInterchainToken(tokenId, minter, name, symbol, decimals);

_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minter, tokenAddress));
_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, tokenAddress, minter);
} else {
if (chainNameHash == keccak256(bytes(destinationChain))) revert CannotDeployRemotelyToSelf();

Expand Down Expand Up @@ -731,15 +780,13 @@ contract InterchainTokenService is
/**
* @notice Processes a deploy token manager payload.
*/
function _processDeployTokenManagerPayload(bytes memory payload) internal {
(, bytes32 tokenId, TokenManagerType tokenManagerType, bytes memory params) = abi.decode(
payload,
(uint256, bytes32, TokenManagerType, bytes)
);
function _processLinkTokenPayload(bytes memory payload) internal {
(, bytes32 tokenId, TokenManagerType tokenManagerType, , bytes memory destinationTokenAddress, , bytes memory linkParams) = abi
.decode(payload, (uint256, bytes32, TokenManagerType, bytes, bytes, bool, bytes));

if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType);

_deployTokenManager(tokenId, tokenManagerType, params);
_deployTokenManager(tokenId, tokenManagerType, destinationTokenAddress.toAddress(), linkParams);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think even if the only param left in EVM is the operator, we should have it be abi encoded when it is passed as params.

}

/**
Expand All @@ -755,7 +802,7 @@ contract InterchainTokenService is

tokenAddress = _deployInterchainToken(tokenId, minterBytes, name, symbol, decimals);

_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minterBytes, tokenAddress));
_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, tokenAddress, minterBytes);
}

/**
Expand Down Expand Up @@ -804,9 +851,6 @@ contract InterchainTokenService is

// Check whether the ITS call should be routed via ITS hub for this destination chain
if (keccak256(abi.encodePacked(destinationAddress)) == ITS_HUB_ROUTING_IDENTIFIER_HASH) {
// Prevent deploy token manager to be usable on ITS hub
if (_getMessageType(payload) == MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER) revert NotSupported();

// Wrap ITS message in an ITS Hub message
payload = abi.encode(MESSAGE_TYPE_SEND_TO_HUB, destinationChain, payload);
destinationChain = ITS_HUB_CHAIN_NAME;
Expand All @@ -833,10 +877,10 @@ contract InterchainTokenService is
if (messageType == MESSAGE_TYPE_INTERCHAIN_TRANSFER) {
address expressExecutor = _getExpressExecutorAndEmitEvent(commandId, sourceChain, sourceAddress, payloadHash);
_processInterchainTransferPayload(commandId, expressExecutor, originalSourceChain, payload);
} else if (messageType == MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER) {
_processDeployTokenManagerPayload(payload);
} else if (messageType == MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN) {
_processDeployInterchainTokenPayload(payload);
} else if (messageType == MESSAGE_TYPE_LINK_TOKEN) {
_processLinkTokenPayload(payload);
} else {
revert InvalidMessageType(messageType);
}
Expand Down Expand Up @@ -875,9 +919,6 @@ contract InterchainTokenService is

// Get message type of the inner ITS message
messageType = _getMessageType(payload);

// Prevent deploy token manager to be usable on ITS hub
if (messageType == MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER) revert NotSupported();
} else {
// Prevent receiving a direct message from the ITS Hub. This is not supported yet.
if (keccak256(abi.encodePacked(sourceChain)) == ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain();
Expand All @@ -890,23 +931,32 @@ contract InterchainTokenService is
* @notice Deploys a token manager on a destination chain.
* @param tokenId The ID of the token.
* @param destinationChain The chain where the token manager will be deployed.
* @param gasValue The amount of gas to be paid for the transaction.
* @param destinationTokenAddress The address of the token on the destination chain.
* @param tokenManagerType The type of token manager to be deployed.
* @param params Additional parameters for the token manager deployment.
* @param params Additional parameters for the token linking.
* @param gasValue The amount of gas to be paid for the transaction.
*/
function _deployRemoteTokenManager(
function _linkToken(
bytes32 tokenId,
string calldata destinationChain,
uint256 gasValue,
bytes memory destinationTokenAddress,
TokenManagerType tokenManagerType,
bytes calldata params
bytes memory params,
uint256 gasValue
) internal {
// slither-disable-next-line unused-return
deployedTokenManager(tokenId);
bytes memory sourceTokenAddress = registeredTokenAddress(tokenId).toBytes();

emit TokenManagerDeploymentStarted(tokenId, destinationChain, tokenManagerType, params);
emit LinkTokenStarted(tokenId, destinationChain, sourceTokenAddress, destinationTokenAddress, tokenManagerType, params);

bytes memory payload = abi.encode(MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER, tokenId, tokenManagerType, params);
bytes memory payload = abi.encode(
MESSAGE_TYPE_LINK_TOKEN,
tokenId,
tokenManagerType,
sourceTokenAddress,
destinationTokenAddress,
params
);

_callContract(destinationChain, payload, IGatewayCaller.MetadataVersion.CONTRACT_CALL, gasValue);
}
Expand Down Expand Up @@ -948,9 +998,13 @@ contract InterchainTokenService is
* @notice Deploys a token manager.
* @param tokenId The ID of the token.
* @param tokenManagerType The type of the token manager to be deployed.
* @param params Additional parameters for the token manager deployment.
* @param tokenAddress The address of the token to be managed.
* @param operator The operator of the token manager.
*/
function _deployTokenManager(bytes32 tokenId, TokenManagerType tokenManagerType, bytes memory params) internal {
function _deployTokenManager(bytes32 tokenId, TokenManagerType tokenManagerType, address tokenAddress, bytes memory operator) internal {
// TokenManagerProxy params
bytes memory params = abi.encode(operator, tokenAddress);

(bool success, bytes memory returnData) = tokenManagerDeployer.delegatecall(
abi.encodeWithSelector(ITokenManagerDeployer.deployTokenManager.selector, tokenId, tokenManagerType, params)
);
Expand Down
44 changes: 43 additions & 1 deletion contracts/interfaces/IInterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { IMulticall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/in
import { IUpgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IUpgradable.sol';

import { IInterchainTokenService } from './IInterchainTokenService.sol';
import { ITokenManagerType } from './ITokenManagerType.sol';

/**
* @title IInterchainTokenFactory Interface
* @notice This interface defines functions for deploying new interchain tokens and managing their token managers.
*/
interface IInterchainTokenFactory is IUpgradable, IMulticall {
interface IInterchainTokenFactory is ITokenManagerType, IUpgradable, IMulticall {
error ZeroAddress();
error InvalidChainName();
error InvalidMinter(address minter);
Expand Down Expand Up @@ -215,4 +216,45 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall {
string calldata destinationChain,
uint256 gasValue
) external payable returns (bytes32 tokenId);

/**
* @notice Computes the deploy salt for a linked interchain token.
* @param deployer The address of the deployer.
* @param salt The unique salt for deploying the token.
* @return deploySalt The deploy salt for the interchain token.
*/
function linkedTokenDeploySalt(address deployer, bytes32 salt) external view returns (bytes32 deploySalt);

/**
* @notice Computes the ID for a canonical interchain token based on its address.
* @param deployer The address of the deployer.
* @param salt The unique salt for deploying the token.
* @return tokenId The ID of the canonical interchain token.
*/
function linkedTokenId(address deployer, bytes32 salt) external view returns (bytes32 tokenId);

/**
* @notice Register an existing token under a `tokenId` computed from the provided `salt`. A token metadata registration message will also be sent to the ITS Hub.
* @param salt The salt used to derive the tokenId for the custom token registration. The same salt must be used when linking this token on other chains under the same tokenId.
* @param tokenAddress The token address of the token being registered.
* @param tokenManagerType The token manager type used for the token link.
* @param operator The operator of the token manager.
* @param gasValue The cross-chain gas value used to register the token metadata on the ITS Hub.
*/
function registerCustomToken(
bytes32 salt,
address tokenAddress,
TokenManagerType tokenManagerType,
address operator,
uint256 gasValue
) external payable returns (bytes32 tokenId);

function linkToken(
bytes32 salt,
string calldata destinationChain,
bytes calldata destinationTokenAddress,
TokenManagerType tokenManagerType,
bytes calldata linkParams,
uint256 gasValue
) external payable returns (bytes32 tokenId);
}
Loading
Loading