EIP-712、EIP-2612、EIP-1967 深度教程
深入理解三个重要的以太坊改进提案:结构化数据签名、无 gas 授权和代理存储槽标准
Part 1: EIP-712 - 结构化数据签名
EIP-712 定义了一种标准化的方式来签名和验证结构化数据,让用户能够清楚地看到他们正在签名的内容,同时提供强大的安全保护。
问题的起源
在 EIP-712 出现之前,以太坊应用通常使用原始字符串签名。这种方式存在严重的安全和用户体验问题。
- 用户看不懂签名内容 - 钱包显示的是一堆十六进制数据
- 不安全 - 恶意网站可以欺骗用户签名任何内容
- 无结构验证 - 无法验证签名的具体字段
- 跨链问题 - 没有域分离(domain separation),存在重放攻击风险
EIP-712 的解决方案
EIP-712 引入了结构化的数据签名方式,通过定义域(domain)、类型(types)和消息(message)来确保签名的安全性和可读性。
- ✅ 钱包可以格式化显示每个字段,用户看得懂
- ✅ 域分离 - 签名绑定到特定合约和链,防止重放
- ✅ 类型安全 - 每个字段都有明确类型
- ✅ 防重放攻击 - 包含 chainId 和 verifyingContract
核心概念详解
1. Domain Separator (域分离符)
域分离符用于确保签名只能在特定的合约和链上使用,防止签名在不同场景下被重放。
struct EIP712Domain {
string name; // dApp or protocol name
string version; // Current version
uint256 chainId; // Chain ID (1 = Mainnet, 137 = Polygon)
address verifyingContract; // Contract that verifies the signature
}
// Domain Separator calculation
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
address(this)
));2. Type Hash (类型哈希)
类型哈希定义了数据结构的签名格式,必须按字母顺序排列字段。
struct Mail {
address from;
address to;
string contents;
}
// Type Hash
bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(address from,address to,string contents)"
);3. Struct Hash (结构哈希)
结构哈希是对具体数据的哈希。注意:动态类型(string、bytes、array)必须先进行哈希。
function hashMail(Mail memory mail) internal pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(bytes(mail.contents)) // Dynamic types must be hashed first
));
}4. 最终签名
将域分离符和结构哈希组合,加上 EIP-191 前缀,生成最终的签名摘要。
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", // EIP-191 prefix
DOMAIN_SEPARATOR,
structHash
));
address signer = ecrecover(digest, v, r, s);完整实现示例
下面是一个完整的 EIP-712 实现示例,包含消息验证功能。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title EIP712Example
* @notice Complete EIP-712 implementation example
* @dev Demonstrates structured data signing and verification
*/
contract EIP712Example {
// ═══════════════════════ EIP-712 TYPE HASHES ═══════════════════════
/// @dev Type hash for EIP712Domain
bytes32 private constant DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
/// @dev Type hash for Message struct
bytes32 private constant MESSAGE_TYPEHASH = keccak256(
"Message(address from,address to,string content,uint256 nonce)"
);
// ═══════════════════════ STORAGE ═══════════════════════
/// @dev Domain separator (cached)
bytes32 private immutable _DOMAIN_SEPARATOR;
/// @dev Nonce for each address (prevents replay attacks)
mapping(address => uint256) public nonces;
// ═══════════════════════ STRUCTS ═══════════════════════
/// @dev Message structure for typed data
struct Message {
address from;
address to;
string content;
uint256 nonce;
}
// ═══════════════════════ EVENTS ═══════════════════════
event MessageVerified(
address indexed from,
address indexed to,
string content,
uint256 nonce
);
// ═══════════════════════ CONSTRUCTOR ═══════════════════════
constructor() {
_DOMAIN_SEPARATOR = keccak256(abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes("EIP712Example")),
keccak256(bytes("1")),
block.chainid,
address(this)
));
}
// ═══════════════════════ EXTERNAL FUNCTIONS ═══════════════════════
/**
* @notice Verify a signed message
* @param message The message struct
* @param v Recovery identifier
* @param r ECDSA signature parameter
* @param s ECDSA signature parameter
* @return True if signature is valid
*/
function verifyMessage(
Message memory message,
uint8 v,
bytes32 r,
bytes32 s
) external returns (bool) {
// 1. Check nonce
require(message.nonce == nonces[message.from], "Invalid nonce");
// 2. Compute struct hash
bytes32 structHash = _hashMessage(message);
// 3. Compute digest
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
_DOMAIN_SEPARATOR,
structHash
));
// 4. Recover signer
address signer = ecrecover(digest, v, r, s);
require(signer != address(0), "Invalid signature");
require(signer == message.from, "Signer mismatch");
// 5. Increment nonce (prevent replay)
nonces[message.from]++;
emit MessageVerified(message.from, message.to, message.content, message.nonce);
return true;
}
/**
* @notice Get the domain separator
* @return Domain separator
*/
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _DOMAIN_SEPARATOR;
}
// ═══════════════════════ INTERNAL FUNCTIONS ═══════════════════════
/**
* @dev Hash a Message struct according to EIP-712
* @param message The message to hash
* @return Hash of the message
*/
function _hashMessage(Message memory message) private pure returns (bytes32) {
return keccak256(abi.encode(
MESSAGE_TYPEHASH,
message.from,
message.to,
keccak256(bytes(message.content)), // Dynamic types must be hashed
message.nonce
));
}
}前端集成
使用 ethers.js v6 在前端生成和验证 EIP-712 签名。
// Frontend (ethers.js v6)
const domain = {
name: "EIP712Example",
version: "1",
chainId: 1,
verifyingContract: "0x..."
};
const types = {
Message: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "content", type: "string" },
{ name: "nonce", type: "uint256" }
]
};
const message = {
from: "0xAlice...",
to: "0xBob...",
content: "Hello, Bob!",
nonce: 0
};
// Sign
const signature = await signer.signTypedData(domain, types, message);
// Parse signature
const sig = ethers.Signature.from(signature);
const { v, r, s } = sig;
// Verify on contract
await contract.verifyMessage(message, v, r, s);Part 2: EIP-2612 - Permit (无 gas 授权)
EIP-2612 扩展了 ERC20 标准,通过离线签名实现无 gas 的代币授权,极大改善了用户体验。
| 特性 | 传统 Approve | Permit |
|---|---|---|
| 交易数量 | 2 笔交易 | 1 笔交易 |
| Gas 成本 | 高(2 笔交易) | 低(1 笔交易) |
| 需要 ETH | 是 | 否(可用 relayer) |
| 用户体验 | 差(等待 2 次确认) | 好(只需签名) |
| 安全性 | 中(常设无限授权) | 高(精确额度+过期时间) |
完整的 Permit Token 实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title MyPermitToken
* @notice ERC20 token with EIP-2612 Permit functionality
* @dev Implements gasless approvals via off-chain signatures
*/
contract MyPermitToken {
// ═══════════════════════ ERC20 STATE ═══════════════════════
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
// ═══════════════════════ EIP-712 STATE ═══════════════════════
bytes32 private constant DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 private constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
bytes32 private immutable _DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
// ═══════════════════════ EVENTS ═══════════════════════
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// ═══════════════════════ CONSTRUCTOR ═══════════════════════
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
// Compute domain separator
_DOMAIN_SEPARATOR = keccak256(abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes(_name)),
keccak256(bytes("1")),
block.chainid,
address(this)
));
// Mint initial supply to deployer
_mint(msg.sender, 1_000_000 * 10**decimals);
}
// ═══════════════════════ ERC20 FUNCTIONS ═══════════════════════
function transfer(address to, uint256 amount) external returns (bool) {
return _transfer(msg.sender, to, amount);
}
function approve(address spender, uint256 amount) external returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
uint256 currentAllowance = allowance[from][msg.sender];
require(currentAllowance >= amount, "Insufficient allowance");
if (currentAllowance != type(uint256).max) {
allowance[from][msg.sender] = currentAllowance - amount;
}
return _transfer(from, to, amount);
}
// ═══════════════════════ EIP-2612 PERMIT ═══════════════════════
/**
* @notice Approve spending via signature (EIP-2612)
* @param owner Token owner
* @param spender Address to approve
* @param value Amount to approve
* @param deadline Signature expiration timestamp
* @param v ECDSA recovery id
* @param r ECDSA signature parameter
* @param s ECDSA signature parameter
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
bytes32 structHash = keccak256(abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonces[owner]++, // Use and increment nonce
deadline
));
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
_DOMAIN_SEPARATOR,
structHash
));
address signer = ecrecover(digest, v, r, s);
require(signer != address(0), "ERC20Permit: invalid signature");
require(signer == owner, "ERC20Permit: unauthorized");
_approve(owner, spender, value);
}
/**
* @notice Get the domain separator
* @return Domain separator for EIP-712
*/
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _DOMAIN_SEPARATOR;
}
// ═══════════════════════ INTERNAL FUNCTIONS ═══════════════════════
function _transfer(
address from,
address to,
uint256 amount
) internal returns (bool) {
require(from != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(balanceOf[from] >= amount, "Insufficient balance");
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
function _approve(
address owner,
address spender,
uint256 amount
) internal {
require(owner != address(0), "Approve from zero address");
require(spender != address(0), "Approve to zero address");
allowance[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _mint(address to, uint256 amount) internal {
require(to != address(0), "Mint to zero address");
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
}前端完整集成示例
// Complete Permit signing flow
const name = await token.name();
const nonce = await token.nonces(ownerAddress);
const chainId = await signer.getChainId();
const domain = {
name: name,
version: "1",
chainId: chainId,
verifyingContract: tokenAddress
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour
const message = {
owner: ownerAddress,
spender: spenderAddress,
value: ethers.parseUnits("100", 18),
nonce: nonce,
deadline: deadline
};
// Sign
const signature = await signer.signTypedData(domain, types, message);
const sig = ethers.Signature.from(signature);
// Call permit
await token.permit(
message.owner,
message.spender,
message.value,
message.deadline,
sig.v, sig.r, sig.s
);实际应用场景
场景 1: DEX 集成
用户可以在一笔交易中完成授权和交易,无需预先 approve。
function swapWithPermit(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external {
// Execute permit
IERC20Permit(tokenIn).permit(
msg.sender, address(this), amountIn, deadline, v, r, s
);
// Execute swap
_swap(tokenIn, tokenOut, amountIn);
}💡 Real-world example: Uniswap V2/V3 routers support Permit, allowing users to swap tokens in a single transaction without pre-approval.
Part 3: EIP-1967 - 代理存储槽标准
EIP-1967 定义了可升级代理合约的标准存储槽位,解决了存储冲突问题,使得合约升级变得安全可靠。
为什么需要可升级合约?
智能合约部署后无法修改代码。代理模式通过分离数据存储和业务逻辑,实现了合约的可升级性。
┌─────────────────┐
│ Proxy Contract │ ← User interaction, address never changes
│ (stores data) │
└────────┬─────────┘
│ delegatecall
▼
┌─────────────────┐
│ Implementation │ ← Business logic, can be upgraded
│ (logic only) │
└─────────────────┘delegatecall 详解
delegatecall 是代理模式的核心机制。它在当前合约的上下文中执行另一个合约的代码。
// call vs delegatecall
// ═══ call ═══
ContractA.call(data);
// - Executes in ContractA's context
// - msg.sender = caller
// - Modifies ContractA's storage
// ═══ delegatecall ═══
ContractA.delegatecall(data);
// - Executes in current contract's context
// - msg.sender = original caller
// - Modifies current contract's storage存储冲突问题
如果代理合约和实现合约使用相同的存储槽位,会导致数据被意外覆盖。
❌ 传统代理的危险
// Dangerous: Storage collision
contract Proxy {
address public implementation; // slot 0
// ... user data from slot 1
}
contract ImplementationV1 {
uint256 public value; // slot 0 ← COLLISION!
}
// Result: Implementation writes to slot 0
// overwrites the implementation address!✅ EIP-1967 的解决方案
// EIP-1967: Use random storage slots
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
// = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
// Why safe?
// - Random slot (extremely low collision probability)
// - Standardized (all tools recognize it)
// - -1 prevents preimage attacksEIP-1967 标准存储槽位
EIP-1967 定义了三个标准化的随机存储槽位,避免与业务数据冲突。
// EIP-1967 Standard Storage Slots
// 1. Implementation slot
bytes32 constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
// = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
// 2. Admin slot
bytes32 constant ADMIN_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);
// = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
// 3. Beacon slot
bytes32 constant BEACON_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1);
// = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50完整的代理合约实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ERC1967Proxy
* @notice Upgradeable proxy following EIP-1967 standard
* @dev Uses standard storage slots to prevent storage collision
*/
contract ERC1967Proxy {
// ═══════════════════════ EIP-1967 STORAGE SLOTS ═══════════════════════
/**
* @dev Storage slot for implementation address
* bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
*/
bytes32 private constant IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
/**
* @dev Storage slot for admin address
* bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
*/
bytes32 private constant ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
// ═══════════════════════ EVENTS ═══════════════════════
event Upgraded(address indexed implementation);
event AdminChanged(address previousAdmin, address newAdmin);
// ═══════════════════════ CONSTRUCTOR ═══════════════════════
/**
* @notice Initialize the proxy
* @param _logic Initial implementation contract address
* @param _admin Admin address who can upgrade the proxy
*/
constructor(address _logic, address _admin) {
_setImplementation(_logic);
_setAdmin(_admin);
}
// ═══════════════════════ FALLBACK ═══════════════════════
/**
* @dev Fallback function that delegates calls to the implementation
* Will run if no other function matches the call data
*/
fallback() external payable {
_delegate(_getImplementation());
}
/**
* @dev Allows contract to receive ETH
*/
receive() external payable {}
// ═══════════════════════ DELEGATION ═══════════════════════
/**
* @dev Delegates the current call to implementation contract
* @param implementation Address to delegate to
*/
function _delegate(address implementation) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(
gas(),
implementation,
0,
calldatasize(),
0,
0
)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// ═══════════════════════ IMPLEMENTATION MANAGEMENT ═══════════════════════
function _getImplementation() internal view returns (address implementation) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
implementation := sload(slot)
}
}
function _setImplementation(address newImplementation) private {
require(newImplementation.code.length > 0, "Implementation is not a contract");
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
sstore(slot, newImplementation)
}
emit Upgraded(newImplementation);
}
function upgradeTo(address newImplementation) external {
require(msg.sender == _getAdmin(), "Only admin can upgrade");
_setImplementation(newImplementation);
}
// ═══════════════════════ ADMIN MANAGEMENT ═══════════════════════
function _getAdmin() internal view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly {
admin := sload(slot)
}
}
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "Admin cannot be zero address");
address previousAdmin = _getAdmin();
bytes32 slot = ADMIN_SLOT;
assembly {
sstore(slot, newAdmin)
}
emit AdminChanged(previousAdmin, newAdmin);
}
function changeAdmin(address newAdmin) external {
require(msg.sender == _getAdmin(), "Only admin can change admin");
_setAdmin(newAdmin);
}
// ═══════════════════════ VIEW FUNCTIONS ═══════════════════════
function implementation() external view returns (address) {
return _getImplementation();
}
function admin() external view returns (address) {
return _getAdmin();
}
}实现合约设计
实现合约必须保持存储布局的兼容性。升级时只能在末尾添加新变量,不能改变已有变量。
版本 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ImplementationV1
* @notice First version of the implementation contract
* @dev To be used with ERC1967Proxy
*/
contract ImplementationV1 {
// ⚠️ IMPORTANT: Storage layout must remain consistent across upgrades
uint256 public value; // slot 0
address public owner; // slot 1
bool private initialized; // slot 2
event ValueChanged(uint256 oldValue, uint256 newValue);
event Initialized(address owner);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier notInitialized() {
require(!initialized, "Already initialized");
_;
}
function initialize(address _owner) external notInitialized {
require(_owner != address(0), "Owner cannot be zero address");
initialized = true;
owner = _owner;
emit Initialized(_owner);
}
function setValue(uint256 _value) external onlyOwner {
uint256 oldValue = value;
value = _value;
emit ValueChanged(oldValue, _value);
}
function getValue() external view returns (uint256) {
return value;
}
function version() external pure returns (string memory) {
return "V1";
}
function isInitialized() external view returns (bool) {
return initialized;
}
}版本 2(升级)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ImplementationV2
* @notice Upgraded version of the implementation contract
* @dev To be used with ERC1967Proxy after upgrade
*/
contract ImplementationV2 {
// ⚠️ IMPORTANT: Storage layout must match V1!
// ═══════════════════════ STORAGE (V1) ═══════════════════════
uint256 public value; // slot 0 - MUST NOT CHANGE
address public owner; // slot 1 - MUST NOT CHANGE
bool private initialized; // slot 2 - MUST NOT CHANGE
// ═══════════════════════ NEW STORAGE (V2) ═══════════════════════
uint256 public multiplier; // slot 3 - NEW in V2
uint256 public counter; // slot 4 - NEW in V2
event ValueChanged(uint256 oldValue, uint256 newValue);
event Initialized(address owner);
event MultiplierChanged(uint256 oldMultiplier, uint256 newMultiplier);
event CounterIncremented(uint256 newCounter);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier notInitialized() {
require(!initialized, "Already initialized");
_;
}
function initialize(address _owner) external notInitialized {
require(_owner != address(0), "Owner cannot be zero address");
initialized = true;
owner = _owner;
emit Initialized(_owner);
}
function initializeV2(uint256 _multiplier) external onlyOwner {
require(counter == 0, "V2 already initialized");
require(_multiplier > 0, "Multiplier must be positive");
multiplier = _multiplier;
counter = 1;
}
// V1 function (UPGRADED: now uses multiplier)
function setValue(uint256 _value) external onlyOwner {
uint256 oldValue = value;
if (multiplier > 0) {
value = _value * multiplier;
} else {
value = _value;
}
emit ValueChanged(oldValue, value);
}
function getValue() external view returns (uint256) {
return value;
}
function version() external pure returns (string memory) {
return "V2";
}
function isInitialized() external view returns (bool) {
return initialized;
}
// NEW V2 FUNCTIONS
function setMultiplier(uint256 _multiplier) external onlyOwner {
require(_multiplier > 0, "Multiplier must be positive");
uint256 oldMultiplier = multiplier;
multiplier = _multiplier;
emit MultiplierChanged(oldMultiplier, _multiplier);
}
function incrementCounter() external {
counter++;
emit CounterIncremented(counter);
}
function getMultiplier() external view returns (uint256) {
return multiplier;
}
function getCounter() external view returns (uint256) {
return counter;
}
}存储布局规则
- ✅ 可以在末尾添加新变量
- ❌ 不能改变已有变量的顺序
- ❌ 不能改变已有变量的类型
- ❌ 不能删除已有变量
Initializer 模式
由于代理模式不能使用 constructor,必须使用 initialize 函数来初始化合约状态。
❌ 为什么不能用 Constructor?
// Constructor only runs on Implementation deployment
// Proxy doesn't see these initializations!
constructor(address _owner) {
owner = _owner; // Only initializes Implementation's storage
}✅ 使用 Initialize 函数
// Use initialize function instead
contract Implementation {
address public owner;
bool private initialized;
function initialize(address _owner) public {
require(!initialized, "Already initialized");
initialized = true;
owner = _owner;
}
}透明代理 vs UUPS
透明代理 (Transparent Proxy)
代理合约根据调用者是否为 admin 来决定执行升级逻辑还是委托调用。
// Transparent Proxy
fallback() external payable {
if (msg.sender == admin()) {
// Admin: Execute proxy functions (upgradeTo)
} else {
// Users: Delegate to implementation
_delegate(implementation());
}
}
// Problem: Every call checks msg.sender (wastes gas)UUPS (Universal Upgradeable Proxy Standard)
升级逻辑放在实现合约中,代理合约更简单,gas 更便宜。
// UUPS Proxy (simpler)
fallback() external payable {
_delegate(implementation()); // Always delegate
}
// Upgrade logic in Implementation
contract ImplementationV1 is UUPSUpgradeable {
function upgradeTo(address newImplementation) public onlyOwner {
// Upgrade logic here
}
}
// Advantages:
// - Simpler proxy, cheaper gas
// - More flexible
// Risk:
// - If new implementation forgets upgradeTo, can't upgrade anymore| 特性 | 透明代理 | UUPS |
|---|---|---|
| Gas 成本 | 高(每次检查 admin) | 低 |
| 升级逻辑位置 | 在 Proxy 中 | 在 Implementation 中 |
| 代理大小 | 大 | 小 |
| 风险 | 低 | 中(可能丢失升级能力) |
安全考虑
1. 时间锁 (Timelock)
在升级生效前设置等待期,给用户时间审查升级内容。
contract TimelockProxy {
uint256 public constant DELAY = 2 days;
address public pendingImplementation;
uint256 public upgradeTime;
function proposeUpgrade(address newImpl) external onlyAdmin {
pendingImplementation = newImpl;
upgradeTime = block.timestamp + DELAY;
}
function executeUpgrade() external onlyAdmin {
require(block.timestamp >= upgradeTime, "Too early");
_upgradeTo(pendingImplementation);
}
}2. 多签控制
要求多个签名者同意才能执行升级,降低单点风险。
// Requires 3/5 multisig to upgrade
contract MultiSigProxy {
mapping(address => bool) public signers;
mapping(bytes32 => uint256) public approvals;
function approveUpgrade(address newImpl) external {
require(signers[msg.sender], "Not signer");
approvals[keccak256(abi.encode(newImpl))]++;
}
function executeUpgrade(address newImpl) external {
require(approvals[keccak256(abi.encode(newImpl))] >= 3, "Need 3 approvals");
_upgradeTo(newImpl);
}
}常见陷阱
- ⚠️ 不要在实现合约中使用 selfdestruct - 会删除代码,代理将无法工作
- ⚠️ 不要 delegatecall 到不可信合约 - 可能修改任意存储
- ⚠️ 不要在 constructor 中初始化 - 使用 initialize 函数
- ⚠️ 升级时保持存储布局兼容 - 否则会导致数据错乱
学习成果总结
完成本教程后,你应该掌握以下内容:
EIP-712
- 理解结构化签名的工作原理
- 能够实现 EIP-712 签名验证
- 掌握前端 EIP-712 签名生成
- 理解 Domain Separator 和 Type Hash
EIP-2612
- 理解 Permit 的工作原理和优势
- 能够为 ERC20 代币添加 Permit 功能
- 掌握在 dApp 中集成 Permit
- 理解元交易的基本概念
EIP-1967
- 理解可升级合约的工作原理
- 能够实现基本的代理模式
- 理解存储布局兼容性
- 掌握安全升级的最佳实践
- 理解透明代理和 UUPS 的区别