Okay, so to be clear:
In understand the idea of the transparent proxy pattern. When the proxy instance is called, there's a check being made if the sender is the proxy admin. If that's the case it's gotta be an upgrade call, otherwise the call is delegated to the implementation contract.
Alright, bottom-line: there has to be an admin.
What is unclear to me is: Why does that admin specifically have to be an instance of ProxyAdmin
?
To be specific, when creating a TransparentUpgradeableProxy
, it always creates an instance of ProxyAdmin
:
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}
Looking at this, one might think, there has to be something special about it.
However, if we look at its implementation, it's really just this:
contract ProxyAdmin is Ownable {
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
constructor(address initialOwner) Ownable(initialOwner) {}
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
In other words, if I wanted to make a call to my proxy as an admin, I always have to go through an extra hop of calling ProxyAdmin
, while I could simply call the underlying proxy myself, if my account was set as an admin.
Why is this necessary?
As I'm writing this, there's one reason that comes to my mind why this might be needed:
If, say, my EOA is set as _admin
of the proxy, then that account would no longer be able to call any other functions on the proxy except the upgrade call. I'd have to create another account if I wanted to do anything but upgrading.
Is that the reason?