Skip to content

Infinite mint possible in L2WormholeGateway contracts

Critical
lukasz-zimnoch published GHSA-54q9-r92x-944r Oct 19, 2023

Package

cross-chain/arbitrum

Affected versions

<= v1.0.1

Patched versions

None
cross-chain/base
<= v1.0.0
None
cross-chain/optimism
<= v1.0.1
None
cross-chain/polygon
<= v1.0.1
None
solidity
<= v1.5.0
None

Description

The vulnerable version of the L2WormholeGateway contract defines a receiveTbtc function that finalizes the tBTC token L1 → L2 bridging process using a Verified Action Approval (VAA) created and signed by Wormhole's off-chain Guardians. The VAA holds important information about the cross-chain transfer of assets and allows the Wormhole Bridge to perform the correct settlements on both chains.

Specifically, the receiveTbtc function:

  1. Uses the given VAA (encodedVm) to complete the transfer on Wormhole Bridge’s end
  2. Receives Wormhole wrapped tBTC as result (bridgeToken)
  3. Uses the received wrapped tBTC to mint the canonical tBTC token for the given L2 chain
function receiveTbtc(bytes calldata encodedVm) external {
    // (...)

    uint256 balanceBefore = bridgeToken.balanceOf(address(this));
    bytes memory encoded = bridge.completeTransferWithPayload(encodedVm);
    uint256 balanceAfter = bridgeToken.balanceOf(address(this));

    uint256 amount = balanceAfter - balanceBefore;
    require(amount > 0, "No tBTC transferred");

    // (...) Mint `amount` to the receiver pointed in the VAA
}

Potential attack vector

By design, the L2WormholeGateway does not delve into the implementation details of the Wormhole Bridge. The receiveTbtc function simply passes the received VAA to the Wormhole Bridge and expects to receive some Wormhole-wrapped tBTC in return (if the Wormhole Bridge deems the given VAA is valid). The reasoning behind this is that VAAs are purely constructs of Wormhole, so the Wormhole Bridge is the only place that should initiate transfers of any assets involved based on them. The combination of these asset transfers and the fact that receiveTbtc does not independently verify received VAAs is where the problem starts.

A malicious actor can :

  1. Create a custom ERC-20 token contract, e.g. MaliciousERC20
  2. Deploy it on L2 and create its wrapped Wormhole representation on L1 Ethereum chain
  3. Bridge some MaliciousERC20 tokens from L2 to L1
  4. Bridge the now wrapped tokens back from L1 to L2 and get some valid VAAs that can be used to call receiveTbtc and bridge.completeTransferWithPayload in result

Of course, action (4) is not able to directly force the Wormhole Bridge to fill L2WormholeGateway with the Worhmole-wrapped tBTC token (bridgeToken) necessary to mint canonical L2 tBTC. This is because the VAA refers to L1 wrapped MaliciousERC20 instead of L1 tBTC, and the Wormhole Bridge can detect that. However, the bridge.completeTransferWithPayload call must invoke the MaliciousERC20.transfer function to unlock the malicious L2 tokens locked during (3). In theory, the malicious actor can implement MaliciousERC20.transfer in a way that affects the bridgeToken balance and forces the L2WormholeGateway to mint canonical L2 tBTC. Unfortunately, it turns out that this is actually possible in practice as the L2WormholeGateway contract additionally exposes the depositWormholeTbtc function:

function depositWormholeTbtc(uint256 amount) external {
    require(
   	 mintedAmount + amount <= mintingLimit,
   	 "Minting limit exceeded"
    );

    emit WormholeTbtcDeposited(msg.sender, amount);
    mintedAmount += amount;
    bridgeToken.safeTransferFrom(msg.sender, address(this), amount);
    tbtc.mint(msg.sender, amount);
}

The purpose of this function is to address an unlikely corner case where a depositor cannot automatically mint their canonical L2 tBTC and is left with the Wormhole wrapped tBTC asset instead. This function was supposed to improve the user experience in that case. Otherwise, the depositor would have to abandon the L2 bridging process and claim back their L1 tBTC.

The malicious actor can use MaliciousERC20.transfer to invoke depositWormholeTbtc which takes the depositor’s Wormhole wrapped tBTC, moves it to L2WormholeGateway and mints canonical L2 tBTC. This obviously causes a change of the L2WormholeGateway's wrapped tBTC balance (bridgeToken) once bridge.completeTransferWithPayload returns. In that case L2WormholeGateway mints the same amount again, doubling the amount of minted canonical L2 tBTC tokens. This pattern can be repeated to mint a significant amount of non-backed canonical L2 tBTC and depeg tBTC from Bitcoin.

Mitigation

Fortunately, to mitigate the scenario described above, it is sufficient to remove the depositWormholeTbtc function from the L2WormholeGateway contract. As a result, receiveTbtc is now the only point that can mint canonical L2 tBTC based on the change in Wormhole wrapped tBTC balance. To further enhance the security and resilience of this mechanism against unforeseen side effects triggered by the Wormhole Bridge contract, we also decided to make receiveTbtc non-reentrant.

Severity

Critical

CVE ID

No known CVE

Weaknesses

No CWEs