Missing msg.value validation in transferOnly remainder path enables zero-cost drain of entire bridge native balance
The transferOnly remainder branch in _executePermits sends native tokens from the bridge's reserves without validating msg.value, letting any caller drain the entire native balance for the cost of a fresh key inception.
Description
The _executePermits function processes native token transfers without
validating that msg.value covers the total transfer amount. For the
transferOnly code path, the bridge sends native tokens from its own
reserves based solely on permit.amount, with zero validation that
the caller provided sufficient msg.value.
Root Cause
For ERC20 tokens, the permit + safeTransferFrom mechanism naturally
limits transfers to what the user signed for. For native tokens
(address(0)), no equivalent mechanism exists. The bridge simply calls
_transferNative using its own balance.
// Bridge.sol lines 331-341if (transferOnly) {uint256 remainder = permit.amount - totalTransferred;if (remainder > 0) {if (ectx.token == address(0)) {_transferNative(ectx.prerotatedKeyHash, remainder); // Sends from bridge reserves!} else {IERC20(permit.token).safeTransferFrom(ectx.user, ectx.prerotatedKeyHash, remainder);}totalTransferred += remainder;}}
No check exists that msg.value >= permit.amount or
msg.value >= totalTransferred.
Vulnerable Scenario
Prerequisites: Bridge holds any amount of native tokens from legitimate users' wraps.
- Attacker generates fresh keys (A, B, C) for inception.
- Attacker calls
registerKeyPairWithTransferwith:ctx.token = address(0)(native token)msg.value = 0- One native permit:
{token: address(0), amount: BRIDGE_BALANCE, recipients: []}
- Processing in
_executePermits:hasNativeTransfer = true(native permit exists)- Recipient loop: 0 recipients,
totalTransferred = 0 transferOnly = true(no wrap/unwrap flags)remainder = BRIDGE_BALANCE - 0 = BRIDGE_BALANCE_transferNative(prerotatedKeyHash, BRIDGE_BALANCE), sends entire bridge native tokens balance to attacker's next keytotalTransferred = BRIDGE_BALANCE == permit.amount, check passes
- Key inception succeeds. Attacker receives all native tokens balance.
Impact
- Complete drain of all native tokens held by the bridge.
- All users with wrapped native tokens become insolvent, cannot unwrap.
- Zero-cost attack (gas only) requiring only a fresh key inception.
Recommendation
Add a cumulative msg.value check for native token operations:
uint256 nativeTokenUsed = 0;nativeTokenUsed += recipient.amount;// ... in the transfer loop for native tokens (line 324):require(msg.value >= nativeTokenUsed, "Insufficient native tokens");// ... in the remainder path (line 335):require(msg.value >= nativeTokenUsed, "Insufficient native tokens");
Resolution
YadaCoin, Confirmed.
Zealynx, Fixed.

