Appearance
Guardian of the Bridge: Unmasking the "Withdrawal" Vulnerability in Damn Vulnerable DeFi
Imagine a crucial bridge connecting Layer 2 (L2) to Layer 1 (L1) on a blockchain, entrusted with millions of valuable tokens. You, as a vigilant bridge operator, receive a batch of withdrawal requests. Four seem legitimate, but one… one feels off. All funds are at risk if you fail. Your mission: finalize all withdrawals, prevent the suspicious one from draining the bridge, and secure the assets. This is the intriguing "Withdrawal" challenge from Damn Vulnerable DeFi.
Let's dive into the architecture and uncover how this ingenious vulnerability works and how to defeat it.
The Bridge's Blueprint: A Three-Contract Dance
The L1 side of our bridge operates through three key contracts:
L1Gateway.sol: This is the primary entry point for L2 withdrawals onto L1.- It enforces a
DELAYperiod (7 days) before withdrawals can be finalized. - It usually requires a
MerkleProofagainst arootof valid withdrawals. - Crucially for our role, as an
OPERATOR_ROLEholder, we can bypass the Merkle proof validation. - The Key Mechanic: Before initiating the actual withdrawal,
L1Gatewaytemporarily sets its publicxSendervariable to thel2Senderaddress that initiated the withdrawal on L2. ThisxSenderis then unset after the call.
- It enforces a
L1Forwarder.sol: This contract acts as an intermediary, receiving messages from theL1Gatewayand forwarding them to their ultimate L1 destination (in our case, theTokenBridge).- It records
successfulMessagesandfailedMessagesto prevent replay attacks or re-processing failed transactions. - The Key Mechanic: When
forwardMessageis called (byL1Gateway), it sets its internalcontext.l2Senderto thel2Senderparameter it received. Thiscontext.l2Sendercan later be retrieved viagetSender().
- It records
TokenBridge.sol: This is the vault, holding the precious Damn Valuable Tokens (DVT). ItsexecuteTokenWithdrawalfunction is responsible for transferring tokens to the L1 receiver.- The Key Vulnerability Check: This function includes a critical authorization check:solidityThis means:
if (msg.sender != address(l1Forwarder) || l1Forwarder.getSender() == otherBridge) revert Unauthorized();msg.sendermust be theL1Forwarder(which it will be).- AND
l1Forwarder.getSender()must not be equal tootherBridge. TheotherBridgeaddress represents the L2 side of the bridge itself.
- The Key Vulnerability Check: This function includes a critical authorization check:
The Subtle Trap: A Revert Disguised as Finality
The vulnerability hinges on the precise order of operations and the state changes across these contracts:
- When you, as an operator, call
L1Gateway.finalizeWithdrawal, theL1Gatewayfirst updates its internalfinalizedWithdrawalsmapping, marking the specific withdrawalleafastrue. This means, fromL1Gateway's perspective, the withdrawal is processed. - Only after marking it finalized,
L1Gatewaymakes an externalcalltoL1Forwarder.forwardMessage. L1Forwarderthen makes an externalcalltoTokenBridge.executeTokenWithdrawal.- Inside
TokenBridge.executeTokenWithdrawal, theUnauthorizedcheck fires.l1Forwarder.getSender()will return thel2Sendervalue that was passed toL1Forwarder.forwardMessage. - If this
l2Sender(the original L2 initiator of the withdrawal) happens to be theotherBridgeaddress, theUnauthorized()error is triggered, andTokenBridge.executeTokenWithdrawalreverts.
The critical insight: The TokenBridge transaction reverts, preventing tokens from being transferred, but the L1Gateway has already marked the withdrawal as finalized!
The Guardian's Solution
Your task is to finalize all the given withdrawals, ensuring the "suspicious" one doesn't actually transfer tokens, thus protecting the bridge's main funds.
Identify the Culprit: We must examine the provided L2 withdrawal logs (which, in the challenge, are hardcoded into the solution test file). We're looking for one of the
l2Senderaddresses (thel2SenderFsparameters in the solution code) that matches theotherBridgeaddress. ThisotherBridgeaddress is typically the L2 counterpart to our L1TokenBridge.Let's assume, through analysis of the provided data, that one of the
l2Sendervalues is indeed theotherBridge's address. (For example, ifl2SenderFs[4]in the solution code refers tootherBridge).Execute the Finalizations: As the operator, we can bypass Merkle proofs. We iterate through all the withdrawal requests. For each one, we craft the
messagetoL1Forwarder(which itself contains themessageforTokenBridge) and calll1Gateway.finalizeWithdrawalwith the appropriate parameters.- For the three legitimate withdrawals,
l1Forwarder.getSender() == otherBridgewill befalse, andexecuteTokenWithdrawalwill proceed, transferring the tokens as intended. - For the one "suspicious" withdrawal where
l2Sender(passed toL1Forwarder) isotherBridge, theTokenBridge.executeTokenWithdrawalfunction will revert due to theUnauthorizedcheck. This prevents the malicious transfer.
- For the three legitimate withdrawals,
Crucially, in both cases, L1Gateway will mark the withdrawal as finalized in its finalizedWithdrawals mapping because this state update occurs before the external call to L1Forwarder (and subsequently TokenBridge).
By performing these steps, we successfully finalize all withdrawals as required, prevent the specific malicious withdrawal from draining funds, and keep the majority of DVT tokens safely within the bridge, thus safeguarding the protocol.
Lessons Learned
The "Withdrawal" challenge brilliantly illustrates several key principles in smart contract security:
- Order of Operations: The sequence of state changes versus external calls is paramount. Updating state before an external call that might revert can lead to inconsistent states or, in this case, a successful defense against a specific attack.
- Context Management: Understanding how
xSenderandcontext.l2Senderprovide temporary context across contract calls is vital. Misinterpreting or misusing this context can create vulnerabilities. - Layered Authorization: While the
TokenBridgehad an authorization check, its reliance on a value (l1Forwarder.getSender()) that could inadvertently be set to a "forbidden" address created a loophole.
This challenge serves as a powerful reminder for developers to rigorously audit cross-contract interactions and authorization logic, especially in complex bridge designs.
Ready to test your skills and explore more vulnerabilities? Check out Damn Vulnerable DeFi at damnvulnerabledefi.xyz.