透明代理升级合约:从部署到升级

  • 0xwu
  • 发布于 23小时前
  • 阅读 65

使用 Foundry + 本地链(Anvil)来演示 Counter 合约,使用透明代理的升级过程,完全可复刻实际运行,让你对透明代理升级有个清晰的概念。

1. 前言

为什么需要透明代理? 在智能合约的开发中,一旦合约部署到链上,它的代码即永久不可更改。但在实际项目中,我们常常需要修复漏洞、添加功能或调整逻辑,这时就需要「可升级合约」架构。而代理模式(Proxy Pattern),就是实现可升级合约的核心技术之一。

其中,透明代理(Transparent Proxy)是目前最广泛使用的升级模式,由 OpenZeppelin 等主流框架推广使用,具有明确的访问隔离机制和安全边界。


透明代理结构由两部分组成:

  • Proxy(代理合约):对外暴露统一地址,所有函数调用转发到当前逻辑合约。
  • Logic(逻辑合约):实际承载业务逻辑,随时可以部署新版本进行升级。

本质上,用户调用的是 Proxy,但执行的是 Logic 的代码,通过 delegatecall 保持存储不变。


为什么选择透明代理?
透明代理模式相比其它升级方式(如 UUPS、Beacon 等),有以下优势:

  • 安全边界清晰:管理员不能调用业务逻辑,普通用户不能进行升级操作。
  • 调用无歧义:所有业务逻辑函数由 Proxy 统一处理,避免权限混淆。
  • 兼容性好:与 OpenZeppelin、Hardhat、Foundry 等框架原生支持的升级体系一致。
  • 逻辑独立、存储共享:逻辑代码可升级,状态变量不丢失。

透明代理的使用场景:

  • DeFi协议 :需要频繁迭代功能、更新手续费逻辑等
  • NFT平台 :迭代版税策略或支持新标准
  • DAOs治理 :实现可投票决定合约升级
  • 长期维护项目:保证合约可长期演进,安全可控

2. 准备

  1. 原始逻辑合约 Counter.sol a. 定义了一个 number状态变量 b. 提供 setNumber() 和 increment() 两个函数
  2. 升级逻辑合约 CounterV2.sol a. 在 Counter 的基础上新增 decrement() 方法
  3. 最小实现的代理合约 Proxy.sol a. 使用 delegatecall 实现调用转发
    b. 支持 upgradeTo() 升级逻辑合约
  4. 初次部署脚本 Deploy.s.sol a. 部署 Counter 合约 b. 部署 Proxy 合约,初始化指向 Counter
  5. 升级部署脚本 DeployV2.s.sol a. 仅部署新的逻辑合约 CounterV2

2.1 Counter逻辑合约

路径:src/Counter.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
  uint256 public number;

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

  function increment() public {
    number++;
  }
}

2.2 升级后的逻辑合约

路径:src/CounterV2.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract CounterV2 {
  uint256 public number;

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

  function increment() public {
    number++;
  }

  // 相比原始逻辑合约,新增了decrement函数
  function decrement() public {
    number--;
  }
}

2.3 Proxy代理合约

