Version 5: how can/should the ProxyAdmin of the TransparentUpgradableProxy be used

in the constructor of the v5 TransparentUpgradableProxy a new ProxyAdmin contract is created that is accessible through the private variable _admin.

my understanding is that it is this ProxyAdmin contract that is supposed to be used to upgrade the implemented logic of a upgradable contract.

now to my question: what is the recommended way to interact with the ProxyAdmin as the TransparentUpgradableProxy doesn't provide external access to the proxy admin contract?

my current workaround is to extend TransparentUpgradableProxy with a getProxyAdmin function.
however, the documentation says that TransparentUpgradableProxy shouldn't be extended.

additional side question: why is _proxyAdmin() not a view function? at least the current implementation would suggest this is not changing any state variables of TransparentUpgradableProxy .

:1234: Code to reproduce


:computer: Environment

2 Likes

link to implementation

You can find the proxy admin address by looking for the AdminChanged event which gets emitted during construction of TransparentUpgradeableProxy. Or read the admin storage slot according to https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.0/contracts/proxy/ERC1967/ERC1967Utils.sol#L104 (but note that this slot could possibly be overwritten by the implementation, even though it should not do so because this leads to an inconsistent state between the immutable variable and the storage slot).

Generally TransparentUpgradeableProxy should not be extended and does not provide any externally accessible functions (other than admin related functions, which are only callable by the admin) specifically because of the transparent aspect, as described in the TransparentUpgradeableProxy documentation. Any exposed functions in the proxy would have a risk of selector clashing with the implementation.

Regarding _proxyAdmin(), you are right that it can be restricted to view, thanks for noticing! We've opened PR https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4688 to change this.

1 Like

Answer by an example pseudocode (which technically works with Truffle / Javascript):

const hash = web3.utils.keccak256("initialize()");

let proxy;
let logic;

async function deploy(firstTime, ...args) {
    const impl = await LogicContract.new(...args, {from: deployer});
    if (firstTime) {
        proxy = await ProxyContract.new(impl.address, upgrader, hash, {from: deployer});
        logic = await LogicContract.at(proxy.address);
    }
    else {
        await proxy.upgradeTo(impl.address, {from: upgrader});
    }
}

After calling this function once, you can:

  1. Use logic in order to call any of your contract functions
  2. Call this function again in order to upgrade that contract

The above assumes that you execute 2 after the LogicContract artifact has changed of course.

thank you for the quick response.

however, not sure if provided pseudocode works for TransparentUpgradableProxy of the v5 contracts:

  • no upgradeTo function there (only upgradeToAndCall in the _fallback())
  • upgradeToAndCall is restricted in a way that only the proxyAdmin can call this function

follow-up question: why is function _proxyAdmin() internal and not public? seems a bit like putting an unnecessary burden on the user to go and obtained the address via event as you suggest above. as the return value is implemented as immutable i can't see any bad side effects from changing the visibility to public. or am i missing something here?

1 Like

Making it public would break the transparent aspect and risks proxy selector clashing. See https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357 for a detailed explanation.

The simplest way to get the admin address is to read the ERC-1967 admin slot, as long as the implementation is trusted.

When you call a function which does not exist in the Proxy contract, execution falls into the fallback function, which then calls that function in the Logic contract.

When you call a function which DOES exist in the Proxy contract, it is executed immediately.

So every public or external function that you implement in the Proxy contract, is a function signature (name and input argument types) which you CANNOT implement in the Logic contract.

Hence you'd want to implement as little as possible in the Proxy contract.

1 Like

You'd note that in the pseudocode that I provided, the account executing upgradeTo is the upgrader (which you refer to as Peroxy-Admin):

await proxy.upgradeTo(impl.address, {from: upgrader});

I'm not familiar with the V5 change that you've mentioned about upgradeTo being replaced with upgradeToAndCall, but I'd imagine that the same concept applies for the new function, i.e., you should call it using the Proxy-Admin account, similarly to what I showed in the pseudocode above.

1 Like

Hello,

As I had very similar comments and questions as the ones brought up by Matthias_Zimmermann, I am wondering if you could elaborate a bit more on the solution that you provided. Indeed, in practice, in a foundry test execution, how can you read an ERC-1967 admin slot of a trusted implementation that was deployed during that execution?

Thanks in advance for your time and consideration.

N.B: I created a similar ticket here, which also addresses another (related) question.

EDIT: I was thinking that maybe the solution brought up by Matthias: an extension of the TransparentUpgradableProxy contract containing a public getter for the admin could be suited to a testing environment? Or is there any arguments against it that would make it a bad idea?

I've commented in the other thread how you can read the ProxyAdmin in Foundry.

While it is possible to extend TransparentUpgradeableProxy, we do not recommend adding any external getter in the proxy, as explained in https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol#L55C1-L55C1

1 Like