Users can steal balance in InterchainTokenService to pay gas fees for remote chain calls through multicall() in InterchainTokenService.sol.
User can send multiple calls at the same time on InterchainTokenService contract with the help of multicall() on the inherited contract Multicall.sol. However, the current implementations of multicall() allow users to send 0 as msg.value but steal balance from InterchainTokenService contract to pay for cross-chain gas fees.
In multicall(), there is no check on (1) whether there is cross-chain requests in data (2) if there is cross-chain requests in data, whether there is any msg.value or sufficient msg.value sent with the transaction to cover cross-chain gas fees.
//Multicall.sol
function multicall(bytes[] calldata data) public payable returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; ++i) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) {
revert(string(result));
}
results[i] = result;
}
}
As seen above, delegateCall() is executed without passing msg.value. And there is also no check on whether there is any native tokens sent with the transaction by users either. The transaction relies on users’ honesty to send ‘msg.value’ to cover any cross-chain gas fees that are required, if there is at least one cross-chain function to be called in multicall().
As an example, suppose deployRemoteCustomTokenManager() is part of the data passed to multicall(). deployRemoteCustomTokenManager() will directly use gasValue arg which is part of calldata user specified to pay for gas. In _deployRemoteTokenManager(), gasValue is passed to _callContract() to pay cross-chain gas to GasService contract. To this point, there is no check on whether there is ‘msg.value’ sent by the user and the gasValue paid could be from the InterchainTokenService contract balance.
//InterchainTokenService.sol
function deployRemoteCustomTokenManager(
bytes32 salt,
string calldata destinationChain,
TokenManagerType tokenManagerType,
bytes calldata params,
uint256 gasValue
) external payable notPaused returns (bytes32 tokenId) {
address deployer_ = msg.sender;
tokenId = getCustomTokenId(deployer_, salt);
_deployRemoteTokenManager(tokenId, destinationChain, gasValue, tokenManagerType, params);
emit CustomTokenIdClaimed(tokenId, deployer_, salt);
}
//InterchainTokenService.sol
function _deployRemoteTokenManager(
bytes32 tokenId,
string calldata destinationChain,
uint256 gasValue,
TokenManagerType tokenManagerType,
bytes memory params
) internal {
bytes memory payload = abi.encode(SELECTOR_DEPLOY_TOKEN_MANAGER, tokenId, tokenManagerType, params);
_callContract(destinationChain, payload, gasValue, msg.sender);
emit RemoteTokenManagerDeploymentInitialized(tokenId, destinationChain, gasValue, tokenManagerType, params);
//InterchainTokenService.sol - _callContract()
...
if (gasValue > 0) {
gasService.payNativeGasForContractCall{ value: gasValue }(
address(this),
destinationChain,
destinationAddress,
payload,
refundTo
);
}
...
As seen above, user can finish the cross-chain call through multicall() by using balance from the contract. And InterchainTokenService is designed to receive native tokens transfers independently, because the BaseProxy.sol which InterchainTokenService inherits have receive() to receive native token transfers.
Manual review
(1) In multicall(), for each data[i] passed by user, verify if it contains a cross-chain function signature.
(2) If it contains a cross-chain function signature, verify encoded gasValue inside data[i] and make sure the added total gasValue is not more than msg.value passed by the user
call/delegatecall
The text was updated successfully, but these errors were encountered:
All reactions