मुख्य सामग्री पर जाएं
एक proxy deployment में दो contracts होते हैं: proxy, जो हर call को delegate करता है, और implementation, जो logic रखता है। दोनों को अलग-अलग verify किया जाना चाहिए। किसी भी proxy के लिए जो ERC-1967 का पालन करता है, Tenderly explorer standard implementation storage slot पढ़ता है, इसलिए एक बार दोनों contracts verify हो जाने के बाद, proxy address पर calls को implementation के ABI के साथ decode किया जाता है। यह गाइड एक Virtual Environment पर एक ERC1967Proxy के पीछे एक UUPS implementation (CounterV1) deploy करती है, दोनों को एक forge script run में verify करती है, implementation को CounterV2 में upgrade करती है, और एक proxy को फिर से verify करना कवर करती है जो बिना verification के deploy किया गया था। Hardhat equivalent के लिए, देखें Proxy Contracts को Verify करना

शुरू करने से पहले

  • Deploy और verify contracts से setup पूरा करें: एक Virtual Environment, एक funded deployer address, और cbor_metadata = true तथा bytecode_hash = "ipfs" के साथ एक foundry.toml
  • environment variables सेट करें:
export TENDERLY_VIRTUAL_TESTNET_RPC=...   # your Virtual Environment RPC URL
export TENDERLY_VERIFIER_URL=$TENDERLY_VIRTUAL_TESTNET_RPC/verify
export PRIVATE_KEY=...                    # deployer private key
  • base library के साथ OpenZeppelin के upgradeable contracts स्थापित करें:
forge install OpenZeppelin/openzeppelin-contracts --no-git --shallow
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-git --shallow
  • दोनों remappings को remappings.txt में जोड़ें:
remappings.txt
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
forge-std/=lib/forge-std/src/
दोनों libraries अलग-अलग भूमिकाएं निभाती हैं: openzeppelin-contracts proxy (ERC1967Proxy) प्रदान करता है, और openzeppelin-contracts-upgradeable implementation building blocks (Initializable, UUPSUpgradeable, OwnableUpgradeable) प्रदान करता है। upgradeable versions constructors को initializer functions से बदलते हैं, क्योंकि जब state proxy के पीछे रहती है तो constructors नहीं चलते।

implementation contract

src/CounterV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public number;

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

    function initialize(address owner_, uint256 initialNumber) external initializer {
        __Ownable_init(owner_);
        number = initialNumber;
    }

    function increment() external {
        number += 1;
    }

    function setNumber(uint256 newNumber) external {
        number = newNumber;
    }

    function version() external pure virtual returns (string memory) {
        return "v1";
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
इस contract के तीन भाग proxy pattern के लिए मायने रखते हैं:
  1. constructor() { _disableInitializers(); } implementation को लॉक करता है। केवल proxy को state रखनी चाहिए; यह constructor implementation address पर direct initialize call को revert कर देता है।
  2. initialize(...) constructor को replace करता है। proxy इसे एक बार delegatecall करता है, उसी transaction में जो proxy को deploy करता है।
  3. _authorizeUpgrade(...) आवश्यक UUPS permissioning hook है। यह हर upgradeToAndCall पर चलता है; यदि यह revert होता है, तो upgrade block हो जाता है।

deploy script

script/DeployProxy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {CounterV1} from "../src/CounterV1.sol";

contract DeployProxyScript is Script {
    function run() public {
        address owner = vm.addr(vm.envUint("PRIVATE_KEY"));
        uint256 initialNumber = 42;

        vm.startBroadcast();

        // 1. Deploy the implementation.
        CounterV1 implementation = new CounterV1();

        // 2. Deploy the proxy, calling initialize(owner, 42) in the same tx.
        bytes memory initData = abi.encodeCall(CounterV1.initialize, (owner, initialNumber));
        ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);

        vm.stopBroadcast();

        console.log("Implementation:", address(implementation));
        console.log("Proxy         :", address(proxy));
        console.log("Owner         :", owner);
    }
}
script logic contract को deploy करती है, abi.encodeCall (जो abi.encodeWithSignature के विपरीत compile time पर arguments को type-check करता है) के साथ initialize calldata बनाती है, और proxy को deploy करती है। proxy का constructor implementation address को ERC-1967 implementation slot में store करता है और उसके विरुद्ध initData को delegatecall करता है, atomically proxy के storage को initialize करता है।

दोनों contracts को deploy और verify करें

Proxy और implementation एक script में दो contracts हैं, इसलिए command किसी भी multi-contract deployment जैसा ही forge script आकार है:
showLineNumbers
forge script script/DeployProxy.s.sol:DeployProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
Output का अपेक्षित अंत:
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (2) contracts
Submitting verification for [src/CounterV1.sol:CounterV1] 0xeDa7...
	Response: `OK`
	Details: `Pass - Verified`
Submitting verification for [lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy] 0xb22e...
	Response: `OK`
	Details: `Pass - Verified`
All (2) contracts were verified!
दूसरे submission पथ पर ध्यान दें: proxy को lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy के रूप में verify किया गया है, आपके lib/ tree से OpenZeppelin contract, src/ में contract नहीं। Foundry इसे स्वचालित रूप से resolve करता है क्योंकि script का broadcast log प्रत्येक new और उसके bytecode को रिकॉर्ड करता है।

