Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5075a45
add hedera libraries and interfaces
rista404 Jun 4, 2025
386f43a
create hts token from token manager + inherit Minter contract
rista404 Jun 6, 2025
3e6fe1f
add token creation price calculation + whbar contract address
rista404 Jun 10, 2025
374103f
pay for token creation, specify price
rista404 Jun 10, 2025
7964a79
deploy hts lib for testing
rista404 Jun 11, 2025
414c13b
rm unused `transmitInterchainTransfer` ITS method
rista404 Jun 11, 2025
ab88ce0
revert on initial supply token deployments in factory
rista404 Jun 11, 2025
a8b20e3
wip tests
rista404 Jun 11, 2025
707e7c1
add test checks for token deployment events
rista404 Jun 13, 2025
5c6cb05
save isHtsToken flag in TokenManagerProxy
rista404 Jun 13, 2025
b133129
add deploy and fund scripts for whbar
rista404 Jun 13, 2025
9face17
get token metadata for hts tokens in factory
rista404 Jun 23, 2025
a11e2fb
approve service to int64.max for hts tokens
rista404 Jun 24, 2025
7dafa59
don't transfer mintership to TM post-deployment
rista404 Jun 24, 2025
98985c2
associate TM only for HTS tokens and LOCK_UNLOCK
rista404 Jun 24, 2025
a959b48
update deploy scripts + add token associate script
rista404 Jun 26, 2025
f7c81fb
set `tokenCreationPrice` during ITS setup
rista404 Jul 1, 2025
7f25371
optionally include a Hedera testnet account in hardhat config
rista404 Jul 1, 2025
f477f74
remove `migrateInterchainToken` support
rista404 Jul 1, 2025
64037cc
update hedera/README.md with the new design
rista404 Jul 3, 2025
e1dbb8e
remove impossible < 0 check
rista404 Jul 3, 2025
ab13603
fix its unit tests suite + improve docs
rista404 Jul 18, 2025
2e83bc7
add note about WHBAR balance of ITS
rista404 Jul 18, 2025
da7fc66
rm cross-invokation deployment context + review fixes
rista404 Jul 18, 2025
b121dd2
add deploy interchain token diagram
rista404 Jul 18, 2025
a64202d
add todo note about auto associations
rista404 Jul 18, 2025
2ecb8e0
add auto association explanation to the docs
rista404 Jul 21, 2025
ffd885d
add register and mint diagram to docs
rista404 Jul 21, 2025
c311f20
add note on test suite time
rista404 Jul 21, 2025
0e48ce6
don't add its as a minter in TM
rista404 Jul 21, 2025
e67e868
rm unused `centsToTinybars` from HTS lib
rista404 Jul 21, 2025
7fec126
explain why we add one tinybar to token creation price
rista404 Jul 21, 2025
123e921
fix deploy TM proxy invalid params test
rista404 Jul 21, 2025
35719b8
fix ITF tests + revert on empty name and symbol
rista404 Jul 21, 2025
b97bb76
skip ERC20 tests
rista404 Jul 21, 2025
6bdd53e
fix canonical interchain token full flow tests
rista404 Jul 21, 2025
84130d1
fix some full flow tests + IHRC719 interface for executable + docs
rista404 Jul 22, 2025
2198e27
fix full flow and upgrade tests
rista404 Jul 23, 2025
acfaf20
require approval of WHBAR for local deployment
rista404 Aug 18, 2025
1088024
make whbar address immutable
rista404 Aug 20, 2025
01ac902
revert removal of `transmitInterchainTransfer` method
rista404 Aug 21, 2025
b04a129
mention non-deterministic token addresses in the overview
rista404 Aug 25, 2025
70c2fed
docs: add info about the token creation price
rista404 Sep 10, 2025
4a934f6
docs: use h2 for sections
rista404 Sep 10, 2025
cdc6e99
docs: add lines between diagrams
rista404 Sep 10, 2025
73bb1b7
docs: add hedera links
rista404 Sep 10, 2025
5666515
docs: add note about canonical ITS
rista404 Sep 10, 2025
1806536
docs: link to tree view instead of commit
rista404 Sep 10, 2025
c6fda17
fix: support registration of native tokens with lower max supply
rista404 Sep 25, 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
81 changes: 44 additions & 37 deletions contracts/InterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { IInterchainTokenFactory } from './interfaces/IInterchainTokenFactory.so
import { ITokenManager } from './interfaces/ITokenManager.sol';
import { IInterchainToken } from './interfaces/IInterchainToken.sol';
import { IERC20Named } from './interfaces/IERC20Named.sol';
import { IMinter } from './interfaces/IMinter.sol';

import { HTS, IHederaTokenService } from './hedera/HTS.sol';

