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:
- Uses the given VAA (
encodedVm
) to complete the transfer on Wormhole Bridge’s end
- Receives Wormhole wrapped tBTC as result (
bridgeToken
)
- 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 :
- Create a custom ERC-20 token contract, e.g.
MaliciousERC20
- Deploy it on L2 and create its wrapped Wormhole representation on L1 Ethereum chain
- Bridge some
MaliciousERC20
tokens from L2 to L1
- 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.
The vulnerable version of the
L2WormholeGateway
contract defines areceiveTbtc
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:encodedVm
) to complete the transfer on Wormhole Bridge’s endbridgeToken
)Potential attack vector
By design, the
L2WormholeGateway
does not delve into the implementation details of the Wormhole Bridge. ThereceiveTbtc
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 thatreceiveTbtc
does not independently verify received VAAs is where the problem starts.A malicious actor can :
MaliciousERC20
MaliciousERC20
tokens from L2 to L1receiveTbtc
andbridge.completeTransferWithPayload
in resultOf 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 wrappedMaliciousERC20
instead of L1 tBTC, and the Wormhole Bridge can detect that. However, thebridge.completeTransferWithPayload
call must invoke theMaliciousERC20.transfer
function to unlock the malicious L2 tokens locked during (3). In theory, the malicious actor can implementMaliciousERC20.transfer
in a way that affects thebridgeToken
balance and forces theL2WormholeGateway
to mint canonical L2 tBTC. Unfortunately, it turns out that this is actually possible in practice as theL2WormholeGateway
contract additionally exposes thedepositWormholeTbtc
function: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 invokedepositWormholeTbtc
which takes the depositor’s Wormhole wrapped tBTC, moves it toL2WormholeGateway
and mints canonical L2 tBTC. This obviously causes a change of theL2WormholeGateway
's wrapped tBTC balance (bridgeToken
) oncebridge.completeTransferWithPayload
returns. In that caseL2WormholeGateway
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 theL2WormholeGateway
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 makereceiveTbtc
non-reentrant.