I'm following the tutorial here: https://blog.dixitaditya.com/ethernaut-level-24-puzzle-wallet

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "../helpers/UpgradeableProxy-08.sol";

contract PuzzleProxy is UpgradeableProxy {
    address public pendingAdmin;
    address public admin;

    constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) {
        admin = _admin;

    modifier onlyAdmin {
      require(msg.sender == admin, "Caller is not the admin");

    function proposeNewAdmin(address _newAdmin) external {
        pendingAdmin = _newAdmin;

    function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
        require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
        admin = pendingAdmin;

    function upgradeTo(address _newImplementation) external onlyAdmin {

contract PuzzleWallet {
    address public owner;
    uint256 public maxBalance;
    mapping(address => bool) public whitelisted;
    mapping(address => uint256) public balances;

    function init(uint256 _maxBalance) public {
        require(maxBalance == 0, "Already initialized");
        maxBalance = _maxBalance;
        owner = msg.sender;

    modifier onlyWhitelisted {
        require(whitelisted[msg.sender], "Not whitelisted");

    function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
      require(address(this).balance == 0, "Contract balance is not 0");
      maxBalance = _maxBalance;

    function addToWhitelist(address addr) external {
        require(msg.sender == owner, "Not the owner");
        whitelisted[addr] = true;

    function deposit() external payable onlyWhitelisted {
      require(address(this).balance <= maxBalance, "Max balance reached");
      balances[msg.sender] += msg.value;

    function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
        require(balances[msg.sender] >= value, "Insufficient balance");
        balances[msg.sender] -= value;
        (bool success, ) = to.call{ value: value }(data);
        require(success, "Execution failed");

    function multicall(bytes[] calldata data) external payable onlyWhitelisted {
        bool depositCalled = false;
        for (uint256 i = 0; i < data.length; i++) {
            bytes memory _data = data[i];
            bytes4 selector;
            assembly {
                selector := mload(add(_data, 32))
            if (selector == this.deposit.selector) {
                require(!depositCalled, "Deposit can only be called once");
                // Protect against reusing msg.value
                depositCalled = true;
            (bool success, ) = address(this).delegatecall(data[i]);
            require(success, "Error while delegating call");

This tutorial discussed that PuzzleProxy is the proxy contract and PuzzleWallet is the logic contract.

  • Does inheriting from the UpgradeableProxy grants PuzzleProxy all the privileges to use all the functions and state variables in UpgradeableProxy just by merely overriding them? Why or why not?
  • How does one know that PuzzleWallet is the logic contract?

2 Answers 2

  1. Contract inheritance: UpgradeableProxy is just a template with some functions, constants and variables so PuzzleProxy can reuse. Yes you can reuse the logic written in parent contracts, some functions require overidding, some functions dont. If you override the parents' function, you rewrite it's logic. It's a common pattern in programming to reuse code. When you write PuzzleProxy is UpgradeableProxy according to the code of the UpgradableProxy, your PuzzleProxy will have access to all internal, public, external functions (e.g. _upgradeTo,_implementation, ...) without having to rewrite the function logic. Notice the code in ethernaunt's github, this UpgradeableProxy is also inherited from another Proxy contract:
import "openzeppelin-contracts-08/proxy/Proxy.sol";
import "openzeppelin-contracts-08/utils/Address.sol";
contract UpgradeableProxy is Proxy {

So in the end, PuzzleProxy inherited some functions from UpgradeableProxy and UpgradeableProxy inherited some functions from Proxy source code (e.g. _fallback(), _delegate). To understand it fully, you should be more comfortable with object-oriented programming, the complete reference in solidity can be found here

  1. This is an interesting and difficult question, without the source code, the users will have to rely on the challenge description that PuzzleWallet is the logic of PuzzleProxy because the setup of the challenge is like that. In real life, given an instance of PuzzleProxy at address 0x1234..., if I know the code of PuzzleProxy here, I can know that the address of the logic contract is at _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;. I then can get the address of the implementation (How to read contents of a deployed contract's storage slot via web3?) and look at it's bytecode (it's the bytecode of PuzzleWallet. However to confirm it's exactly as PuzzleWallet, it's quite difficult unless you know how they compiled PuzzleWallet with the solc compiler). Another method to get the implementation address without source code is to trace the transactions to the proxy and see it uses DELEGATE call to the implementation address

----- Update

Ethernaunt gives you the code how they deploy the contracts: https://github.com/OpenZeppelin/ethernaut/blob/e2536a8072a72e146b3a22c6f021ae1ffc948288/contracts/contracts/levels/PuzzleWalletFactory.sol#L14-L19

  • So in the Ethernaut challenge, basically we take that PuzzleWallet is the logic contract because the description of the challenge said so? But you also showed me a link to the PuzzleProxy and I saw the address of the logic contract (PuzzleWallet) at the _implementation_slot. So which is which? Do we take it as granted because the description of the challenge said so or we have to find the address corresponding to the _implementation_slot to determine which contract is the logic contract if there are multiple contracts in the same .sol file? Thanks. Jan 8, 2023 at 7:12
  • 1
    In real life without any description and just giving the proxy address without any source code, it's very difficult to know. But in ethernaunt you can take it as granted because they gave you the code where they deploy the logic and the proxy : github.com/OpenZeppelin/ethernaut/blob/master/contracts/…
    – minhhn2910
    Jan 8, 2023 at 7:24
  • are you referring the proxy address as the address at _implementation_slot? I thought that is the address of the logic contract? Jan 8, 2023 at 7:33
  • 1
    The logic contract address is "STORED" at implementation_slot in the storage of proxy's contract. You have to read it from the proxy contract's state to get the logic contract address: read more ethereum.stackexchange.com/questions/125130/…
    – minhhn2910
    Jan 8, 2023 at 7:53
  • Do you mind taking a look at my other question? Thanks. ethereum.stackexchange.com/questions/142583/… Jan 9, 2023 at 18:06

Or you can setup like this by downloading the UpgradeableProxy-08.sol contract from OpenZeppelin github repo.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./helpers/UpgradeableProxy-08.sol";

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.