路径:src/Proxy.sol 代理合约顾名思义,就是代表别人办事,它自己不做具体的事,而是将用户请求转发给真正干活的人,还要替换干活的人。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Proxy {
    // 存储逻辑合约地址的槽位,使用 keccak256 哈希生成唯一标识,并减去 1 以避免冲突
    bytes32 private constant LOGIC_SLOT = bytes32(uint256(keccak256("my.logic.address")) - 1);
    // 存储管理员地址的槽位,使用 keccak256 哈希生成唯一标识
    bytes32 private constant ADMIN_SLOT = keccak256("my.proxy.admin");

    // 构造函数,初始化管理员地址和逻辑合约地址
    constructor(address _logicAddr) {
        _setAdmin(msg.sender); // 设置部署合约的地址为管理员
        _setLogicAddr(_logicAddr); // 设置逻辑合约地址
    }

    // fallback 函数,用于将调用委托给逻辑合约
    fallback() external payable {
        _delegate(_getLogicAddr());
    }

    // receive 函数,用于接收以太币并将调用委托给逻辑合约
    receive() external payable {
        _delegate(_getLogicAddr());
    }

    // 升级逻辑合约地址的函数,仅管理员可调用
    function upgradeTo(address newLogicAddr) external {
        require(msg.sender == _getAdmin(), "Not admin"); // 确保调用者是管理员
        _setLogicAddr(newLogicAddr); // 更新逻辑合约地址
    }

    // 内部函数,将调用委托给指定的逻辑合约地址
    function _delegate(address _logicAddr) internal {
        assembly {
            // 将 calldata 复制到内存
            calldatacopy(0, 0, calldatasize())
            // 执行 delegatecall,将调用委托给逻辑合约
            let result := delegatecall(gas(), _logicAddr, 0, calldatasize(), 0, 0)
            // 将返回数据复制到内存
            returndatacopy(0, 0, returndatasize())
            // 根据调用结果处理返回或回退
            switch result
            case 0 {
                revert(0, returndatasize()) // 调用失败,回退交易
            }
            default {
                return(0, returndatasize()) // 调用成功,返回数据
            }
        }
    }

    // ==================== 内部存储操作函数 ====================

    // 获取逻辑合约地址
    function _getLogicAddr() internal view returns (address _logicAddr) {
        bytes32 slot = LOGIC_SLOT; // 获取逻辑合约地址的槽位
        assembly {
            _logicAddr := sload(slot) // 从槽位中加载地址
        }
    }

    // 设置逻辑合约地址
    function _setLogicAddr(address newLogicAddr) internal {
        bytes32 slot = LOGIC_SLOT; // 获取逻辑合约地址的槽位
        assembly {
            sstore(slot, newLogicAddr) // 将新地址存储到槽位中
        }
    }

    // 获取管理员地址
    function _getAdmin() internal view returns (address _admin) {
        bytes32 slot = ADMIN_SLOT; // 获取管理员地址的槽位
        assembly {
            _admin := sload(slot) // 从槽位中加载地址
        }
    }

    // 设置管理员地址
    function _setAdmin(address newAdmin) internal {
        bytes32 slot = ADMIN_SLOT; // 获取管理员地址的槽位
        assembly {
            sstore(slot, newAdmin) // 将新地址存储到槽位中
        }
    }
}

2.4 首次部署脚本

路径:script/Deploy.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "../src/Counter.sol";
import "../src/Proxy.sol";

contract DeployScript is Script {
    function run() external {
        // 开始广播交易
        vm.startBroadcast();

        // 部署逻辑合约 Counter,并输出其地址
        Counter logic = new Counter();
        console.log("Counter deployed at:", address(logic));

        // 部署代理合约 Proxy,并将逻辑合约地址传递给代理合约
        Proxy proxy = new Proxy(address(logic));
        console.log("Proxy deployed at:", address(proxy));

        // 停止广播交易
        vm.stopBroadcast();
    }
}

2.5 升级后部署脚本

路径:script/DeployV2.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

// 逻辑合约升级后的部署脚本
contract CounterScript is Script {
    function setUp() public {}

    function run() public {
        // 开始广播交易
        vm.startBroadcast();

        // 部署新的逻辑合约 CounterV2,并输出其地址
        CounterV2 logic= new CounterV2();
        console.log("CounterV2 deployed at:", address(logic));

        // 停止广播交易
        vm.stopBroadcast();
    }
}

3. 部署测试

image.png

大概测试流程:

  1. 本地网络:使用Anvil本地网络,其中提供了默认账户私钥;
  2. 部署:执行 首次部署脚本;其中是先部署Counter逻辑合约,然后再部署代理合约,并初始化指向逻辑合约;
  3. 测试:通过调用代理合约,测试逻辑合约中number的变化情况
  4. 升级:执行 升级后部署脚本;其中仅部署升级后的逻辑合约,然后调用代理合约的updateTo函数将地址更新为新逻辑合约地址
  5. 再测试:通过调用代理合约,测试新逻辑合约中decrement函数,以及number的变化情况;

