使用 Foundry + 本地链(Anvil)来演示 Counter 合约,使用透明代理的升级过程,完全可复刻实际运行,让你对透明代理升级有个清晰的概念。
为什么需要透明代理? 在智能合约的开发中,一旦合约部署到链上,它的代码即永久不可更改。但在实际项目中,我们常常需要修复漏洞、添加功能或调整逻辑,这时就需要「可升级合约」架构。而代理模式(Proxy Pattern),就是实现可升级合约的核心技术之一。
其中,透明代理(Transparent Proxy)是目前最广泛使用的升级模式,由 OpenZeppelin 等主流框架推广使用,具有明确的访问隔离机制和安全边界。
透明代理结构由两部分组成:
本质上,用户调用的是 Proxy,但执行的是 Logic 的代码,通过 delegatecall
保持存储不变。
为什么选择透明代理?
透明代理模式相比其它升级方式(如 UUPS、Beacon 等),有以下优势:
透明代理的使用场景:
Counter.sol
a. 定义了一个 number状态变量
b. 提供 setNumber() 和 increment() 两个函数CounterV2.sol
a. 在 Counter 的基础上新增 decrement() 方法 Proxy.sol
a. 使用 delegatecall 实现调用转发Deploy.s.sol
a. 部署 Counter 合约
b. 部署 Proxy 合约,初始化指向 CounterDeployV2.s.sol
a. 仅部署新的逻辑合约 CounterV2路径: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++;
}
}
路径: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--;
}
}
路径: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) // 将新地址存储到槽位中
}
}
}
路径: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();
}
}
路径: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();
}
}
大概测试流程:
我们默认使用第一个账户作为合约管理员,也是部署者;
管理员的账户: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
先部署Counter合约,再部署Proxy代理合约,部署结果如下:
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
Proxy代理合约地址:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
管理员的私钥:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
通过调用 Proxy 合约,验证 setNumber(),是否正确反映到 number 的状态上
$ 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
$ cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "number()(uint256)" \
--rpc-url http://localhost:8545
# 结果
42
先部署升级后的逻辑合约,再调用代理的upgradeTo替换最新的逻辑合约地址;
$ 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
$ 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
Proxy代理合约地址:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
管理员的私钥:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
逻辑合约升级后,通用通过调用 Proxy 合约,验证新增的 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
$ cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "number()(uint256)" \
--rpc-url http://localhost:8545
# 结果;原来是42,decrement后变为41
41
在本文中,我们手把手实践了合约部署到升级的全过程,已经对合约的升级有了操作上的理解,方便你后续深入思考和学习;
你可以继续思考下:如何使用OpenZeppelin来实现代码?为什么使用delegatecall来实现合约升级?什么是共享存储槽?
参考链接
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!