Lucene search

K
code423n4Code4renaCODE423N4:2023-07-AXELAR-FINDINGS-ISSUES-390
HistoryJul 21, 2023 - 12:00 a.m.

Users can abuse multicall feature on InterchainTokenService to steal contract funds

2023-07-2100:00:00
Code4rena
github.com
3
exploit
multicall
cross-chain

7 High

AI Score

Confidence

Low

Lines of code

Vulnerability details

Impact

Users can steal balance in InterchainTokenService to pay gas fees for remote chain calls through multicall() in InterchainTokenService.sol.

Proof of Concept

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;
        }
    }

(<https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/utils/Multicall.sol#L22-L25&gt;)

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 &gt; 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.

Tools Used

Manual review

Recommended Mitigation Steps

(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

Assessed type

call/delegatecall


The text was updated successfully, but these errors were encountered:

All reactions

7 High

AI Score

Confidence

Low