Proper Design Pattern for Ownable Contract with Initializer and Several Secondary Contracts

I want to call secondary contract public methods with the primaryOnly modifier. However, to keep the contracts upgradeable the closest proof of concept uses initializer methods to pass instances of the secondary contracts to the main contract and several secondary contracts initialize with other secondary contract instances so that they may rely on their public view methods in require statements.

The problem I am running into is that I have made my public methods which alter or update storage on their contract variables onlyPrimary and when the Ownable contract calls them it causes the transaction to revert. Apparently the Ownable contract which stores instances of Secondary contracts in its initializer is not registered as the primary address when deployed with openzeppelin create.

Can someone provide a basic access control or ownership example to demonstrate the proper design pattern to achieve this goal?

What I have now looks like:

import "./Contract1.sol";
import "./Contract2.sol";

PrimaryContract is Ownable {
  Contract1 public contract1;
  Contract2 public contract2;

  function initialize(Contract1 _contract1, Contract2 _contract2) public {
    contract1 = _contract1;
    contract2 = _contract2;
  }

  function createContract1Record(string _name, uint _value) public {
    contract1.createRecord(_name, _value);
  }
}

import "./Contract2.sol";
Contract1 is Secondary {
  Contract2 public contract2;

  struct Record {
    string name;
    uint value;
  }

  uint256 recordCtr;

  mapping(uint256 => Record) records;
  
  function initialize(Contract2 _contract2) public {
    contract2 = _contract2;
  }

  function createRecord(string _name, uint _value) public onlyPrimary {
    require(!contract2.isNameRegistered(_name), "The name is already registered (made up example).");
    recordCtr = recordCtr.add(1);
    Record storage record = records[recordCtr];
    record.name = _name;
    record.value = _value;
    contract2.register(_name, recordCtr);
  }
}
1 Like

Hi @arosboro,

If I have understood what you are doing correctly:

  • The secondary contract needs the address of the primary contract.
  • The primary contract needs the address of the secondary contract.

Using upgradeable contracts one way to do this would be:

  1. The primary contract needs to be created and initialized with the owner
  2. The secondary contract needs to be created and initialized with the address of the primary contract
  3. The primary contract needs to have the address of the secondary contract set, and this needs appropriate access control, in this case onlyOwner.

Please note, initializers should only be run once, so we need to add a check to ensure this using the modifier initializer. See the Initializers documentation for details.

As an aside, I used OpenZeppelin Contracts Ethereum Package 2.2.3 as there was an issue with version 2.3.
openzeppelin link @openzeppelin/contracts-ethereum-package@2.2.3


I have created a small example doing this process:

Alpha.sol

pragma solidity ^0.5.0;

// Import base Initializable contract
import "@openzeppelin/upgrades/contracts/Initializable.sol";

// Import interface and library from OpenZeppelin contracts
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";

import "./Beta.sol";

contract Alpha is Initializable, Ownable {
    Beta private _beta;

    function initialize(address sender) public initializer {
        Ownable.initialize(sender);
    }

    function setBeta(Beta beta) public onlyOwner {
        _beta = beta;
    }

    function doStuff() public {
        _beta.doStuff();
    }
}

Beta.sol

pragma solidity ^0.5.0;

// Import base Initializable contract
import "@openzeppelin/upgrades/contracts/Initializable.sol";

// Import interface and library from OpenZeppelin contracts
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Secondary.sol";

contract Beta is Initializable, Secondary {
    function initialize(address sender) public initializer {
        Secondary.initialize(sender);
    }

    function doStuff() public onlyPrimary {
    }
}

Get Account

Choose account to use for owner, for simplicity, I used the first account.

$ oz accounts
? Pick a network development
Accounts for dev-1573097128410:
Default: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
All:
- 0: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1

...

Create Alpha

Create the Alpha contract on ganache-cli and call initialize to set the address of the owner.

$ oz create
✓ Compiled contracts with solc 0.5.12 (commit.7709ece9)
? Pick a contract to instantiate Alpha
? Pick a network development
✓ Deploying @openzeppelin/contracts-ethereum-package dependency to network dev-1573097445291
✓ Contract Alpha deployed
✓ Contract Beta deployed
All contracts have been deployed
? Do you want to call a function on the instance after creating it? Yes
? Select which function * initialize(sender: address)
? sender (address): 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
✓ Setting everything up to create contract instances
✓ Instance created at 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec

Create Beta

Create the Beta contract on ganache-cli and call initialize to set the address of the Alpha contract.

$ oz create
Nothing to compile, all contracts are up to date.
? Pick a contract to instantiate Beta
? Pick a network development
All contracts are up to date
? Do you want to call a function on the instance after creating it? Yes
? Select which function * initialize(sender: address)
? sender (address): 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
✓ Instance created at 0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe
0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe

Set Beta address

On the Alpha contract, set the Beta address, so that the Alpha contract can call the Beta.

$ oz send-tx
? Pick a network development
? Pick an instance Alpha at 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
? Select which function setBeta(beta: address)
? beta (address): 0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe
✓ Transaction successful. Transaction hash: 0x90e0ae6bca057719273ce3d98d0ff5f5a83293fa6de4f698df5e916f48cfdc54

Call Beta not from Primary

Attempt to call an onlyPrimary function on the Beta contract not from the Primary contract. This will fail.

$ oz send-tx
? Pick a network development
? Pick an instance Beta at 0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe
? Select which function doStuff()
✖ Calling: 'doStuff' with no arguments
Error while trying to send transaction to 0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe. Error: Returned error: VM Exception while processing transaction: revert Secondary: caller is not the primary account

Call Beta from Primary

Call an onlyPrimary function on the Beta contract from the Primary contract. This will succeed.

$ oz send-tx
? Pick a network development
? Pick an instance Alpha at 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
? Select which function doStuff()
✓ Transaction successful. Transaction hash: 0x3dab346850fd0590fdd7430bde5877bb3bfaf52dd2e9058abca8baea7eb72da3

Feel free to ask all the questions that you need.

1 Like

2 posts were split to a new topic: OpenZeppelin SDK from