0

I have been encountering challenges when testing contracts that utilize immutable state variables. As we know, immutable variables are assigned only once during contract deployment and cannot be modified thereafter. This offers advantages such as efficient reading, gas optimization, and security, but makes it hard to test various scenarios as I have to redeploy the contract each time I want to change the value of an immutable variable. Previously, I would simply create a contract for testing purposes that extends the target contract in order to expose any public state variables that need to be modified to simulate different scenarios, as shown in the example code below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "../Hifi.sol";

/// @author Hifi
/// @dev Strictly for test purposes. Do not use in production.
contract GodModeHifi is Hifi {
    constructor(address account, address minter_) Hifi(account, minter_) {}

    function __godMode_setCheckpoint(
        address delegatee,
        uint32 index,
        uint32 blockNumber,
        uint32 newVotes
    ) external {
        checkpoints[delegatee][index] = Checkpoint(blockNumber, newVotes);
    }

    function __godMode_setNumCheckpoints(address account, uint32 newNumCheckpoints) external {
        numCheckpoints[account] = newNumCheckpoints;
    }
}

The method above would not work with immutable state variables. I'm looking for an alternative method that could help me improve the testability of my contracts without compromising the benefits of utilizing immutable state variables (gas optimization, security, etc.).

** Example above is from hifi-finance/hifi-governance.

** Disclaimer: I work at Hifi Finance as a full-time blockchain engineer.

2 Answers 2

1

The best thing to do when testing is to use test fixtures (setup/teardown).

However, a totally contrived way to make a mutable-immutable variable, is to make the immutable variable private/internal and include a virtual view getter function. See below:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract Foo {
    uint256 immutable _VALUE;

    constructor(uint256 _initialValue) {
        _VALUE = _initialValue;
    }

    function VALUE() external view virtual returns (uint256) {
        return _VALUE;
    }
}

contract GodModeFoo is Foo {
    uint256 private __godMode_value;
    bool private __use_godMode_value;

    constructor(uint256 _initalValue) Foo(_initalValue) {}

    function __set_value(uint256 _value) external {
        __use_godMode_value = true;
        __godMode_value = _value;
    }

    function VALUE() external view override(Foo) returns (uint256) {
        if (__use_godMode_value) {
            return __godMode_value;
        }
        return _VALUE;
    }
}

You'll notice that the GodModeFoo contract has a getter function which overrides the base contract. The immutable will be returned by default, but if the setter function is called, the view function will switch to returning the non-immutable value.

Again though, the best way to achieve what you want (different values for an immutable across tests) is to use fixtures and parametrization, it's standard practice in unit testing.

1
  • Thanks @0xcefa1090d. I have upvoted your answer, but I believe I may have found a better way that doesn't require defining public getter functions or making any changes whatsoever to the contract to be tested. I will post the answer asap. Commented Mar 21, 2023 at 0:49
0

The unique property of immutable state variables is that they are set either upon declaration or within the constructor. Typically, immutable state variables are initialized in the constructor, with their values depending on the parameters passed to it. To enhance the testability of contracts with immutable state variables, this property can be leveraged by creating a test contract as demonstrated below:

// SPDX-License-Identifier: UNLICENSED
// solhint-disable func-name-mixedcase
pragma solidity ^0.8.4;

import "@prb/contracts/token/erc20/IErc20.sol";

import "../external/uniswap/interfaces/IUniswapV3Pool.sol";
import "../oracles/IUniswapV3PriceFeed.sol";
import "../oracles/UniswapV3PriceFeed.sol";

/// @title GodModeUniswapV3PriceFeed
/// @author Hifi
/// @dev Strictly for test purposes. Do not use in production.
contract GodModeUniswapV3PriceFeed is IUniswapV3PriceFeed {
    IUniswapV3PriceFeed internal instance;

    constructor(
        IUniswapV3Pool pool_,
        IErc20 refAsset_,
        uint32 twapInterval_
    ) {
        instance = new UniswapV3PriceFeed(pool_, refAsset_, twapInterval_);
    }
...
    function pool() external view returns (IUniswapV3Pool) {
        return instance.pool();
    }

    function refAsset() external view returns (IErc20) {
        return instance.refAsset();
    }

    function twapInterval() external view returns (uint32) {
        return instance.twapInterval();
    }
...
    function __godMode_setPool(IUniswapV3Pool newPool) external {
        instance = new UniswapV3PriceFeed(newPool, instance.refAsset(), instance.twapInterval());
    }

    function __godMode_setRefAsset(IErc20 newRefAsset) external {
        instance = new UniswapV3PriceFeed(instance.pool(), newRefAsset, instance.twapInterval());
    }

    function __godMode_setTwapInterval(uint32 newTwapInterval) external {
        instance = new UniswapV3PriceFeed(instance.pool(), instance.refAsset(), newTwapInterval);
    }
}

In the code above, the test contract GodModeUniswapV3PriceFeed utilizes elements of the Proxy and Factory Method software design patterns. It implements the UniswapV3PriceFeed interface, but forwards function calls to the internal instance. And when a god mode setter function is called, it deploys a new instance with a different configuration. This method allows to maintain the benefits of using immutable state variables, such as gas optimization and security, while improving testability.

** Example above is from hifi-finance/hifi.

** Disclaimer: I work at Hifi Finance as a full-time blockchain engineer.

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.