1

I'm used to Java mocks, where I could access and modify private values. As we are on our local machine and own ethereum node, we may do the same.

My problem is I want to modify a required address. Only my Oracle is authorized to call a function, and I would like my test to do it so.

contract CodeFanSites {
    address  private constant _Oracle = 0xf17f52151EbEF6C7334FAD080c5704D77216b732;

    function addSite(address payable user, string memory domain) external {
        require(msg.sender == _Oracle);
         // ....
    }
}

And my tests, using truffle :

contract("CodeFanSites", (accounts) => {
  let [nik, oracle] = accounts;

  it("should be able to add a new domain site", async () => {
    const contract = await CodeFanSites.new();
    console.log({balance: sites, accounts, nik});

    // I need before here to modify the _Oracle private address in the contract
    const result = await contract.addSite(oracle,  "site.com");
    // ....
})

How can I change the _Oracle private address in the contract before calling the addSite() function in the test ?

Note that _Oracle don't have to be constant, it may just be private. I don't want anybody to be able to change this value, except while testing.

6
  • you cannot. it is a constant. why you don't give the address as a parameter when you deploy the CodeFanSites Contract? then it will be more flexible
    – Majd TL
    Commented Dec 7, 2021 at 15:28
  • Because I don't want anybody to be able to change my Oracle adress. It would break my system Commented Dec 8, 2021 at 8:14
  • It don't have to be constant though. Private is enough. Commented Dec 8, 2021 at 14:55
  • You may want to try something like Smock github.com/defi-wonderland/smock.
    – Ismael
    Commented Dec 8, 2021 at 20:02
  • Perfect, thank you. I think I will stay with truffle for a while, but I will surely move quickly to HardHat with such features. Commented Dec 8, 2021 at 21:11

4 Answers 4

4
+100

The short answer is you can't so have to design contracts with a testing plan in mind.

In my opinion, the root of the problem is the hard-coded oracle address. This could be abstract, allowing oracle mocks during testing. Consider:

contract CodeFanSites {

  address public immutable _oracle;

  constructor(address oracle) {
    _oracle = oracle;
  }

  function addSite(address payable user, string memory domain) external {
    require(msg.sender == _oracle);
    // ....
  }
}

Summary of changes.

  1. _oracle - style
  2. public - discoverable state
  3. immutable - same effect as constant but more flexible at deployment time
  4. constructor() - set the value

You can run tests on local chains or forks of networks, create many accounts and even work with free money and mocks but you cannot force a contract to do something it wasn't coded to do. There is no way to override that hard-coded address or change the logic in the function temporarily.

The proposed method gives the same efficiency as a constant and the same immutability as well as a way to confirm correct deployment using the address that it must be on the mainnet. You could even consider embedding comments to document your intent - "correct" oracle addresses on different testnets and mainnet.

Hope it helps.

1
  • 2
    Using "immutable" makes the contract more robust against unintentionally overriding the _oracle address.
    – Ismael
    Commented Jan 13, 2022 at 3:58
1

You need some functions and keep the owner (contract creator) in private variable. _owner should be set in constructor and a modifier will check setOracle function to be called only by the owner.

contract CodeFanSites {
    address private _owner;
    address private _Oracle = 0xf17f52151EbEF6C7334FAD080c5704D77216b732;

    modifier onlyOwner() {
        require(msg.sender == _owner, "not authorized");
        _;
    }

    constructor () {
        _owner = msg.sender;
    }

    function setOracle(address oracle) public onlyOwner returns (bool)            
    {
        _Oracle = oracle;
        return true;
    }

    function addSite(address payable user, string memory domain) external {
        require(msg.sender == _Oracle);
         // ....
    }
}

then you can set oracle before addSite function

const result = await contract.setOracle(oracle);
1

In my opinion, the best approach to test the exact same smart contract that will be deployed in production is to use a derivate smart contract for testing and use the internal modifier instead of the private one.

contract CodeFanSites {
    address internal _Oracle = 0xf17f52151EbEF6C7334FAD080c5704D77216b732;
}

contract CodeFanSitesTest is CodeFanSites {

    function init(address _testOracle) public {
        _Oracle = _testOracle;
    }

    function getInternalOracleAddress() public view returns (address) {
        return _Oracle;
    }

}

This way, you can test CodeFanSitesTest as it is CodeFanSites, without worrying to modify CodeFanSites before deploying in production: indeed, private and internal modifiers work exactly the same if there's no derivate contract.

Fewer modifications before deployment mean fewer risks in general.

0

Note : The option from Rob at constructor time is also very good, probably safer (https://ethereum.stackexchange.com/a/118397/87100)

Finally I did something that PancakeBunny and probably others are doing :

function addVault(address vault, address token, uint allocPoint) public onlyOwner {
        require(vaults[vault].token == address(0), "BunnyChef: vault is already set");
        ...
}

So I don't need anymore mocks for this case, but I keep an eye on Smock lib

On my code, I added a setter for the owner and Oracle:

contract CodeFanSites {
     
   // Initialized to 0
    address OWNER;
    address ORACLE;

    /**
     * Can set owner only once, at deployment
     */
    function setOwner(address owner) public {
        require(OWNER == address(0), "Owner already set");
        OWNER = owner;
    }

    function setOracle(address oracle) public {
        require(msg.sender == OWNER);
        ORACLE = oracle;
    }
}

Then I added the values at deploy time, here using hardhat rather than truffle

async function main() {
  run('compile');
  const CodeFanDomains = await ethers.getContractFactory('CodeFanDomains');
  const codeFan = await CodeFanDomains.deploy();
  console.log('codeFan deployed to:', codeFan.address);
  const [owner, oracle, johnDoe] = (await ethers.getSigners()).map(u=>u.address)

  const instance = await CodeFanDomains.attach(codeFan.address);
  await instance.setOwner(owner);
  await instance.setOracle(oracle);
  console.log("Deployed ok")
}

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.