Replay Attacks, Self-destructing Contracts, and Ethereum’s Memory Layout: What I Learned From Completing Damn Vulnerable Defi Challenge #13 ‘Wallet Mining’

crewmateJ
5 min readFeb 22, 2023

Since early December, I have been obsessed with the challenges over at damnvulnerabledefi.xyz . They have taken my knowledge of DeFi and smart contracts to the next level, to say the least. After I completed the first twelve challenges by early January, I was very happy to hear that @tinchoabatte had just released three more challenges!

Challenge #13 was most definitely the hardest, and it took me about a week to finish. Here is how I completed it, and what I learnt in the process. Keep in mind, many of the concepts used to complete this challenge I was learning for the first time. For more experienced blockchain developers, I imagine this challenge could be completed in 1–2 hours.

https://www.damnvulnerabledefi.xyz/challenges/wallet-mining/

For context, I had already completed Challenge 11 so I had a decent understanding of the Gnosis contracts already. The most helpful resource I found was this video.

After reading the problem, I knew the first and most obvious step would be to deploy the Gnosis contracts onto this new chain. I had just recently read @0xfoobar’s article on vanity addresses, so I was aware that it was possible to replicate contract addresses on new chains.

Here is a snippet from the article that I used as my first lead:

The article explains that smart contract addresses can be kept consistent across chains by using CREATE2 and keeping the ‘sender’ variable constant by deploying via a smart contract. I was able to quickly disregard this thesis through Etherscan, by seeing that the deployments for the Gnosis contracts 0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F and 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B were performed via an EOA, not a contract.

The article also nods at keyless transactions, which was another rabbit-hole I went down by reading this article. This theory was also disregarded since the EOA that deployed each contract had completed other transactions.

I was at a bit of a loss so I headed to the Github repo for clues, where I found this:

“Also, the scenario presented in the challenge is quite similar to an issue that happened not so long ago.”

The first thing I thought of was the Optimism hack in June last year, where 20 million OP tokens were sent to the wrong address but recovered by a white-hat through a replay attack. But I thought replay attacks were a thing of the past post EIP-155?

As it turns out, a transaction can be replicated across chains post EIP-155 if the transaction is submitted with a chain ID of zero (instead of the typical 1 for mainnet). I couldn’t find the chain ID on Etherscan but was able to quickly find it using this:

const tx = await provider.getTransaction(TX_HASH);
console.log('Chain ID is: ', tx.chainId);

For both deployment transactions 0x06d2fa464546e99d2147e1fc997ddb624cec9c8c5e25a050cc381ee8a384eed3 and 0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261 the chain ID was zero. These transactions were sent by 0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a and had a nonce of 0 and 2 respectively, so I had to check the chain ID of the transaction with nonce 1 on that EOA — which I found also had a chain ID of 0.

With this knowledge, I knew I could replicate these transactions even though I didn’t have the private key to the Gnosis Safe Deployer wallet 0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a.

So I had the first hurdle out of the way, now I needed to complete the rest of the challenge. The fact that the challenge name was ‘Wallet Mining’ and that 20 million tokens were stored in an empty address 0x9b6fb606a9f5789444c17768c6dfcf2f83563801 was enough of a hint for me.

The solution is that the 43rd Gnosis Safe that gets deployed via the WalletDeployer contract is this address. By making the wallet a 1-of-1 multi-sig and setting myself as the only owner, I could safely execute a transfer of the tokens out of this wallet. In fact, it even didn’t matter who the owners of the multi-sig were since I could execute the transfer within the ‘setup’ function of the MasterCopy contract.

/// Source: https://etherscan.deth.net/address/0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F

/// @dev Setup function sets initial storage of contract.
/// @param _owners List of Safe owners.
/// @param _threshold Number of required confirmations for a Safe transaction.
/// @param to Contract address for optional delegate call.
/// @param data Data payload for optional delegate call.
/// @param fallbackHandler Handler for fallback calls to this contract
/// @param paymentToken Token that should be used for the payment (0 is ETH)
/// @param payment Value that should be paid
/// @param paymentReceiver Adddress that should receive the payment (or 0 if tx.origin)
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
/// ...
/// ...
/// ...
}

So I had the 20 million tokens, but there were still some tokens in the WalletDeployer contract — 43 to be exact. I probably should have called it a day here and walked home with the 20 million tokens I had already stolen. But I wanted to complete the challenge!

I spent at least two days reading all about Solidity’s low-level assembly language just to understand the function in WalletDeployer.sol — part of this was learning the official memory layout for Solidity, through the docs.

// TODO(0xth3g450pt1m1z0r) put some comments
function can(address u, address a) public view returns (bool) {
assembly {
let m := sload(0)
if iszero(extcodesize(m)) {return(0, 0)}
let p := mload(0x40)
mstore(0x40,add(p,0x44))
mstore(p,shl(0xe0,0x4538c4eb))
mstore(add(p,0x04),u)
mstore(add(p,0x24),a)
if iszero(staticcall(gas(),m,p,0x44,p,0x20)) {return(0,0)}
if and(not(iszero(returndatasize())), iszero(mload(p))) {return(0,0)}
}
return true;
}

Even after understanding what is going on here, I couldn’t see exactly where the exploit was.

I did have some leads though. The problem explicitly mentions that AuthorizerUpgradeable.sol uses an upgradeable mechanism — the UUPS proxy pattern. I recalled reading a post by @0xCygaar about how he took control of the Qzuki implementation contract. However Qzuki used the Transparent Upgradeable Proxy pattern. I also remembered reading in the comments of this post that a vulnerability in UUPS proxies is that an attacker can take ownership, upgrade, then call a self-destruct function on the implementation contract.

Thinking about this, we can see how all the checks in the assembly block can be passed. I needn’t explain all them here, but if you review Proxy.solyou may be able to understand how this is the case.

So that’s about it, once we know we can force the ‘can’ function to return true, we can be payed 1 token for each deployment we make. Deploy 43 more Gnosis Safes, and we have officially taken all the tokens.

Ideally we would do the whole self-destruct-the-implementation-contract bit first, so that we only had to deploy a total of 43 safes rather than 86, but this was simply the order I found the solution.

Relevant files I used for the solution can be found at https://github.com/crewmateJ/damn-vulnerable-defi-13

--

--