proxy linkage की पुष्टि करें

Explorer proxy के माध्यम से calls को decode करता है जब proxy और implementation दोनों verify हों। यह पुष्टि करने के लिए कि proxy delegate कर रहा है, ERC-1967 implementation slot को सीधे पढ़ें:
showLineNumbers
PROXY=<your-proxy-address>
IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<implementation address, 20 bytes>
फिर proxy के माध्यम से एक implementation function को call करें:
cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42
Tenderly Dashboard में, proxy के address page में verified ERC1967Proxy source, linked implementation address के साथ एक proxy tag, और एक ABI view दिखाई देता है जिसमें implementation के functions शामिल हैं। proxy.increment() या proxy.setNumber(...) को call करने वाले Transactions call trace में implementation के function names और parameter labels के साथ decode किए जाते हैं।
यदि explorer proxy को दिखाता है लेकिन implementation के ABI के साथ calls को decode नहीं करता, तो सबसे सामान्य कारण यह है कि दोनों contracts में से केवल एक verify है। जो भी छूट गया है उस पर फिर से forge verify-contract चलाएं।

implementation को upgrade करें

UUPS upgrades upgradeToAndCall(newImpl, callData) के माध्यम से जारी किए जाते हैं, जो UUPSUpgradeable से inherit होता है। एक V2 जोड़ें:
src/CounterV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {CounterV1} from "./CounterV1.sol";

contract CounterV2 is CounterV1 {
    function decrement() external {
        number -= 1;
    }

    function version() external pure override returns (string memory) {
        return "v2";
    }
}
V2 लिखते समय Storage layout बाधाएं:
  • V1 से inherit करें, या V1 के storage layout को verbatim copy करें। Storage slots positional हैं; एक मौजूदा state variable से पहले एक नया state variable जोड़ने से proxy का state corrupt हो जाता है।
  • नए state variables V1 के मौजूदा variables के बाद जाते हैं, कभी बीच में नहीं।
  • मौजूदा variable types को न बदलें या declarations को न reorder करें।
script/UpgradeProxy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {CounterV1} from "../src/CounterV1.sol";
import {CounterV2} from "../src/CounterV2.sol";

contract UpgradeProxyScript is Script {
    function run() public {
        address proxy = vm.envAddress("PROXY");

        vm.startBroadcast();

        CounterV2 newImplementation = new CounterV2();
        CounterV1(proxy).upgradeToAndCall(address(newImplementation), bytes(""));

        vm.stopBroadcast();

        console.log("New implementation:", address(newImplementation));
        console.log("Proxy upgraded:    ", proxy);
    }
}
इसे उसी verify flags के साथ चलाएं:
showLineNumbers
PROXY=<your-proxy-address> \
forge script script/UpgradeProxy.s.sol:UpgradeProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
केवल CounterV2 deploy और verify होता है (output (1) contracts की रिपोर्ट करता है)। proxy का address नहीं बदलता; केवल इसका ERC-1967 slot बदलता है। script return होने के बाद:
showLineNumbers
cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<CounterV2 address>

cast call $PROXY "version()(string)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# "v2"

cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42   <-- state preserved; only the logic changed
Explorer में proxy address अब CounterV2 के ABI के विरुद्ध decode करता है। पुराना CounterV1 implementation अपने मूल address पर verified रहता है लेकिन अब सक्रिय implementation नहीं है।

एक proxy को verify करें जो पहले से deployed है

यदि आपने बिना --verify के deploy किया, तो प्रत्येक contract के लिए अलग से forge verify-contract चलाएं। implementation new CounterV1() के माध्यम से बिना constructor arguments के deploy किया गया था, इसलिए किसी --constructor-args flag की आवश्यकता नहीं है:
showLineNumbers
forge verify-contract <IMPL_ADDRESS> src/CounterV1.sol:CounterV1 \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --watch
proxy को deploy समय पर उपयोग किए गए exact constructor arguments की आवश्यकता होती है, ABI-encoded:
showLineNumbers
# ABI-encode: constructor(address implementation, bytes initData)
INIT_DATA=$(cast calldata "initialize(address,uint256)" <OWNER_ADDRESS> 42)

forge verify-contract <PROXY_ADDRESS> \
  lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --constructor-args $(cast abi-encode "constructor(address,bytes)" <IMPL_ADDRESS> $INIT_DATA) \
  --watch
सही करने के लिए तीन विवरण:
  1. contract path lib/ location का उपयोग करता है, src/ नहीं। proxy OpenZeppelin का code है, इसलिए verifier जिस source से match करता है वह lib/openzeppelin-contracts/... में रहता है। forge script --verify इसे स्वचालित रूप से resolve करता है; forge verify-contract नहीं।
  2. initData को deploy-time initData से exactly match करना चाहिए। भले ही deployment के बाद state बदल गई हो, constructor argument मूल initialize(owner, 42) calldata है जिसके साथ proxy bytecode बनाया गया था।
  3. proxy का constructor signature (address, bytes) है, और forge verify-contract पहले से ABI-encoded रूप की अपेक्षा करता है, जो cast abi-encode उत्पन्न करता है।

अगले कदम