/**
* @title InterchainTokenFactory
Expand Down Expand Up @@ -144,9 +147,14 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl
string memory currentChain = '';
uint256 gasValue = 0;

if (initialSupply > 0) {
minterBytes = address(this).toBytes();
} else if (minter != address(0)) {
// HTS tokens must previously be associated with an account
// to be able to send tokens to it. Since a new token will be created
// it's not possible to send it right away.
if (initialSupply != 0) {
revert HTS.InitialSupplyUnsupported();
}

if (minter != address(0)) {
if (minter == address(interchainTokenService)) revert InvalidMinter(minter);

minterBytes = minter.toBytes();
Expand All @@ -155,21 +163,6 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl
}

tokenId = _deployInterchainToken(deploySalt, currentChain, name, symbol, decimals, minterBytes, gasValue);

if (initialSupply > 0) {
IInterchainToken token = IInterchainToken(interchainTokenService.registeredTokenAddress(tokenId));
ITokenManager tokenManager = ITokenManager(interchainTokenService.deployedTokenManager(tokenId));

token.mint(sender, initialSupply);

token.transferMintership(minter);
tokenManager.removeFlowLimiter(address(this));

// If minter == address(0), we still set it as a flow limiter for consistency with the remote token manager.
tokenManager.addFlowLimiter(minter);

tokenManager.transferOperatorship(minter);
}
}

/**
Expand Down Expand Up @@ -329,8 +322,8 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl
*/
function _checkTokenMinter(bytes32 tokenId, address minter) internal view {
// Ensure that the minter is registered for the token on the current chain
IInterchainToken token = IInterchainToken(interchainTokenService.registeredTokenAddress(tokenId));
if (!token.isMinter(minter)) revert NotMinter(minter);
ITokenManager tokenManager = ITokenManager(interchainTokenService.deployedTokenManager(tokenId));
if (!tokenManager.isMinter(minter)) revert NotMinter(minter);

// Sanity check to prevent accidental use of the current ITS address as the token minter
if (minter == address(interchainTokenService)) revert InvalidMinter(minter);
Expand Down Expand Up @@ -412,31 +405,45 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl
}