3.1 启动本地网络

我们默认使用第一个账户作为合约管理员,也是部署者;

管理员的账户:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

管理员的私钥:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

$ anvil
                             _   _
                            (_) | |
      __ _   _ __   __   __  _  | |
     / _` | | '_ \  \ \ / / | | | |
    | (_| | | | | |  \ V /  | | | |
     \__,_| |_| |_|   \_/   |_| |_|

    1.0.0-stable (e144b82070 2025-02-13T20:02:16.393821500Z)
    https://github.com/foundry-rs/foundry

默认账户
Available Accounts
==================
(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000.000000000000000000 ETH)
(1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000.000000000000000000 ETH)
(2) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000.000000000000000000 ETH)
(3) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000.000000000000000000 ETH)
(4) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000.000000000000000000 ETH)
(5) 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000.000000000000000000 ETH)
(6) 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000.000000000000000000 ETH)
(7) 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000.000000000000000000 ETH)
(8) 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000.000000000000000000 ETH)
(9) 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000.000000000000000000 ETH)

默认私钥,和上面的账户一一对应
Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
(2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
(3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
(4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
(5) 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
(6) 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
(7) 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
(8) 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
(9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

默认钱包,助记词,
Wallet
==================
Mnemonic:          test test test test test test test test test test test junk
Derivation path:   m/44'/60'/0'/0/

链id
Chain ID
==================
31337

Base Fee
==================
1000000000

Gas Limit
==================
30000000

Genesis Timestamp
==================
1745993379

# 本地节点的url
Listening on 127.0.0.1:8545

3.2 首次部署

先部署Counter合约,再部署Proxy代理合约,部署结果如下:

  • 获取Counter逻辑合约地址:0x5FbDB2315678afecb367f032d93F642f64180aa3
  • 获取Proxy代理合约地址:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
forge script script/Deploy.s.sol \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast 

# 结果
[⠊] Compiling...
[⠑] Compiling 2 files with Solc 0.8.28
[⠘] Solc 0.8.28 finished in 636.64ms
Compiler run successful!
Script ran successfully.

== Logs ==
  Counter deployed at: 0x5FbDB2315678afecb367f032d93F642f64180aa3     # 逻辑合约地址
  Proxy deployed at: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512       # 代理合约地址
## Setting up 1 EVM.

==========================
Chain 31337
Estimated gas price: 2.000000001 gwei
Estimated total gas used for script: 570415
Estimated amount required: 0.001140830000570415 ETH
==========================                      
##### anvil-hardhat                                                                                                                                                                                                                
✅  [Success] Hash: 0xefdac91645c9c6c41794a27dc8a54eafa39efba4d378348b9027dc36b66522da                                                                                                                                             
Contract Address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512                                                                                                                                                                       
Block: 1
Paid: 0.000281969000281969 ETH (281969 gas * 1.000000001 gwei)                                                                                                                                                            
##### anvil-hardhat                                                                                                                                                                                                                
✅  [Success] Hash: 0x6795deaad7fd483eda4b16af7d8b871c7f6e49beb50709ce1cf0ca81c29247d1 
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.000156813000156813 ETH (156813 gas * 1.000000001 gwei)
✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000438782000438782 ETH (438782 gas * avg 1.000000001 gwei)     
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: D:/dapp/counter_proxy\broadcast\Deploy.s.sol\31337\run-latest.json
Sensitive values saved to: D:/dapp/counter_proxy/cache\Deploy.s.sol\31337\run-latest.json

3.3 测试逻辑

Proxy代理合约地址:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

管理员的私钥:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

通过调用 Proxy 合约,验证 setNumber(),是否正确反映到 number 的状态上

  1. 调用setnumber函数
$ cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "setNumber(uint256)" 42 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://localhost:8545

# 结果
blockHash            0x917a7e48b8eb7ce00c5408388280d796be471a0b3b06b2c5440b9af948d78380
blockNumber          5
contractAddress
cumulativeGasUsed    31558
effectiveGasPrice    589950953
from                 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed              31558
logs                 []
logsBloom            0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status               1 (success)
transactionHash      0x3e254f6380114e50555335c6bc465b7650561833e2453f78a32e422a7f2547e3
transactionIndex     0
type                 2
blobGasPrice         1
blobGasUsed
authorizationList
to                   0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  1. 查看number
$ cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "number()(uint256)" \
--rpc-url http://localhost:8545

# 结果
42

3.4 升级合约

先部署升级后的逻辑合约,再调用代理的upgradeTo替换最新的逻辑合约地址;

  1. 部署升级后的逻辑合约
    • 获取升级后的逻辑合约地址:0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
$ forge script script/DeployV2.s.sol \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast

# 结果
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Logs ==
  CounterV2 deployed at: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9  # 新逻辑合约地址
## Setting up 1 EVM.
=========================
Chain 31337
Estimated gas price: 1.537751179 gwei
Estimated total gas used for script: 227436
Estimated amount required: 0.000349739977147044 ETH
==========================                
##### anvil-hardhat
✅  [Success] Hash: 0x5e9711bd8959e49aaa6bc939b61f210ca2aa52c9657482475f06efc707810df8           
Contract Address: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Block: 4
Paid: 0.000117755816836644 ETH (174951 gas * 0.673078844 gwei)
✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000117755816836644 ETH (174951 gas * avg 0.673078844 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: D:/dapp/counter_proxy/broadcast\DeployV2.s.sol\31337\run-latest.json
Sensitive values saved to: D:/dapp/counter_proxy/cache\DeployV2.s.sol\31337\run-latest.json
  1. 代理合约更新最新逻辑合约地址
$ cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 \
"upgradeTo(address)" 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://localhost:8545

# 结果
blockHash            0xa86f77d7812a7d68ac64e71567d042c71fb8d6222c1e435ee05c69dbe83e44fb
blockNumber          3
contractAddress
cumulativeGasUsed    29079
effectiveGasPrice    770105469
from                 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed              29079
logs                 []
logsBloom            0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status               1 (success)
transactionHash      0x5c5d337f243256a3022821cbf7cf7110986e5094ea88f2139ba9bbe54ffba3f8
transactionIndex     0
type                 2
blobGasPrice         1
blobGasUsed
authorizationList
to                   0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

3.5 再测试逻辑

Proxy代理合约地址:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

管理员的私钥:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

逻辑合约升级后,通用通过调用 Proxy 合约,验证新增的 decrement() 函数是否生效,原方法是否仍然有效

  1. 调用decrement()函数
$ cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "decrement()" \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://localhost:8545

# 结果
blockHash            0x3315b2008c6ce5e86df2645b5b37c89f32ae078468067a4d9c4fae471f8c266e
blockNumber          6
contractAddress
cumulativeGasUsed    31274
effectiveGasPrice    516362232
from                 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed              31274
logs                 []
logsBloom            0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status               1 (success)
transactionHash      0xdc5ee939e839329efafd3ffaa2a251ff9089ac2d2952fd7834fe8aab34aea593
transactionIndex     0
type                 2
blobGasPrice         1
blobGasUsed
authorizationList
to                   0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  1. 查看number变化
$ cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "number()(uint256)" \
--rpc-url http://localhost:8545

# 结果;原来是42,decrement后变为41
41

4. 总结

在本文中,我们手把手实践了合约部署到升级的全过程,已经对合约的升级有了操作上的理解,方便你后续深入思考和学习;

你可以继续思考下:如何使用OpenZeppelin来实现代码?为什么使用delegatecall来实现合约升级?什么是共享存储槽?

参考链接

https://learnblockchain.cn/article/11224

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xwu
0xwu
0x919C...8e48
hello