Damn Vulnerable DeFi V4 New Challenge Solution Walkthrough: Withdrawal — Part 1

Ton-Chanh Le
3 min readAug 1, 2024

--

You may not be aware, but Damn Vulnerable DeFi recently released their new V4 version. You can read their announcement here. The new version has migrated to Foundry, which offers an improved experience for Solidity developers. This allows you to focus more on the challenges themselves.

There are four brand new challenges: Curvy Puppet, Shards, Withdrawal, and The Rewarder. Additionally, there is a revisited version of Naive Receiver. All of them are equipped with advanced features like multicalls, meta-transactions, permit2, Merkle proofs, and ERC1155. In this series, I will walk you through how I solved each of these challenges in detail.

Spoiler alert: Don’t read beyond this point if you haven’t tried these challenges yet. I recommend enjoying them yourself first!

Part 2 can be found here.

Withdrawal is the first challenge I tackled with. The challenge requires you to rescue a token bridge between an L1 and L2 from a malicious withdrawal. Let’s dive deep into it.

Step 1: Understand the structure of the withdrawals and identify the suspicious withdrawal.

There are four withdrawals initiated on L2, whose event logs are provided in a JSON file. Now, let’s take a closer look at the first log:

{
"topics": [
"0x43738d035e226f1ab25d294703b51025bde812317da73f87d849abbdbb6526f5",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000087EAD3e78Ef9E26de92083b75a3b037aC2883E16",
"0x000000000000000000000000fF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5"
],
"data": "0xeaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba0000000000000000000000000000000000000000000000000000000066729b630000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010401210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}

The long hex strings might look intimidating, but don’t worry; we can decode them using the event signature. Fortunately, the authors provided us with the L2 smart contracts related to the withdrawal logs, where we can find the event signature in L2MessageStore.sol.

event MessageStored(
bytes32 id, uint256 indexed nonce, address indexed caller, address indexed target, uint256 timestamp, bytes data
);

It’s a bit clearer now. Three indexed arguments (i.e. nonce, caller, and target) will be placed after the first topic in the topics field of the log. The first topic is the selector of the event, which can be obtained using L2MessageStore.MessageStored.selector in Solidity ≥0.8.15 or keccak256("MessageStored(bytes32,uint256,address,address,uint256,bytes)").

The remaining arguments (i.e. id, timestamp, and data) will be encoded into the data field of the log.

  • The first 32-byte word is the message id, computed at this line.
  • The next word is the timestamp, which is START_TIMESTAMP in the test file Withdrawal.t.sol.
  • Since data is a dynamic array, the next two words are the offset and the length of data, followed by the actual data itself.

Let’s go deeper to figure out how the actual data is encoded to decode it. The data is computed at this line, which is the encoding of a contract call L1Forwarder.forwardMessage with its arguments. Therefore, the next 4 bytes are the selector of L1Forwarder.forwardMessage (remember to double check by recomputing the selector with keccak256). The next three words are the first three arguments uint256 nonce, address l2Sender, address target.

We can now envision the life cycle of an L2 message: the L2 system sends a message of an L2 sender (l2Sender) to L1 by emitting an event. Next, the L1 system captures and processes this event, as well as propagates the message to L1Forwarder.forwardMessage, which then forwards the message to target. But who’s the target? From the decoded address, it’s l1TokenBridge set up in the test file Withdrawal.t.sol.

It’s time to figure out which function in the contract TokenBridge of l1TokenBridge is called by examining the actual L2 message. message is also a dynamic array, thus the next two words are its offset and length. The 4-byte 0x81191e51 is the selector of a function in TokenBridge, which must be its sole function executeTokenWithdrawal (again, double-check with keccak256). The next two words are the arguments of executeTokenWithdrawal, which are the receiver and the withdrawn amount. Here, in the first withdrawal, the amount is 0x8ac7230489e80000, equivalent to 10e18.

For your convenience, I’ve split the JSON data field into multiple lines and labeled them. Try examining the remaining logs yourself to identify the suspicious withdrawal.

eaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba // id
0000000000000000000000000000000000000000000000000000000066729b63 // timestamp
0000000000000000000000000000000000000000000000000000000000000060 // data.offset
0000000000000000000000000000000000000000000000000000000000000104 // data.length
01210a38 // L1Forwarder.forwardMessage.selector
0000000000000000000000000000000000000000000000000000000000000000 // L2Handler.nonce
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // l2Sender
0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd50 // target (l1TokenBridge)
0000000000000000000000000000000000000000000000000000000000000080 // message.offset
0000000000000000000000000000000000000000000000000000000000000044 // message.length
81191e51 // TokenBridge.executeTokenWithdrawal.selector
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // receiver
0000000000000000000000000000000000000000000000008ac7230489e80000 // amount (10e18)
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000

To be continued… — Part 2 is now available here.

--

--