ethereumsolidityhardhatproxy-patternevm

Issues using a delegateCall (proxy contract with Solidity) and using variables inside the delegate contract


I've written a simple proxy contract with solidity and I've got an issue with the variables inside the delegate contract. When I delegateCall, all my variables are equal to 0, except if there are constant. Is there any reason for that or am I missing something ?

My proxy contract :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

contract Proxy {
    mapping(string => address) public strategies;

    function addStrategy(string memory id, address implementation) external {
        strategies[id] = implementation;
    }

    function removeStrategy(string memory id) external {
        delete strategies[id];
    }

    function displayVar(string memory strategyId) external {
        address strategy = strategies[strategyId];
        require(strategy != address(0x0), "Strategy not found..");

        (bool success, bytes memory data) = strategy.delegatecall(
            abi.encodeWithSignature("displayVar()")
        );
    }
}

The deleguate contract :

pragma solidity ^0.8.3;

import "hardhat/console.sol";

contract Delegate {
    mapping(string => address) public strategies;
    address public constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52;
    address public curve = 0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5;
    address public constant cvx = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31;
    address public constant CVX = 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B;

    function displayVar() external returns (bool) {
        console.log(CRV);
        console.log(curve);
        console.log(cvx);
        console.log(CVX);
    }
}

the test with HardHat :

import { Contract, ContractFactory } from "ethers";
import { ethers } from "hardhat";

describe("test via proxy", function () {
  let Proxy: ContractFactory, proxy: Contract;
  let Delegate: ContractFactory, delegate: Contract;
  const stratName = "test";

  before(async function () {
    Proxy = await ethers.getContractFactory("Proxy");
    proxy = await Proxy.deploy();
    await proxy.deployed();

    Delegate = await ethers.getContractFactory("Delegate");
    delegate = await Delegate.deploy();
    await delegate.deployed();

    await proxy.addStrategy(stratName, delegate.address);
  });

  it("should display", async function () {
    const [owner] = await ethers.getSigners();
    await proxy.connect(owner).displayVar(stratName);
  });
});

And finally the output is :

0xd533a949740bb3306d119cc777fa900ba034cd52
0x0000000000000000000000000000000000000000
0xf403c135812408bfbe8713b5a23a04b3d48aae31
0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b

Solution

  • A quick intro: When you use delegatecall, you are “using” the targeted contract’s code (in your case Delegate) but keeping the storage of the proxy. In other words, the storage of the proxy is completely independent (that is the purpose of the proxy, to be upgradable and / or to save gas on deployment.

    With this in mind, your proxy contract can only use the code of delegate, but maintaining its own storage. But, constant and immutable variables do not occupy a storage slot, they are injected in the bytecode at compile time. That is why your proxy also has them. But all the other variables are defaulted to 0 (depending on the type).