code4rena-2022-04-axelar-h01

[H-01] Cross-chain smart contract calls can revert but source chain tokens remain burnt and are not refunded

보고서

Summary

Axelar wrapped 토큰을 다른 체인으로 이동할 때, 출발지 체인의 토큰을 burn하고 도착지 체인에서 새로 발행 또는 전송한다. 이 때, 도착지 체인 측의 작업이 잘못되어 트랜잭션이 취소되어도 출발지 체인 측에서 burn한 토큰을 돌려받을 방법이 없다.

Keyword

bridge, cross chain, business logic vul

Vulnerability

callContractWithToken 함수를 호출하면 ERC20 토큰을 타 체인으로 이동하며 메시지를 전달할 수 있다.

출발지 체인 측의 토큰이 Axelar에서 배포한 ERC20 컨트랙트가 아닌 외부 토큰이라면 transferFrom을 이용해 Gateway 컨트랙트로 토큰을 전송한다. 반면 Axelar에서 제작한 wrapped 토큰이라면 출발지 체인측 토큰을 burn 하고 도착지 체인측에서 새로 발행 또는 전송한다(도착지 체인 토큰이 Axelar wrapped 토큰인지 여부에 따라 다르다).

    function sendToken(
        string memory destinationChain,
        string memory destinationAddress,
        string memory symbol,
        uint256 amount
    ) external {
@>      _burnTokenFrom(msg.sender, symbol, amount);
        emit TokenSent(msg.sender, destinationChain, destinationAddress, symbol, amount);
    }
 
    function callContractWithToken(
        string memory destinationChain,
        string memory destinationContractAddress,
        bytes memory payload,
        string memory symbol,
        uint256 amount
    ) external {
@>      _burnTokenFrom(msg.sender, symbol, amount);
        emit ContractCallWithToken(
            msg.sender,
            destinationChain,
            destinationContractAddress,
            keccak256(payload),
            payload,
            symbol,
            amount
        );
    }
 
    function _burnTokenFrom(
        address sender,
        string memory symbol,
        uint256 amount
    ) internal {
        address tokenAddress = tokenAddresses(symbol);
 
        if (tokenAddress == address(0)) revert TokenDoesNotExist(symbol);
        if (amount == 0) revert InvalidAmount();
 
        TokenType tokenType = _getTokenType(symbol);
        bool burnSuccess;
 
        if (tokenType == TokenType.External) {
            _checkTokenStatus(symbol);
 
            burnSuccess = _callERC20Token(
                tokenAddress,
                abi.encodeWithSelector(IERC20.transferFrom.selector, sender, address(this), amount)
            );
 
            if (!burnSuccess) revert BurnFailed(symbol);
 
            return;
        }
 
        if (tokenType == TokenType.InternalBurnableFrom) {
@>          burnSuccess = _callERC20Token(
@>              tokenAddress,
@>              abi.encodeWithSelector(IERC20BurnFrom.burnFrom.selector, sender, amount)
            );
 
            if (!burnSuccess) revert BurnFailed(symbol);
 
            return;
        }
 
        burnSuccess = _callERC20Token(
            tokenAddress,
            abi.encodeWithSelector(
                IERC20.transferFrom.selector,
                sender,
                BurnableMintableCappedERC20(tokenAddress).depositAddress(bytes32(0)),
                amount
            )
        );
 
        if (!burnSuccess) revert BurnFailed(symbol);
 
        BurnableMintableCappedERC20(tokenAddress).burn(bytes32(0));
    }

callContractWithToken로 보낸 토큰과 메시지는 이를 보내고자 한 목적지 체인 상의 목적지 컨트랙트(IAxelarExecutable를 상속해 구현)의 executeWithToken 함수를 호출해 수령한다.

    function executeWithToken(
        bytes32 commandId,
        string memory sourceChain,
        string memory sourceAddress,
        bytes calldata payload,
        string memory tokenSymbol,
        uint256 amount
    ) external {
        bytes32 payloadHash = keccak256(payload);
        if (
            !IAxelarGateway(gateway).validateContractCallAndMint(
                commandId,
                sourceChain,
                sourceAddress,
                payloadHash,
                tokenSymbol,
                amount
            )
        ) revert NotApprovedByGateway();
 
        _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount);
    }

목적지 체인에서 executeWithToken를 호출하는 트랜잭션은 여러가지 이유로 실패할 수 있다. 하지만 목적지 체인에서 실패하여도 출발지 체인에서 이미 burn 또는 transfer한 토큰을 복구할 방법은 없다.

따라서 도착지 체인에서 토큰을 받지 못해 유저가 자산을 잃을 수 있다.

Impact

유저가 목적지에서 토큰을 받지 못하여 자산을 잃는다.

Mitigation

목적지 체인에서 컨트랙트 콜이 실패할 경우 출발지 체인의 토큰을 다시 돌려준다.

Memo

주최측은 목적지 체인의 트랜잭션이 revert 되었더라도 다시 실행하면 된다고 하였다. 이 컨테스트를 열 때는 아직 공식적인 방법을 제공하지 않았던 것 같다. 현재는 Axelar의 익스플로러에서 유저의 지갑을 연결에 목적지 체인에서 트랜잭션을 재시도할 수 있는 방법을 제공한다.

주최측이 이렇게 설명했지만, 심판은 그래도 문제 자체가 존재하며, 이 시점에는 공식적인 방법이 없었으므로 High로 인정했다.


tags: bughunting, axelar, smart contract, solidity, bridge, cross chain, business-logic-vul, severity high