/**
* @notice Retrieves the metadata of an ERC20 token. Reverts with `NotToken` error if metadata is not available.
* @notice Retrieves the metadata of an ERC20 or HTS token. Reverts with `NotToken` error if metadata is not available.
* @notice Returns HTS.InvalidtokenDecimals() if the decimals are not supported.
* @param tokenAddress The address of the token.
* @return name The name of the token.
* @return symbol The symbol of the token.
* @return decimals The number of decimals for the token.
*/
function _getTokenMetadata(address tokenAddress) internal view returns (string memory name, string memory symbol, uint8 decimals) {
IERC20Named token = IERC20Named(tokenAddress);
function _getTokenMetadata(address tokenAddress) internal returns (string memory name, string memory symbol, uint8 decimals) {
bool isHTSToken = HTS.isToken(tokenAddress);

if (isHTSToken) {
IHederaTokenService.FungibleTokenInfo memory fTokenInfo = HTS.getFungibleTokenInfo(tokenAddress);
name = fTokenInfo.tokenInfo.token.name;
symbol = fTokenInfo.tokenInfo.token.symbol;
int32 htsDecimals = fTokenInfo.decimals;
if (htsDecimals > int32(uint32(type(uint8).max))) {
revert HTS.InvalidTokenDecimals();
}
decimals = uint8(uint32(htsDecimals));
} else {
IERC20Named token = IERC20Named(tokenAddress);

try token.name() returns (string memory name_) {
name = name_;
} catch {
revert NotToken(tokenAddress);
}
try token.name() returns (string memory name_) {
name = name_;
} catch {
revert NotToken(tokenAddress);
}

try token.symbol() returns (string memory symbol_) {
symbol = symbol_;
} catch {
revert NotToken(tokenAddress);
}
try token.symbol() returns (string memory symbol_) {
symbol = symbol_;
} catch {
revert NotToken(tokenAddress);
}

try token.decimals() returns (uint8 decimals_) {
decimals = decimals_;
} catch {
revert NotToken(tokenAddress);
try token.decimals() returns (uint8 decimals_) {
decimals = decimals_;
} catch {
revert NotToken(tokenAddress);
}
}
}

Expand Down
176 changes: 80 additions & 96 deletions contracts/InterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ import { IMinter } from './interfaces/IMinter.sol';
import { Create3AddressFixed } from './utils/Create3AddressFixed.sol';
import { Operator } from './utils/Operator.sol';
import { ChainTracker } from './utils/ChainTracker.sol';
import { TokenCreationPricing } from './utils/TokenCreationPricing.sol';
import { ItsHubAddressTracker } from './utils/ItsHubAddressTracker.sol';

import { IWHBAR } from './hedera/IWHBAR.sol';

/**
* @title The Interchain Token Service
* @notice This contract is responsible for facilitating interchain token transfers.
Expand All @@ -42,6 +45,7 @@ contract InterchainTokenService is
ExpressExecutorTracker,
InterchainAddressTracker,
ChainTracker,
TokenCreationPricing,
ItsHubAddressTracker,
IInterchainTokenService
{
Expand Down Expand Up @@ -236,17 +240,6 @@ contract InterchainTokenService is
tokenAddress = ITokenManager(deployedTokenManager(tokenId)).tokenAddress();
}

/**
* @notice Returns the address of the interchain token associated with the given tokenId.
* @dev The token does not need to exist.
* @param tokenId The tokenId of the interchain token.
* @return tokenAddress The address of the interchain token.
*/
function interchainTokenAddress(bytes32 tokenId) public view returns (address tokenAddress) {
tokenId = _getInterchainTokenSalt(tokenId);
tokenAddress = _create3Address(tokenId);
}

/**
* @notice Calculates the tokenId that would correspond to a link for a given deployer with a specified salt.
* @param sender The address of the TokenManager deployer.
Expand Down Expand Up @@ -404,9 +397,7 @@ contract InterchainTokenService is
emit InterchainTokenIdClaimed(tokenId, deployer, salt);

if (bytes(destinationChain).length == 0) {
address tokenAddress = _deployInterchainToken(tokenId, minter, name, symbol, decimals);

_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, tokenAddress, minter);
_deployTokenManagerWithInterchainToken(tokenId, name, symbol, decimals, minter);
} else {
if (chainNameHash == keccak256(bytes(destinationChain))) revert CannotDeployRemotelyToSelf();

Expand Down Expand Up @@ -575,35 +566,6 @@ contract InterchainTokenService is
_interchainTransfer(tokenId, destinationChain, destinationAddress, amount, data, gasValue);
}

/******************\
Copy link

Choose a reason for hiding this comment

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

Custom tokens should still be able to use this so probably do not remove it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted the removal.

TOKEN ONLY FUNCTIONS
\******************/

/**
* @notice Transmit an interchain transfer for the given tokenId.
* @dev Only callable by a token registered under a tokenId.
* @param tokenId The tokenId of the token (which must be the msg.sender).
* @param sourceAddress The address where the token is coming from.
* @param destinationChain The name of the chain to send tokens to.
* @param destinationAddress The destinationAddress for the interchainTransfer.
* @param amount The amount of token to give.
* @param metadata Optional metadata for the call for additional effects (such as calling a destination contract).
*/
function transmitInterchainTransfer(
bytes32 tokenId,
address sourceAddress,
string calldata destinationChain,
bytes memory destinationAddress,
uint256 amount,
bytes calldata metadata
) external payable whenNotPaused {
amount = _takeToken(tokenId, sourceAddress, amount, true);

bytes memory data = _decodeMetadata(metadata);

_transmitInterchainTransfer(tokenId, sourceAddress, destinationChain, destinationAddress, amount, data, msg.value);
}

/*************\
OWNER FUNCTIONS
\*************/
Expand Down Expand Up @@ -639,6 +601,22 @@ contract InterchainTokenService is
_removeTrustedChain(chainName);
}

/**
* @notice Used to set the token creation price in tinycents.
* @param price The new token creation price in tinycents.
*/
function setTokenCreationPrice(uint256 price) external onlyOperatorOrOwner {
Copy link

Choose a reason for hiding this comment

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

Q: Why cast to u8 in onlyOperatorOrOwner

Copy link
Member Author

Choose a reason for hiding this comment

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

if you ask about the hasRole? the second argument of role is a uint8. see here: https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/00682b6c3db0cc922ec0c4ea3791852c93d7ae31/contracts/utils/RolesBase.sol#L51

I'll leave the convo open in case you meant something else.

_setTokenCreationPrice(price);
}

/**
* @notice Used to set the WHBAR contract address.
* @param whbarAddress_ The new WHBAR contract address.
*/
function setWhbarAddress(address whbarAddress_) external onlyOperatorOrOwner {
Copy link

Choose a reason for hiding this comment

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

this should probably be immutable since it should never change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to not allow changing, it's set during the deployment of the ITS implementation.

_setWhbarAddress(whbarAddress_);
}

/**
* @notice Allows the owner to pause/unpause the token service.
* @param paused Boolean value representing whether to pause or unpause.
Expand All @@ -651,22 +629,15 @@ 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
\****************/

function _setup(bytes calldata params) internal override {
(address operator, string memory chainName_, string[] memory trustedChainNames) = abi.decode(params, (address, string, string[]));
(address operator, string memory chainName_, string[] memory trustedChainNames, uint256 _tokenCreationPrice) = abi.decode(
params,
(address, string, string[], uint256)
);
if (operator == address(0)) revert ZeroAddress();
if (bytes(chainName_).length == 0 || keccak256(bytes(chainName_)) != chainNameHash) revert InvalidChainName();

Expand All @@ -682,6 +653,8 @@ contract InterchainTokenService is
_removeTrustedAddress(trustedChainName);
}
}

