# 合约代理模式 **Published by:** [cyptoJune](https://paragraph.com/@cyptojune/) **Published on:** 2024-01-04 **URL:** https://paragraph.com/@cyptojune/SCJ8ogxuerun7y5iPwuT ## Content 概述 合约部署在链上之后,代码是不可变的,这样做既有优点也是缺点 优点:安全,用户知道合约内容,会知道发生什么;(合约开源) 缺点:如果合约中有Bug,既不能修改也不能升级,只能部署新合约,但是新合约和旧合约的地址不一样,而且合约的数据也需要花费大量的gas进行迁移,成本相对较大! 那么有没有办法合约部署之后进行修改和升级呢,答案就是代理模式。 原理 代理模式将合约数据和逻辑分开,分别保存在不同的合约中。以下的图可以看出,数据(状态变量)保存在代理合约中,而逻辑函数保存在逻辑合约中。代理合约通过delegatecall,将函数调用全权委托给逻辑合约(Implementation)执行,再把最终的结果返回给调用者(caller) 优势 可升级:当我们需要升级合约的逻辑时,只需要将代理合约指向新的逻辑合约 省gas: 如果多个合约复用一套逻辑,我们只需要部署一个逻辑合约,然后在部署多个只保存数据的代理合约,指向逻辑合约。 代码演示 // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; // 代理合约 contract Proxy { address public implementation; // 逻辑合约地址 /* @ 初始化逻辑合约地址 */ constructor(address implementation_) { implementation = implementation_; } fallback() external payable { address _implementation = implementation; assembly { // 将msg.data拷贝到内存里 // calldatacopy操作码的参数: 内存起始位置,calldata起始位置,calldata长度 calldatacopy(0, 0, calldatasize()) // 利用delegatecall调用implementation合约 // delegatecall操作码的参数:gas, 目标合约地址,input mem起始位置,input mem长度,output area mem起始位置,output area mem长度 // output area起始位置和长度位置,所以设为0 // delegatecall成功返回1,失败返回0 let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) // 将return data拷贝到内存 // returndata操作码的参数:内存起始位置,returndata起始位置,returndata长度 returndatacopy(0, 0, returndatasize()) switch result // 如果delegate call失a败,revert case 0 { revert(0, returndatasize()) } // 如果delegate call成功,返回mem起始位置为0,长度为returndatasize()的数据(格式为bytes) default { return(0, returndatasize()) } } } } // 逻辑合约 contract Logic { address public implementation; // 与Proxy保持一致,防止插槽冲突 uint public x = 99; event CallSuccess(); // 调用成功事件 // 这个函数会释放CallSuccess事件并返回一个uint。 // 函数selector: 0xd09de08a function increment() external returns(uint) { emit CallSuccess(); return x + 1; } } contract Caller { // 代理合约的地址 address public proxy; // 构造函数,在合约部署的时候初始化proxy变量 constructor(address proxy_) { proxy = proxy_; } // 利用abi.encodeWithSignature()获取了increment()函数的selector function increment() external returns(uint) { ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()")); return abi.decode(data,(uint)); } } 代理合约Proxy: 使用了fallback函数,fallback()函数会在调用合约不存在的函数时被触发。可用于接收eth,也可以用于代理合约proxy contract. fallback()声明的时不需要function关键字,必须由exteral()函数。其中fallback用了内联汇编,其中操作码如下: calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t。 delegatecall(g, a, in, insize, out, outsize):调用地址a的合约,输入为mem[in..(in+insize)) ,输出为mem[out..(out+outsize)), 提供gwei的以太坊gas。这个操作码在错误时返回0,在成功时返回1。 returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t。 switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。 return(p, s):终止函数执行, 返回数据mem[p..(p+s))。 revert(p, s):终止函数执行, 回滚状态,返回数据mem[p..(p+s))。 逻辑合约Logic:increment()函数会被Proxy合约调用,释放CallSuccess事件,并返回一个uint.它的函数选择器是0xd09de08a。 调用者合约Caller: • increase():利用call来调用代理合约的increment()函数,并返回一个uint。在调用时,我们利用abi.encodeWithSignature()获取了increment()函数的selector。在返回时,利用abi.decode()将返回值解码为uint类型。 总结 代理合约利用delegatecall将函数调用委托给了另一个逻辑合约,使得数据和逻辑分别由不同合约负责。并且,它利用内联汇编黑魔法,让没有返回值的回调函数也可以返回数据。 参考 WTF-Solidity 学院代理合约部分 https://github.com/AmazingAng/WTF-Solidity/blob/main/46_ProxyContract/readme.md openzeppelin proxy代理部分 ## Publication Information - [cyptoJune](https://paragraph.com/@cyptojune/): Publication homepage - [All Posts](https://paragraph.com/@cyptojune/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@cyptojune): Subscribe to updates