Skip to main content

Week 17 - 10.31 代理模式大阅兵

Day1 - EIP1967

之前讲到智能合约代理模式核心就是利用delegatcall来将合约调用从代理合约转发到实现合约。

而基于代理模式就容易实现可升级的合约,由于合约的变量存储都是存储在代理合约中的,而可升级的代理合约中除了保存业务本身所需的变量外,还需要保存升级合约相关变量,主要有逻辑合约地址和管理员地址等。

EIP-1967的目的就是规定一个通用的存储插槽使用标准,用于在代理合约中的特定位置存放逻辑合约的地址。其规定了如下特定的插槽:

=> 逻辑合约地址 
bytes32(uint256(keccak256("eip1967.proxy.implementation") - 1)) 
更新该地址时,需要同时发出: 
event Upgraded(address indexed implementation); 
 
=> beacon地址 
bytes32(uint256(keccak256("eip1967.proxy.beacon") - 1)) 
更新该地址时,需要发出: 
event BeaconUpgraded(address indexed beacon); 
 
=> admin 地址 
bytes32(uint256(keccak256("eip1967.proxy.admin") - 1)) 
更新该地址时,需要发出: 
event AdminChanged(address indexed previousAdmin, address newAdmin);

根据名称进行hash得到slot的位置来进行存储,避免了与合约变量slot位置冲突。

EIP1967详情参见 https://eips.ethereum.org/EIPS/eip-1967

Day2 - ERC1967Proxy

ERC1967Proxy 是 EIP1967 的openzepplin实现,它提供了一些修改 EIP1967 SLOT值的方法和事件,主要包括逻辑合约地址和管理员地址,具体实现参见 Openzepplin ERC1967Proxy

FUNCTIONS

constructor(_logic, _data)

_implementation()

ERC1967UPGRADE

_getImplementation()

_upgradeTo(newImplementation)

_upgradeToAndCall(newImplementation, data, forceCall)

_upgradeToAndCallUUPS(newImplementation, data, forceCall)

_getAdmin()

_changeAdmin(newAdmin)

_getBeacon()

_upgradeBeaconToAndCall(newBeacon, data, forceCall)

PROXY

_delegate(implementation)

_fallback()

fallback()

receive()

_beforeFallback()

EVENTS

ERC1967UPGRADE
Upgraded(implementation)

AdminChanged(previousAdmin, newAdmin)

BeaconUpgraded(beacon)

Day3 - Transparent Proxy

为避免代理函数选择器冲突的问题,Openzepplin实现了透明代理模式

Transparent Proxy 为暴露了合约升级相关方法的一个代理模式实现,而这种模式主要在EIP1967Proxy的基础上增加了如下功能

  1. 如果管理员以外的任何帐户调用代理,则该调用将被转发到实现,即使该调用与代理本身暴露的方法相同。
  2. 如果管理员调用代理,它可以访问管理功能,但它的调用永远不会被转发到实现合约。如果管理员试图在实现上调用一个函数,将会返回错误“admin cannot fallback to proxy target”。

这就意味着管理员帐户只能用于升级代理或更改管理员等管理员操作,因此最好是不使用的专用帐户其他任何事情。这将避免在尝试从代理实现调用函数时因突然错误而引起的头痛。

建议使用ProxyAdmin合约的实例作为管理员账户。

详情参见 https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent_proxy

Day4 - UUPS Proxy

在透明代理模式中,升级逻辑驻留在代理合约中--意味着升级是由代理处理的。必须调用upgradeTo(address newImpl)这样的函数来升级到一个新的实现合约。然而,由于这个逻辑放在代理合约里,部署这类代理的成本很高。

透明代理还需要管理机制来决定是委托调用实现中的功能还是执行代理合约本身的功能,以Box为例:

contract Box {
    uint256 private _value;

    function store(uint256 value) public { /*..*/ }

    function retrieve() public view returns (uint256) { /*..*/ }
}

contract BoxProxy {

     function _delegate(address implementation) internal virtual { /*..*/ }

     function getImplementationAddress() public view returns (address) { /*..*/ }

     fallback() external { /*..*/ }

     // Upgrade logic in Proxy contract
     upgradeTo(address newImpl) external {
         // Changes stored address of implementation of contract
         // at its slot in storage
     }
}

UUPS Proxy

UUPS模式首次在EIP1822提出。与透明模式不同,在UUPS中,升级逻辑是由实现合约本身处理的。因此该模式中,不需要再像Transparent模式那样区分管理员升级和普通函数调用,Proxy直接把所有的请求都通过delegatecall丢给Implementation(如果是升级,Implementation的升级函数会确认一下是否为管理员),因此不再需要ProxyAdmin。

实现合约包括升级逻辑的方法,以及通常的业务逻辑。你可以通过让它继承一个包括升级逻辑的通用标准接口来使任何实现合约符合UUPS标准,比如继承OpenZeppelin的UUPSUpgradeable接口:

contract Box is UUPSUpgradeable {
    uint256 private _value;

    function store(uint256 value) public { /*..*/ }

    function retrieve() public view returns (uint256) { /*..*/ }

     // Upgrade logic in Implementation contract
     upgradeTo(address newImpl) external {
         // Changes stored address of implementation of contract
         // at its slot in storage
     }
}

contract BoxProxy {

     function _delegate(address implementation) internal virtual { /*..*/ }

     function getImplementationAddress() public view returns (address) { /*..*/ }

     fallback() external { /*..*/ }

}

注意:使用UUPS代理模式时强烈建议继承这个接口来实现合约。因为如果不能在新版本的实现中包含升级逻辑(非UUPS兼容),就升级到它了,将永远锁定升级机制!因此建议你使用防止这种情况发生的措施的库(如UUPSUpgradeable)

参考文章:

  1. https://learnblockchain.cn/article/4257
  2. https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable

Day5 - Beacon 模式

和前面2种模式不同,Beacon模式的Implementation地址并不存放在Proxy合约里,而是存放在Beacon合约里,Proxy合约里存放的是Beacon合约的地址。

在合约交互的时候,用户同样是和Proxy合约打交道,不过此时因为Proxy合约中并未保存Implementation地址,所以它要先访问Beacon合约获取Implementation地址,然后再通过delegatecall调用Implementation。

在合约升级的时候,管理员并不需要和Proxy合约打交道,而只需要交互Beacon合约,把Beacon合约存储的Implementation改掉就行了。

Beacon模式

使用场景

通常当多个代理需要同时升级为相同的implementation时,适合使用Beacon模式,可以做到只需要修改Beacon合约的Implementation地址达到所有Proxy都升级的效果。

参考文章:

  1. https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon
  2. https://mirror.xyz/rbtree.eth/qDSQvenBZ_TWqZLUTlxiXVlhKQzPygRyv88EMFWxJro