InterchainTokenService.expressReceiveTokenWithData does not apply the Check-Effect-Interaction pattern. In some edge cases, the caller can lose funds.
The life cycle of sending token across chains contains 3 phases:
//P1: launch on source chain
TokenManager.callContractWithInterchainToken
interchainTokenService.transmitSendToken
interchainTokenService._callContract
gateway.callContract
emit ContractCall
//P2: The off-chain program collects all events of the target chain,
//builds the Command array, and operators sign for them.
//Call AxelarGateway.execute of the target chain to set each command to be executed to true.
AxelarGateway.execute
IAxelarAuth(AUTH_MODULE).validateProof
//Loop through setting each command to true
_setCommandExecuted(commandId, true);
_setCommandExecuted(commandId, true);
......
//P3: The relayer calls InterchainTokenService.execute for each command,
//or it can be called by the user himself.
InterchainTokenService.execute //we only consider SELECTOR_SEND_TOKEN_WITH_DATA here.
gateway.validateContractCall
InterchainTokenService._execute
_processSendTokenWithDataPayload
_popExpressReceiveTokenWithData
tokenManager.giveToken
destinationAddress.executeWithInterchainToken
expressReceiveTokenWithData uses the callerβs tokens to fullfill a callContractWithInterchainToken ahead of time. It is only called before the commandβs execution status was set to true, due to this check. That is, before the completion of P2 in the above life cycle. It first transfers the token from the caller to destinationAddress, then calls destinationAddress.expressExecuteWithInterchainToken, and finally calls _setExpressReceiveTokenWithData to set the value in the corresponding slot to the caller address(called expressCaller).Such a process is actually the Check-Interaction-Effect mode. destinationAddress can be a malicious contract, and it is very dangerous to interact with the malicious contract before setting the slot.
Suppose the following scenario:
Bob deploys a malicious contract A and calls TokenManager.callContractWithInterchainToken to send 100e18 token.
Aliceβs program periodically (every 1 minute) checks whether there is an outgoing sendToken. No such events were detected this time.
When the off-chain program calls AxelarGateway.execute, this tx1 enters the memory pool, pending.
Aliceβs program checks again. This time the command initiated by contract A in step 1 is detected. Therefore, call InterchainTokenService.expressReceiveTokenWithData. tx2 enters the mempool.
Bob notices the parameter input of tx1. Call A.setParram to submit this parameter to contract A (higher gas). tx3 enters the memory pool.
tx3 is prioritized due to higher gas payment.
When tx2 is executed, expressReceiveTokenWithData first transfers the token of 100e18 from alice to A, and then calls A.expressExecuteWithInterchainToken, where there are 2 parts.
* AxelarGateway.execute(input), this input is obtained in step 5. This is to [set all commands](<https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/AxelarGateway.sol#L372-L377>) contained in input as executable.
* InterchainTokenService.execute executes the command initiated in step 1. Since [expressCaller](https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/its/interchain-token-service/InterchainTokenService.sol#L643>) has not been set at this time, 100e18 token is sent to A [here](<https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/its/interchain-token-service/InterchainTokenService.sol#L657).
Execution flow returns here, where expressCaller is set to alice. However, alice will never be able to obtain the 100e18 token. A gets 200e18 tokens.
In summary, A gets an extra 100e18, which belongs to alice. This scenario is not common. Itβs an edge case.
Manual Review
File: contracts\its\interchain-token-service\InterchainTokenService.sol
467: function expressReceiveTokenWithData(
468: bytes32 tokenId,
469: string memory sourceChain,
470: bytes memory sourceAddress,
471: address destinationAddress,
472: uint256 amount,
473: bytes calldata data,
474: bytes32 commandId
475: ) external {
476: if (gateway.isCommandExecuted(commandId)) revert AlreadyExecuted(commandId);
477:
478: address caller = msg.sender;
479: ITokenManager tokenManager = ITokenManager(getValidTokenManagerAddress(tokenId));
480: IERC20 token = IERC20(tokenManager.tokenAddress());
481:
482: SafeTokenTransferFrom.safeTransferFrom(token, caller, destinationAddress, amount);
483:
484:- _expressExecuteWithInterchainTokenToken(tokenId, destinationAddress, sourceChain, sourceAddress, data, amount);
484:+ _setExpressReceiveTokenWithData(tokenId, sourceChain, sourceAddress, destinationAddress, amount, data, commandId, caller);
485:
486:- _setExpressReceiveTokenWithData(tokenId, sourceChain, sourceAddress, destinationAddress, amount, data, commandId, caller);
486:+ _expressExecuteWithInterchainTokenToken(tokenId, destinationAddress, sourceChain, sourceAddress, data, amount);
487: }
Reentrancy
The text was updated successfully, but these errors were encountered:
All reactions