Puzzle Wallet
Become the admin of the Puzzle Wallet contract by exploiting delegatecall and storage layout vulnerabilities.
Vulnerable Code
Analyze the Solidity code below to find the vulnerability.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/access/Ownable.sol"; // OwnableUpgradeable? Assume standard Ownable for now
contract PuzzleProxy /* is Ownable ??? */ { // Proxy Contract
    address public pendingAdmin;
    address public admin;
    address public implementation; // Address of PuzzleWallet logic contract
    constructor(address _admin, address _implementation, bytes memory _initData) payable {
        admin = _admin;
        implementation = _implementation;
        (bool success,) = _implementation.delegatecall(_initData);
        require(success, "Proxy init failed");
    }
    // Function to propose a new admin
    function proposeNewAdmin(address _newAdmin) public {
        // In real proxy, admin check is complex. Assume basic check for now
        require(msg.sender == admin, "Not admin");
        pendingAdmin = _newAdmin;
    }
    // Function for pendingAdmin to approve the upgrade (not directly relevant to exploit)
    function approveNewAdmin(address _expectedAdmin) public {
        require(msg.sender == pendingAdmin, "Not pending admin");
        require(msg.sender == _expectedAdmin, "Not expected admin");
        admin = pendingAdmin;
        pendingAdmin = address(0);
    }
    // Function to upgrade implementation (not directly relevant to exploit)
    function upgradeTo(address _newImplementation) public {
        require(msg.sender == admin, "Not admin");
        implementation = _newImplementation;
    }
    // Fallback function delegates calls to implementation
    fallback() external payable {
        address _implementation = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}
contract PuzzleWallet is Ownable { // Logic Contract (Ownable state likely separate)
    address public owner; // Slot 0 (Ownable's owner)
    uint256 public maxBalance; // Slot 1
    mapping(address => bool) public whitelisted; // Slot 2 (Mapping base slot)
    mapping(address => uint256) public balances; // Slot 3 (Mapping base slot)
    function init(uint256 _maxBalance) public {
        // Initializer - sets maxBalance, SHOULD set owner too
        require(maxBalance == 0, "Already initialized"); // Basic re-init protection
        maxBalance = _maxBalance;
        owner = msg.sender; // <-- PROBLEM: Standard Ownable constructor sets owner, initializer might clash
    }
    // Function to set max balance - intended for owner
    function setMaxBalance(uint256 _maxBalance) external {
       // Assume Ownable's onlyOwner modifier is used implicitly
       require(msg.sender == owner, "Not owner"); // Explicit check if Ownable not quite right
       maxBalance = _maxBalance;
    }
    // Function to add to whitelist
    function addToWhitelist(address addr) public {
        require(msg.sender == owner, "Not owner");
        whitelisted[addr] = true;
    }
    // Deposit funds - only whitelisted
    function deposit() external payable {
        require(whitelisted[msg.sender], "Not whitelisted");
        // Vulnerable: check against maxBalance AFTER increasing balance
        balances[msg.sender] += msg.value; // Potential reentrancy, but not the main exploit path
        require(address(this).balance <= maxBalance, "Max balance reached");
    }
    // Execute arbitrary call - only whitelisted
    function execute(address to, uint256 value, bytes calldata data) external payable {
        require(whitelisted[msg.sender], "Not whitelisted");
        balances[msg.sender] -= value; // Decrement user balance first
        (bool success, ) = to.call{value: value}(data);
        require(success, "Execution failed");
    }
    // Multicall function - processes multiple calls in one tx
    // Vulnerable: Allows calling deposit() multiple times within one transaction
    function multicall(bytes[] calldata data) external payable {
        // Delegatecall vulnerability is NOT here; multicall allows repeated calls
        // require(whitelisted[msg.sender]); // Should check whitelist? Assume yes.
        bool depositCalled = false;
        for (uint i = 0; i < data.length; i++) {
            // Check if the call is to the deposit function
             bytes4 selector = bytes4(data[i][:4]);
             if (selector == this.deposit.selector) {
                 require(!depositCalled, "Deposit can only be called once");
                 depositCalled = true;
             }
             (bool success, ) = address(this).call{value: msg.value}(data[i]); // Pass value only once? Or per call? Ambiguous. Assume value applies to the whole multicall? Ethernaut sends value with multicall.
             require(success);
        }
    }
}
Submit Explanation
Explain the vulnerability and how to exploit it.
Hints (5)
Just a little peak
Hint 1
Hint 2
Hint 3
Hint 4
Hint 5
Explanation
Discomfort = Learning
