1

After solving all ethernaut levels I left in the end 2 levels with Proxy contracts because I didn't know much about it.All info in google is the same, so I decided to use Unstructured Storage Proxies problem here. Here is the code of this level:

// 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 {
        _upgradeTo(_newImplementation);
    }
}

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");
        }
    }
}

As I understand the idea of this problem. If we put smth into PuzzleWallet first variables:

 address public owner;
 uint256 public maxBalance

Proxy variables:

 address public pendingAdmin;
 address public admin;

will get the same values.

I noticed that after deploying values of this 2 variables in PuzzleWallet were zeroes. So I found out PuzzleWallet address and simply called function init(uint256 _maxBalance) public to put my address into owner variable and to see if it works. ( I understand that my aim is 1st slot not 0). After that I decided to check the slots:

console.log("Wallet")
    for (i = 0;i<3;i++){
    let slot3 = await provider.getStorageAt("0xd80CDe189a3654e6370A41E3bbd4133291F94eDf", i);
    console.log("Slot",i,slot3)
    }
    console.log("Proxy")
    for (i = 0;i<3;i++){
      let slot3 = await provider.getStorageAt("0x0506628c59fB4EB881165d3A690F27E0A99014e0", i);
      console.log("Slot",i,slot3)
      }

And I have this:

Wallet
Slot 0 0x000000000000000000000000eea3babd50e53f297eae61cb54b1d794c16ef588 // Owner in wallet(my msg.sender)
Slot 1 0x0000000000000000000000000000000000000000000000000000000000000000
Slot 2 0x0000000000000000000000000000000000000000000000000000000000000000
Proxy
Slot 0 0x0000000000000000000000004df32584890a0026e56f7535d0f2c6486753624f //pendingAdmin
Slot 1 0x0000000000000000000000004df32584890a0026e56f7535d0f2c6486753624f //admin in Proxy
Slot 2 0x0000000000000000000000000000000000000000000000000000000000000000

Theory in google says: One cannot just go around and simply declare address implementation in Proxy contract because that would cause storage collision with the storage of implementation which may have multiple variables in it at overlapping storage slots! enter image description here Any write to var1 in implementation would actually write to implementation in Proxy (storage layer!).

But what I see is two independent storages of 2 contracts. And variables from implementation contract have their own life in slots. May be this collision works only if I'm trying to set a variable in the implementation contract using proxy? But in this case proxy makes a delegateCall to Implement contract, does it's business and doesn't pay any attention to Implementation's contract slots, simply moving back to the proxy contract.(sorry for my slang, I'm always trying to visualize what's happening)

After looking at solution in internet I'm a little bit confused and dissapointed, because I was trying to change uint256 public maxBalance too with the idea of 1 storage for 2 contracts. And people were simply using all functions one by one.

Maybe there is a way how I can put value from storage 0 in Implementation contract to storage 0 in Proxy contract?

1 Answer 1

1

May be this collision works only if I'm trying to set a variable in the implementation contract using proxy

Yes, you are right.

delegatecall works like this: The proxy contract grabs all the logic from the implementation contract, but use all proxy's value(state variables, msg.sender, msg.value, etc.) to do the computation.

So if you want to exploit implementation contract to alter state variables in proxy contract, you should always call proxy contract and let it delegatecall the implementation contract for you.

If you directly call the implementation contact, it will have no business with the proxy contract.

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.