Appearance
Wallet Mining: Unmasking the Proxy's Fatal Flaw to Rescue Millions
The world of DeFi is a high-stakes arena, and sometimes, even the most robust systems can harbor hidden vulnerabilities. Welcome to "Wallet Mining," a Damn Vulnerable DeFi challenge that plunged us into a frantic race against time, a lost nonce, and a critical proxy design flaw, all to save 20 million DVT tokens.
The Scene of the Crime: A Promising Reward System Gone Awry
Our story begins with the WalletDeployer contract, designed to incentivize users to deploy Gnosis Safe wallets by rewarding them with 1 DVT per successful deployment. To ensure controlled distribution, it integrates with an upgradeable authorization mechanism managed by an AuthorizerUpgradeable contract, deployed via a TransparentProxy. This setup dictates that only specific "wards" are authorized to receive rewards for deploying Safes at designated "aim" addresses.
The core problem emerged when the team, in a baffling oversight, transferred a whopping 20 million DVT tokens to 0xCe07CF30B540Bb84ceC5dA5547e1cb4722F9E496. This address was intended to be a plain 1-of-1 Safe owned by a user. However, a critical piece of information was lost: the nonce required for the Safe's deterministic deployment. This meant the address, despite holding a fortune, was nothing more than an empty shell, not a functional Safe.
To add to the chaos, rumors of an undisclosed system vulnerability surfaced, sending the team into a panic. The desperate user, whose funds were now marooned, granted us access to her private key – our only hope to save her fortune. Our mission: recover all 20 million DVT tokens from the user's deposit address, retrieve the reward tokens from the WalletDeployer for the rightful ward, and do it all in a single transaction.
The Critical Glitch: A Subtle Storage Collision
The key to unlocking this challenge lies in a classic proxy pattern vulnerability: a storage collision. Let's break down the relevant contracts:
TransparentProxy.sol: This contract, inheriting from OpenZeppelin'sERC1967Proxy, introduces its ownupgraderstate variable:soliditycontract TransparentProxy is ERC1967Proxy { address public upgrader = msg.sender; // This occupies storage slot 0 // ... }AuthorizerUpgradeable.sol: This is the logic contract for theAuthorizerproxy. It defines its own initial state variable:soliditycontract AuthorizerUpgradeable { uint256 public needsInit = 1; // This also occupies storage slot 0 mapping(address => mapping(address => uint256)) private wards; // slot 1 // ... }
The Aha! Moment: When AuthorizerUpgradeable is used as an implementation behind TransparentProxy, its storage layout is mapped onto the proxy's storage. This means AuthorizerUpgradeable.needsInit effectively shares the same storage slot (slot 0) as TransparentProxy.upgrader!
Here's how this collision plays out:
- During the initial setup,
AuthorizerFactory.deployWithProxydeploys theTransparentProxyand callsAuthorizerUpgradeable.init()through it. Thisinit()call, on the proxy's context, setsneedsInit(which isupgraderin the proxy's storage) to0. - Immediately after,
AuthorizerFactorycallsTransparentProxy.setUpgrader(upgraderAddress). This function writesupgraderAddress(a non-zero address) into storage slot 0 of the proxy. - Crucially, this action overwrites the
needsInitvalue to a non-zero value (theupgraderAddressitself, when interpreted as auint256)!
This means that the require(needsInit != 0, "cannot init"); check in AuthorizerUpgradeable.init() will surprisingly pass again if we attempt to re-initialize the contract through the proxy!
The Exploitation Strategy: A Masterclass in Orchestration
With the storage collision identified, our path to salvation became clear. The solution involves a carefully choreographed series of steps, all bundled into a single transaction from our attacking contract:
Re-initializing the Authorizer: Leveraging the storage collision, we first call
AuthorizerUpgradeable(authorizer).init()again. This time, we set our attacking contract's address as awardauthorized to deploy to theUSER_DEPOSIT_ADDRESS(0xCe07CF30B540Bb84ceC5dA5547e1cb4722F9E496). This grants us the necessary permissions for the next step.Deploying the Phantom Safe: Now authorized, we invoke
WalletDeployer.drop(). This function allows us to finally deploy the 1-of-1 Safe at theUSER_DEPOSIT_ADDRESS. The challenge was the "lost nonce," so we had to find the correctsaltNoncethat would deterministically deploy the Safe at that specific address. The solution code revealed this to be13. Upon successful deployment, theWalletDeployertransfers 1 DVT reward to our contract, as we are now the authorizedward.Extracting the User's Fortune: With the Safe now live at
USER_DEPOSIT_ADDRESSand holding 20 million DVT, we can construct aSafeTxto transfer these tokens to our contract. We then use Forge'svm.sign()function with the provideduserPrivateKeyto generate the necessary signature for the Safe'sexecTransactionfunction. Executing this transaction drains the 20 million DVT from the newly deployed Safe into our attacking contract.Distribution and Finalization: Finally, our contract performs two crucial transfers:
- The 20 million DVT tokens (now held by our contract) are sent to the rightful
user. - The 1 DVT reward (received from
WalletDeployer) is transferred to the originalwardspecified in the challenge setup.
- The 20 million DVT tokens (now held by our contract) are sent to the rightful
All these actions, from the malicious re-initialization to the final fund distribution, are executed within the constructor of our MyContract, fulfilling the "single transaction" requirement.
Conclusion: A Reminder of Proxy Pitfalls
This "Wallet Mining" challenge serves as a powerful reminder of the subtle yet devastating vulnerabilities that can arise in complex smart contract architectures, especially when dealing with proxy patterns. The storage collision between TransparentProxy.upgrader and AuthorizerUpgradeable.needsInit was the critical flaw, allowing for an unauthorized re-initialization that completely subverted the intended access control.
It highlights the importance of:
- Thorough Storage Slot Analysis: Developers must be acutely aware of storage slot allocation in both proxy and implementation contracts to prevent unintended collisions.
- Robust Initialization Guards: While
needsInitwas a good intention, its interaction with the proxy's storage created a bypass. - Deterministic Deployment Nuances: The "lost nonce" aspect showcases the precision required when dealing with
create2addresses.
Ultimately, by understanding these intricate details and carefully orchestrating our exploit, we successfully rescued the user's funds and restored order to the precarious "Wallet Mining" system. Another day saved in the wild west of DeFi!