Hello,
I have some questions about the UUPS proxy pattern after reading through the documentation and implementing what I think is correct. First, here's a simplified version of my contracts:
Implementation/(Logic) Contract:
pragma solidity ^0.8.21;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyLogic is Initializable, OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable {
/**
* see: https://forum.openzeppelin.com/t/what-does-disableinitializers-function-mean/28730/4
* To summarize, the implementation contract (logic contract) should look similar to the
* first post. It makes sense to have a both _disableInitializer constructor and initialize
* function. The constructor is to disable the **implementation** contract from being initialized,
* while the initializer function is to allow the **proxy** to be initialized."
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address initialOwner_
) initializer public {
__Ownable_init(initialOwner_);
__Pausable_init();
__UUPSUpgradeable_init();
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
}
Proxy Contract:
pragma solidity 0.8.21;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract MyProxy is ERC1967Proxy {
constructor(
address implementation_
) ERC1967Proxy(implementation_, abi.encodeWithSignature(
"initialize(address)",
msg.sender
)) {
}
function implementation() public view returns (address) {
return _implementation();
}
receive() external payable {}
}
Now, here's how I'm deploying them for testing (using a local hardhat node w/ brownie connected to it):
- Deploy the
MyLogic
contract fromaccounts[0]
- Deploy the
MyProxy
contract fromaccounts[1]
, passing it the logic address from (1) - Create a generic
Contract
instance (this is a brownie thing) withContract.from_abi("MyProxyContract", proxy.address, logic.abi)
(it should be mostly clear what's going on here, let me know if not). This is the "real" proxy that I will interact with
In general, this is how I believe the contracts should be deployed, because if I do this I can interact w/ the address from (2) (via the contract instance created in step (3)), and see all the functions/vars from (1). Making changes to the contract works as expected, where the state of the proxy is updated.
Now, my questions/observations:
-
I noticed that after deploying the logic contract (1), I can call
.owner()
on it (presumably from the inheritedOwnableUpgradeable
functionality), but the owner is the zero address. However, if I look at the_owner
variable on it, it's set to accounts[0], which is what I would have expected.owner()
to return. If I call.owner()
on the contract created in step (3), I get theaccounts[1]
address as expected.The proxy portion seems right, but as far as the logic part, am I doing something wrong or am I misunderstanding something? Shouldn't the owner of the logic contract be
accounts[0]
? (Noteaccounts[0]
is not the zero address...it's the first hardhat account in the local hardhat node) -
As you can see I wrote an
implementation()
function on my proxy to easily return the logic address. Is there anything wrong w/ doing this? Is there a better way to get the address? I noticed that I cannot call.implementation()
on the Contract instance created in step (3), presumably because it's using the abi of the logic contract which doesn't have animplementation()
function...but how would I setup my proxy so that it's easy to get the implementation address (not the storage slot, the actual address) off it? -
Finally, if I call
.pause()
on the contract from step (3) (the "real" proxy), it works as expected. I can check if it's paused, unpaused, and change it as the owner, etc. I can also deploy any number of additional proxies like in step (2), from any accounts, connect them to the same logic address, and it seems it all works right. Ownership/pausability of each proxy is enforced as expected.However, I don't see an obvious way to pause the logic contract, since its owner is the zero address and therefore I can't call
.pause()
on it since it is anonlyOwner
function. Is there a way to implement pausability at the implementation level, such that as the owner of the logic contract I could pause that contract and stop all proxies from interacting with it? -
Finally, in general, is there anything obvious that I'm doing wrong here? Am I misunderstanding anything about the implementation of this UUPS pattern?
Thanks so much for reading and for any insight you can provide!