EIP Standards

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 实现示例,包含消息验证功能。

EIP712Example.sol
// 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 Integration (TypeScript)
// 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 的代币授权,极大改善了用户体验。

特性传统 ApprovePermit
交易数量2 笔交易1 笔交易
Gas 成本高(2 笔交易)低(1 笔交易)
需要 ETH否(可用 relayer)
用户体验差(等待 2 次确认)好(只需签名)
安全性中(常设无限授权)高(精确额度+过期时间)

完整的 Permit Token 实现

MyPermitToken.sol
// 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);
    }
}

前端完整集成示例

Permit Signing (TypeScript)
// 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 attacks

EIP-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

完整的代理合约实现

ERC1967Proxy.sol
// 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

ImplementationV1.sol
// 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(升级)

ImplementationV2.sol
// 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 的区别

相关资源