CounterV1) behind an ERC1967Proxy on a Virtual Environment, verifies both in one forge script run, upgrades the implementation to CounterV2, and covers re-verifying a proxy that was deployed without verification. For the Hardhat equivalent, see Verifying Proxy Contracts.
Before you begin
- Complete the setup from Deploy and verify contracts: a Virtual Environment, a funded deployer address, and a
foundry.tomlwithcbor_metadata = trueandbytecode_hash = "ipfs". - Set the environment variables:
- Install OpenZeppelin’s upgradeable contracts alongside the base library:
- Add both remappings to
remappings.txt:
remappings.txt
openzeppelin-contracts provides the proxy (ERC1967Proxy), and openzeppelin-contracts-upgradeable provides the implementation building blocks (Initializable, UUPSUpgradeable, OwnableUpgradeable). The upgradeable versions replace constructors with initializer functions, because constructors don’t run when state lives behind a proxy.
The implementation contract
src/CounterV1.sol
constructor() { _disableInitializers(); }locks the implementation. Only the proxy is supposed to hold state; this constructor makes a directinitializecall on the implementation address revert.initialize(...)replaces the constructor. The proxy delegatecalls it once, in the same transaction that deploys the proxy._authorizeUpgrade(...)is the required UUPS permissioning hook. It runs on everyupgradeToAndCall; if it reverts, the upgrade is blocked.
The deploy script
script/DeployProxy.s.sol
initialize calldata with abi.encodeCall (which type-checks the arguments at compile time, unlike abi.encodeWithSignature), and deploys the proxy. The proxy’s constructor stores the implementation address in the ERC-1967 implementation slot and delegatecalls initData against it, atomically initializing the proxy’s storage.
Deploy and verify both contracts
The proxy and the implementation are two contracts in one script, so the command is the sameforge script shape as any multi-contract deployment:
showLineNumbers
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy, the OpenZeppelin contract from your lib/ tree, not a contract in src/. Foundry resolves this automatically because the script’s broadcast log records every new and its bytecode.
Confirm the proxy linkage
The explorer decodes calls through the proxy when both the proxy and the implementation are verified. To confirm the proxy is delegating, read the ERC-1967 implementation slot directly:showLineNumbers
ERC1967Proxy source, a proxy tag with the linked implementation address, and an ABI view that includes the implementation’s functions. Transactions calling proxy.increment() or proxy.setNumber(...) are decoded with the implementation’s function names and parameter labels in the call trace.
If the explorer shows the proxy but doesn’t decode calls with the implementation’s ABI, the most common cause is that only one of the two contracts is verified. Re-run
forge verify-contract on whichever is missing.Upgrade the implementation
UUPS upgrades are issued throughupgradeToAndCall(newImpl, callData), inherited from UUPSUpgradeable. Add a V2:
src/CounterV2.sol
- Inherit from V1, or copy V1’s storage layout verbatim. Storage slots are positional; adding a new state variable before an existing one corrupts the proxy’s state.
- New state variables go after V1’s existing variables, never between them.
- Don’t change existing variable types or reorder declarations.
script/UpgradeProxy.s.sol
showLineNumbers
CounterV2 is deployed and verified (the output reports (1) contracts). The proxy address doesn’t change; only its ERC-1967 slot does. After the script returns:
showLineNumbers
CounterV2’s ABI. The old CounterV1 implementation remains verified at its original address but is no longer the active implementation.
Verify a proxy that’s already deployed
If you deployed without--verify, run forge verify-contract for each contract separately.
The implementation was deployed via new CounterV1() with no constructor arguments, so no --constructor-args flag is needed:
showLineNumbers
showLineNumbers
- The contract path uses the
lib/location, notsrc/. The proxy is OpenZeppelin’s code, so the source the verifier matches against lives inlib/openzeppelin-contracts/....forge script --verifyresolves this automatically;forge verify-contractdoesn’t. initDatamust match the deploy-timeinitDataexactly. Even if state changed after deployment, the constructor argument is the originalinitialize(owner, 42)calldata the proxy bytecode was constructed with.- The proxy’s constructor signature is
(address, bytes), andforge verify-contractexpects the already-ABI-encoded form, which is whatcast abi-encodeproduces.
Next steps
- Deploy and verify contracts: the base Foundry and Hardhat verification flow on a Virtual Environment.
- Smart Contract Verification Using Foundry: verifying contracts on public networks through Tenderly’s verification API.
- Verifying Proxy Contracts: the Hardhat plugin flow for UUPS, Transparent, and Beacon proxies.