diff --git a/DESIGN.md b/DESIGN.md index acfb3013..d0d87f06 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -47,11 +47,11 @@ The messages going through the Axelar Network between `InterchainTokenServices` ### `INTERCHAIN_TRANSFER` -This message has the following data encoded and should only be sent after the proper tokens have been procured by the service. It should result in the proper funds being transferred to the user at the destionation chain. +This message is used to transfer tokens between chains. The tokens are handled appropriately on the source chain (lock/burn etc.), and then this message is sent to the ITS contract on the remote chain which sends the tokens to the destination address. | Name | Type | Description | | ------------------- | --------- | --------------------------------------------------------------------------------------------------------- | -| selector | `uint256` | Will always have a value of `0` | +| messageType | `uint256` | Will always have a value of `0` | | tokenId | `bytes32` | The `interchainTokenId` of the token being transferred | | source address | `bytes` | The address of the sender, encoded as bytes to account for different chain architectures | | destination address | `bytes` | The address of the recipient, encoded as bytes as well | @@ -60,11 +60,11 @@ This message has the following data encoded and should only be sent after the pr ### `DEPLOY_INTERCHAIN_TOKEN` -This message has the following data encoded and should only be sent after the `interchainTokenId` has been properly generated (a user should not be able to claim just any `interchainTokenId`) +This message is used to deploy an `InterchainToken` on a remote chain, that corresponds to the `tokenId` of a local ERC-20 token registered in ITS. This allows a user to deploy tokens to remote chains from a single source chain, instead of having to make a tx on each chain. It also allows the implementation on each chain to be flexible (e.g. `tokenId` derivation can be different on remote chains). | Name | Type | Description | | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------- | -| selector | `uint256` | Will always have a value of `1` | +| messageType | `uint256` | Will always have a value of `1` | | tokenId | `bytes32` | The `interchainTokenId` of the token being deployed | | name | `string` | The name for the token | | symbol | `string` | The symbol for the token | @@ -73,11 +73,31 @@ This message has the following data encoded and should only be sent after the `i ### `DEPLOY_TOKEN_MANAGER` -This message has the following data encoded and should only be sent after the proper tokens have been procured by the service. It should result in the proper funds being transferred to the user at the destination chain. +This message is used to deploy a token manager on a remote chain, that corresponds to a local token manager. This is useful to link custom tokens via ITS with the same tokenId. | Name | Type | Description | | ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| selector | `uint256` | Will always have a value of `2` | +| messageType | `uint256` | Will always have a value of `2` | | tokenId | `bytes32` | The `interchainTokenId` of the token being deployed | | token manager type | `uint256` | The type of the token manager, look at the [code](./contracts/interfaces/ITokenManagerType.sol) for details on EVM, but it could be different for different architectures | | params | `bytes` | The parameters for the token manager deployments, look [here](./contracts/token-manager/TokenManager.sol#L179) for details on EVM chain parameters | + +### `SEND_TO_HUB` + +This message is used to route an ITS message via the ITS Hub. The ITS Hub applies certain security checks, and then routes it to the true destination chain. This mode is enabled if the trusted address corresponding to the destination chain is set to the ITS Hub identifier. + +| Name | Type | Description | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| messageType | `uint256` | Will always have a value of `2` | +| destinationChain | `string` | The true destination chain for the ITS call | +| payload | `bytes` | The actual ITS message that's being routed through ITS Hub + +### `RECEIVE_FROM_HUB` + +This message is used to receive an ITS message from the ITS Hub. The ITS Hub applies certain security checks, and then routes it to the ITS contract. The message is accepted if the trusted address corresponding to the original source chain is set to the ITS Hub identifier. + +| Name | Type | Description | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| messageType | `uint256` | Will always have a value of `2` | +| sourceChain | `string` | The original source chain for the ITS call | +| payload | `bytes` | The actual ITS message that's being routed through ITS Hub diff --git a/README.md b/README.md index d721d26c..de33c116 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,31 @@ To build and run tests, do the following npm ci npm run build +``` + +## Test +```bash npm run test ``` -## Test Coverage Report +### Gas Report + +Generate gas report + +```bash +REPORT_GAS=true npm run test +``` + +### Coverage + +Generate test coverage report + +```bash +COVERAGE=true npm run test +``` -For the most recent test coverage report of the `main` branch, please visit the following [page](https://axelarnetwork.github.io/interchain-token-service/). +Test coverage reports of the `main` branch can also be found on [Codecov](https://app.codecov.io/gh/axelarnetwork/interchain-token-service). ## Deployment Guide diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index 77b4558e..8fe0db1f 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -88,16 +88,18 @@ contract InterchainTokenService is uint32 internal constant LATEST_METADATA_VERSION = 1; /** - * @dev Chain name for Axelar. This is used for routing ITS calls via ITS hub on Axelar. + * @dev Chain name where ITS Hub exists. This is used for routing ITS calls via ITS hub. + * This is set as a constant, since the ITS Hub will exist on Axelar. */ - string internal constant AXELAR_CHAIN_NAME = 'Axelarnet'; - bytes32 internal constant AXELAR_CHAIN_NAME_HASH = keccak256(abi.encodePacked(AXELAR_CHAIN_NAME)); + string internal constant ITS_HUB_CHAIN_NAME = 'Axelarnet'; + bytes32 internal constant ITS_HUB_CHAIN_NAME_HASH = keccak256(abi.encodePacked(ITS_HUB_CHAIN_NAME)); /** - * @dev Special trusted address value that indicates that the ITS call - * for that destination chain should be routed via the ITS hub. + * @dev Special identifier that the trusted address for a chain should be set to, which indicates if the ITS call + * for that chain should be routed via the ITS hub. */ - bytes32 internal constant ITS_HUB_TRUSTED_ADDRESS_HASH = keccak256('hub'); + string internal constant ITS_HUB_ROUTING_IDENTIFIER = 'hub'; + bytes32 internal constant ITS_HUB_ROUTING_IDENTIFIER_HASH = keccak256(abi.encodePacked(ITS_HUB_ROUTING_IDENTIFIER)); /** * @notice Constructor for the Interchain Token Service. @@ -805,6 +807,8 @@ contract InterchainTokenService is /** * @notice Calls a contract on a specific destination chain with the given payload + * @dev This method also determines whether the ITS call should be routed via the ITS Hub. + * If the `trustedAddress(destinationChain) == 'hub'`, then the call is wrapped and routed to the ITS Hub destination. * @param destinationChain The target chain where the contract will be called. * @param payload The data payload for the transaction. * @param gasValue The amount of gas to be paid for the transaction. @@ -815,17 +819,9 @@ contract InterchainTokenService is IGatewayCaller.MetadataVersion metadataVersion, uint256 gasValue ) internal { - string memory destinationAddress = trustedAddress(destinationChain); - - // Check if the ITS call should be routed via ITS hub for this destination chain - if (keccak256(abi.encodePacked(destinationAddress)) == ITS_HUB_TRUSTED_ADDRESS_HASH) { - // Wrap ITS message in an ITS Hub message - payload = abi.encode(MESSAGE_TYPE_SEND_TO_HUB, destinationChain, payload); - destinationChain = AXELAR_CHAIN_NAME; - destinationAddress = trustedAddress(AXELAR_CHAIN_NAME); - } + string memory destinationAddress; - if (bytes(destinationAddress).length == 0) revert UntrustedChain(); + (destinationChain, destinationAddress, payload) = _getCallParams(destinationChain, payload); (bool success, bytes memory returnData) = gatewayCaller.delegatecall( abi.encodeWithSelector( @@ -855,17 +851,9 @@ contract InterchainTokenService is IGatewayCaller.MetadataVersion metadataVersion, uint256 gasValue ) internal { - string memory destinationAddress = trustedAddress(destinationChain); + string memory destinationAddress; - // Check if the ITS call should be routed via ITS hub for this destination chain - if (keccak256(abi.encodePacked(destinationAddress)) == ITS_HUB_TRUSTED_ADDRESS_HASH) { - // Wrap ITS message in an ITS Hub message - payload = abi.encode(MESSAGE_TYPE_SEND_TO_HUB, destinationChain, payload); - destinationChain = AXELAR_CHAIN_NAME; - destinationAddress = trustedAddress(AXELAR_CHAIN_NAME); - } - - if (bytes(destinationAddress).length == 0) revert UntrustedChain(); + (destinationChain, destinationAddress, payload) = _getCallParams(destinationChain, payload); (bool success, bytes memory returnData) = gatewayCaller.delegatecall( abi.encodeWithSelector( @@ -883,6 +871,32 @@ contract InterchainTokenService is if (!success) revert GatewayCallFailed(returnData); } + /** + * @dev Get the params for the cross-chain message, taking routing via ITS Hub into account. + */ + function _getCallParams( + string memory destinationChain, + bytes memory payload + ) internal view returns (string memory, string memory, bytes memory) { + string memory destinationAddress = trustedAddress(destinationChain); + + // Prevent sending directly to the ITS Hub chain. This is not supported yet, so fail early to prevent the user from having their funds stuck. + if (keccak256(abi.encodePacked(destinationChain)) == ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain(); + + // 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) { + // Wrap ITS message in an ITS Hub message + payload = abi.encode(MESSAGE_TYPE_SEND_TO_HUB, destinationChain, payload); + destinationChain = ITS_HUB_CHAIN_NAME; + destinationAddress = trustedAddress(ITS_HUB_CHAIN_NAME); + } + + // Check whether no trusted address was set for the destination chain + if (bytes(destinationAddress).length == 0) revert UntrustedChain(); + + return (destinationChain, destinationAddress, payload); + } + function _execute( bytes32 commandId, string calldata sourceChain, @@ -890,20 +904,9 @@ contract InterchainTokenService is bytes memory payload, bytes32 payloadHash ) internal { - // Read the first 32 bytes of the payload to determine the message type - uint256 messageType = _getMessageType(payload); - // True source chain, this is overridden if the ITS call is coming via the ITS hub - string memory originalSourceChain = sourceChain; - - // Only allow routed call wrapping once - if (messageType == MESSAGE_TYPE_RECEIVE_FROM_HUB) { - if (keccak256(abi.encodePacked(sourceChain)) != AXELAR_CHAIN_NAME_HASH) revert UntrustedChain(); - - (, originalSourceChain, payload) = abi.decode(payload, (uint256, string, bytes)); - - // Get message type of the inner ITS message - messageType = _getMessageType(payload); - } + uint256 messageType; + string memory originalSourceChain; + (messageType, originalSourceChain, payload) = _getExecuteParams(sourceChain, payload); if (messageType == MESSAGE_TYPE_INTERCHAIN_TRANSFER) { address expressExecutor = _getExpressExecutorAndEmitEvent(commandId, sourceChain, sourceAddress, payloadHash); @@ -930,20 +933,9 @@ contract InterchainTokenService is if (!gateway.validateContractCallAndMint(commandId, sourceChain, sourceAddress, payloadHash, tokenSymbol, amount)) revert NotApprovedByGateway(); - // Read the first 32 bytes of the payload to determine the message type - uint256 messageType = _getMessageType(payload); - // True source chain, this is overridden if the ITS call is coming via the ITS hub - string memory originalSourceChain = sourceChain; - - // Unwrap ITS message if coming from ITS hub - if (messageType == MESSAGE_TYPE_RECEIVE_FROM_HUB) { - if (keccak256(abi.encodePacked(sourceChain)) != AXELAR_CHAIN_NAME_HASH) revert UntrustedChain(); - - (, originalSourceChain, payload) = abi.decode(payload, (uint256, string, bytes)); - - // Get message type of the inner ITS message - messageType = _getMessageType(payload); - } + uint256 messageType; + string memory originalSourceChain; + (messageType, originalSourceChain, payload) = _getExecuteParams(sourceChain, payload); if (messageType != MESSAGE_TYPE_INTERCHAIN_TRANSFER) { revert InvalidMessageType(messageType); @@ -966,6 +958,38 @@ contract InterchainTokenService is } } + /** + * @dev Return the parameters for the execute call, taking routing via ITS Hub into account. + */ + function _getExecuteParams( + string calldata sourceChain, + bytes memory payload + ) internal view returns (uint256, string memory, bytes memory) { + // Read the first 32 bytes of the payload to determine the message type + uint256 messageType = _getMessageType(payload); + + // True source chain, this is overridden if the ITS call is coming via the ITS hub + string memory originalSourceChain = sourceChain; + + // Unwrap ITS message if coming from ITS hub + if (messageType == MESSAGE_TYPE_RECEIVE_FROM_HUB) { + if (keccak256(abi.encodePacked(sourceChain)) != ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain(); + + (, originalSourceChain, payload) = abi.decode(payload, (uint256, string, bytes)); + + // Check whether the original source chain is expected to be routed via the ITS Hub + if (trustedAddressHash(originalSourceChain) != ITS_HUB_ROUTING_IDENTIFIER_HASH) revert UntrustedChain(); + + // Get message type of the inner ITS message + messageType = _getMessageType(payload); + } 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(); + } + + return (messageType, originalSourceChain, payload); + } + /** * @notice Deploys a token manager on a destination chain. * @param tokenId The ID of the token. @@ -979,7 +1003,7 @@ contract InterchainTokenService is string calldata destinationChain, uint256 gasValue, TokenManagerType tokenManagerType, - bytes memory params + bytes calldata params ) internal { // slither-disable-next-line unused-return validTokenManagerAddress(tokenId); diff --git a/contracts/interfaces/IGatewayCaller.sol b/contracts/interfaces/IGatewayCaller.sol index 5be02f2e..8093720d 100644 --- a/contracts/interfaces/IGatewayCaller.sol +++ b/contracts/interfaces/IGatewayCaller.sol @@ -26,7 +26,7 @@ interface IGatewayCaller { * @param destinationAddress The address of the contract to be called on the destination chain * @param payload The data payload for the transaction * @param metadataVersion The version of metadata to be used - * @param gasValue The amount of gas to be paid for the transaction + * @param gasValue The amount of gas to be paid for the cross-chain message. If this is 0, then gas payment is skipped. `msg.value` must be at least gasValue. */ function callContract( string calldata destinationChain, @@ -44,7 +44,7 @@ interface IGatewayCaller { * @param symbol The symbol of the token to be sent * @param amount The amount of tokens to be sent * @param metadataVersion The version of metadata to be used - * @param gasValue The amount of gas to be paid for the transaction + * @param gasValue The amount of gas to be paid for the cross-chain message. If this is 0, then gas payment is skipped. `msg.value` must be at least gasValue. */ function callContractWithToken( string calldata destinationChain, diff --git a/contracts/utils/GatewayCaller.sol b/contracts/utils/GatewayCaller.sol index e7c02dae..f739facd 100644 --- a/contracts/utils/GatewayCaller.sol +++ b/contracts/utils/GatewayCaller.sol @@ -8,7 +8,7 @@ import { IGatewayCaller } from '../interfaces/IGatewayCaller.sol'; /** * @title GatewayCaller contract - * @dev This contract is used to handle cross-chain calls via the Axelar gateway + * @dev This contract is used to handle cross-chain ITS calls via the Axelar gateway. */ contract GatewayCaller is IGatewayCaller { IAxelarGateway public immutable gateway; @@ -30,7 +30,7 @@ contract GatewayCaller is IGatewayCaller { * @param destinationAddress The address of the contract to be called on the destination chain * @param payload The data payload for the transaction * @param metadataVersion The version of metadata to be used - * @param gasValue The amount of gas to be paid for the transaction + * @param gasValue The amount of gas to be paid for the cross-chain message. If this is 0, then gas payment is skipped. `msg.value` must be at least gasValue. */ function callContract( string calldata destinationChain, @@ -47,6 +47,7 @@ contract GatewayCaller is IGatewayCaller { destinationChain, destinationAddress, payload, + // solhint-disable-next-line avoid-tx-origin tx.origin ); } else if (metadataVersion == MetadataVersion.EXPRESS_CALL) { @@ -56,6 +57,7 @@ contract GatewayCaller is IGatewayCaller { destinationChain, destinationAddress, payload, + // solhint-disable-next-line avoid-tx-origin tx.origin ); } else { @@ -74,7 +76,7 @@ contract GatewayCaller is IGatewayCaller { * @param symbol The symbol of the token to be sent * @param amount The amount of tokens to be sent * @param metadataVersion The version of metadata to be used - * @param gasValue The amount of gas to be paid for the transaction + * @param gasValue The amount of gas to be paid for the cross-chain message. If this is 0, then gas payment is skipped. `msg.value` must be at least gasValue. */ function callContractWithToken( string calldata destinationChain, @@ -95,6 +97,7 @@ contract GatewayCaller is IGatewayCaller { payload, symbol, amount, + // solhint-disable-next-line avoid-tx-origin tx.origin ); } else if (metadataVersion == MetadataVersion.EXPRESS_CALL) { @@ -106,6 +109,7 @@ contract GatewayCaller is IGatewayCaller { payload, symbol, amount, + // solhint-disable-next-line avoid-tx-origin tx.origin ); } else {