_setTokenCreationPrice(_tokenCreationPrice);
}

/**
Expand Down Expand Up @@ -767,11 +740,8 @@ contract InterchainTokenService is
payload,
(uint256, bytes32, string, string, uint8, bytes)
);
address tokenAddress;

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

_deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, tokenAddress, minterBytes);
_deployTokenManagerWithInterchainToken(tokenId, name, symbol, decimals, minterBytes);
}

/**
Expand Down Expand Up @@ -970,6 +940,58 @@ contract InterchainTokenService is
_routeMessage(destinationChain, payload, gasValue);
}

/**
* @notice Deploys a token manager.
* @param tokenId The ID of the token.
* @param operator The operator of the token manager.
*/
function _deployTokenManagerWithInterchainToken(
bytes32 tokenId,
string memory name,
string memory symbol,
uint8 decimals,
bytes memory operator
) internal {
// Price in tinybars
uint256 tokenCreatePrice = _tokenCreationPriceTinybars();

// TokenManagerProxy deploy params
bytes memory params = abi.encode(operator, name, symbol, decimals, tokenCreatePrice);

TokenManagerType tokenManagerType = TokenManagerType.NATIVE_INTERCHAIN_TOKEN;

// Get the pre-determined token manager address
address tokenManager_ = tokenManagerAddress(tokenId);

// Approve the token manager deployer to spend the token creation price
IWHBAR(whbarAddress()).approve(tokenManager_, tokenCreatePrice);

(bool success, bytes memory returnData) = tokenManagerDeployer.delegatecall(
Copy link

Choose a reason for hiding this comment

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

check if can delegatecall

abi.encodeWithSelector(ITokenManagerDeployer.deployTokenManager.selector, tokenId, tokenManagerType, params)
);
if (!success) revert TokenManagerDeploymentFailed(returnData);

assembly {
tokenManager_ := mload(add(returnData, 0x20))
}

(success, returnData) = tokenHandler.delegatecall(
Copy link

Choose a reason for hiding this comment

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

check if can delegatecall

abi.encodeWithSelector(ITokenHandler.postTokenManagerDeploy.selector, tokenManagerType, tokenManager_)
);
if (!success) revert PostDeployFailed(returnData);

// Get the token address from the deployed token manager
address tokenAddress = ITokenManager(tokenManager_).tokenAddress();

address minter;
if (bytes(operator).length != 0) minter = operator.toAddress();

// slither-disable-next-line reentrancy-events
emit InterchainTokenDeployed(tokenId, tokenAddress, minter, name, symbol, decimals);
// slither-disable-next-line reentrancy-events
emit TokenManagerDeployed(tokenId, tokenManager_, tokenManagerType, params);
}

/**
* @notice Deploys a token manager.
* @param tokenId The ID of the token.
Expand Down Expand Up @@ -1009,44 +1031,6 @@ contract InterchainTokenService is
salt = keccak256(abi.encode(PREFIX_INTERCHAIN_TOKEN_SALT, tokenId));
}

/**
* @notice Deploys an interchain token.
* @param tokenId The ID of the token.
* @param minterBytes The minter address for the token.
* @param name The name of the token.
* @param symbol The symbol of the token.
* @param decimals The number of decimals of the token.
*/
function _deployInterchainToken(
bytes32 tokenId,
bytes memory minterBytes,
string memory name,
string memory symbol,
uint8 decimals
) internal returns (address tokenAddress) {
if (bytes(name).length == 0) revert EmptyTokenName();
if (bytes(symbol).length == 0) revert EmptyTokenSymbol();

bytes32 salt = _getInterchainTokenSalt(tokenId);

address minter;
if (bytes(minterBytes).length != 0) minter = minterBytes.toAddress();

(bool success, bytes memory returnData) = interchainTokenDeployer.delegatecall(
abi.encodeWithSelector(IInterchainTokenDeployer.deployInterchainToken.selector, salt, tokenId, minter, name, symbol, decimals)
);
if (!success) {
revert InterchainTokenDeploymentFailed(returnData);
}

assembly {
tokenAddress := mload(add(returnData, 0x20))
}

// slither-disable-next-line reentrancy-events
emit InterchainTokenDeployed(tokenId, tokenAddress, minter, name, symbol, decimals);
}

/**
* @notice Decodes the metadata into a version number and data bytes.
* @dev The function expects the metadata to have the version in the first 4 bytes, followed by the actual data.
Expand Down
Loading
Loading