Using upgradeable contracts with methods that return a struct

hi there!

i'm running into a weird error when trying to use the upgradeable contracts. here's my test contract -

pragma solidity ^0.8.0;

import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";

contract Box is OwnableUpgradeable {

    struct TaxSchedule {
        // measured in seconds. eg 1 = 1 second
        uint256 ageTime;
        // measured as a percentage cut of the reward. eg 5 = 5% of the reward
        uint256 taxAmount;
    }
    // global tax schedule
    TaxSchedule[] taxes;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() public virtual initializer {
        __Ownable_init();
        // 1 month
        taxes.push(TaxSchedule({ageTime: 30 * 24 * 60 * 60, taxAmount: 20}));
        // 3 months
        taxes.push(TaxSchedule({ageTime: 3 * 30 * 24 * 60 * 60, taxAmount: 16}));
        // 6 months
        taxes.push(TaxSchedule({ageTime: 6 * 30 * 24 * 60 * 60, taxAmount: 12}));
    }

    function getTax() public virtual returns (TaxSchedule memory) {
        return taxes[0];
    }
}

Along with the contract I'm also deploying the ProxyAdmin and TransparentUpgradeableProxy. When I call getTax(), I see the following warning -

transaction.py:695: UserWarning: Unable to find function on 0x1184C6Bb603126B7bE10EaF3C94A4c9D1afB98F2 for input 0x54b762a6
  warn(f"Unable to find function on {contract} for input {self.input}")

I expect return_value to be a dictionary with the TaxSchedule data, but instead it's empty.

0x1184C6Bb603126B7bE10EaF3C94A4c9D1afB98F2 is the address of the deployed TransparentUpgradeableProxy. This is how I'm loading the Box contract (using brownie) -

Contract.from_abi("Box", '0x1184C6Bb603126B7bE10EaF3C94A4c9D1afB98F2', Box.abi)

Any idea what could be wrong?

If you're using a Transparent Proxy, you need to make sure to deploy the proxy with one account and interact with the proxy with another account. The proxy deployer does not have access to the underlying contract functions, it only has acccess to the proxy management interface (upgrade and so on).

Ok, I might be doing something wrong. So I deployed the contract, admin proxy and proxy contract with the same account. When I read the documentation it says I can access admin functions when I call the proxy via the proxy admin contract. Calls done as any other user (like the initial deployer account) gets forwarded to the underlying contract. This I know works because I took it from here - https://github.com/brownie-mix/upgrades-mix/blob/main/scripts/01_deploy_box.py#L36 and I tested it locally. The only thing that does not work is if the callee method on the underlying contract returns a struct. The struct is not there in the completed transaction. I bypassed this by changing this method to return uint256 instead (basically changing some logic on the dapp side).

Ok I see then it sounds like you're doing that right and it was not the issue.

I'm not sure what the issue might be but returning a struct should have no particular effect on an upgradeable proxy, data of any size or shape is returned as is. I would suggest trying the same thing without a proxy in the middle to see if it works that way.

Yeah I have and it does. I will dig into this when I get a moment and post an update. Thank you for responding to my post!

@frangio how do you guys recommend managing upgrades to contracts? Should we create a new contract every time? For instance create BoxV2 contracts instead of modifying Box contract. Is extending the previous contract ok? So BoxV2 is Box and override any methods from Box in BoxV2.

Hi @Vivek_Rao,

It's useful to keep both the old and new versions of the contract available in your source code, so that you can test the upgrade path. For the new version, it could be a modified copy of the old version, or an extension. Each approach has its tradeoffs, and there is some discussion of those tradeoffs in this thread: To Inherit Version1 to Version2, Or to Copy Code + Inheritance Order from Version1 to Version2?.

Note that if extending, you may want to mark functions as virtual so that they can be overridden (see thread: How to update contracts and preserve storage in inheritance)

Thanks Eric! I think I will go for the extending solution. I will make sure to mark all methods as virtual so they can be overridden.

1 Like