EIP1967 指定代理合约的标准

  1. 避免存储管理员地址被冲突

  2. 避免函数选择器碰撞导致changeadmin这种函数被碰撞导致改变了管理员地址

通过手动指定一个随机slot来存储,进而避免被算出来碰撞的可能性

实现地址存储于0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
管理地址存储于0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
是通过下面两个的地址来计算的

bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)

只要查看这些地址是否具有非0值,就可以判断这是不是一个代理合约,这也是ether scan判断的方式

信标槽位 将多个代理指向同一个实现合约,因为数据被保存在代理合约的插槽中

interface IBeacon {
function implementation() external view returns (address);
}

更改信标合约的返回值可以很简单的改变所有的代理
信标插槽0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)

有两种代理合约的模板

OpenZeppelin和UUPS的设计模式都是不让在代理合约中直接实现更改管理员地址的操作来避免出现函数选择器碰撞,进而来导致实现合约地址或者是管理员地址被错误的更改

下面我们来逐一分析:

OpenZeppelin的实现模式:
不同于ERC1967规定的,他并不使用管理地址0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103来存储管理员的地址,而是在每次创建的同时
都会创建一个proxy admin 合约,将代理合约的管理员地址设置为proxy admin的地址,再通过管控proxy admin的管理地址来进而管理代理合约的管理地址。
反正搞这么多一堆的大致意思就是为了防止admin地址无法正常调用proxy里的功能,因为proxy里会判断msg.sender是否是admin地址,进而决定是否升级代理合约的实现。

大概的流程就是:

admin --> proxy admin contract --> proxy contract --> implementation contract

下面我们仔细来看下各个部分的实现:
proxy admin contract:

pragma solidity ^0.8.20;

import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
import {Ownable} from "../../access/Ownable.sol";

contract ProxyAdmin is Ownable {
    string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";

    constructor(address initialOwner) Ownable(initialOwner) {}

    function upgradeAndCall(
        ITransparentUpgradeableProxy proxy,
        address implementation,
        bytes memory data
    ) public payable virtual onlyOwner {
        proxy.upgradeToAndCall{value: msg.value}(implementation, data); //这个合约很简单,他只有这一个功能,用来升级代理合约的实现
    }
}

proxy contract,这个合约由三个合约组成

1.proxy.sol 给定地址,并向该地址发送委托调用

abstract contract Proxy {
    function _delegate(address implementation) internal virtual {
        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())
            }
        }
    }

    function _implementation() internal view virtual returns (address); //这个函数没实现,由继承他的重写

    function _fallback() internal virtual {
        _delegate(_implementation());
    }

    fallback() external payable virtual {
        _fallback();
    }
}

2.ERC1967Proxy.sol

pragma solidity ^0.8.20;

import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "./ERC1967Utils.sol";

contract ERC1967Proxy is Proxy {

    constructor(address implementation, bytes memory _data) payable {
        ERC1967Utils.upgradeToAndCall(implementation, _data);
    }

    // reads from bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
    function _implementation() internal view virtual override returns (address) {  //重写上面的_implementation,返回我们上面的slot的地址
        return ERC1967Utils.getImplementation();
    }
}

3.TransparentUpgradeableProxy.sol 重头戏,负责合约的实现

contract TransparentUpgradeableProxy is ERC1967Proxy {
    address private immutable _admin;  //不变量,这样可以避免每次fallback都从slot里面读取地址,省GAS

    error ProxyDeniedAdminAccess();

    constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
        _admin = address(new ProxyAdmin(initialOwner));
        // Set the storage value and emit an event for ERC-1967 compatibility
        ERC1967Utils.changeAdmin(_proxyAdmin()); //初始化的时候将我们创建的proxy admin这个地址设置成为admin,这个change admin是private方法也就不存在外部调用/selector 碰撞的可能
    }

    function _proxyAdmin() internal view virtual returns (address) {
        return _admin;
    }
    //主要讲解下这个,这里我们其实是调用了proxy admin 合约的upgradeToAndCall方法,进而调用proxy里的upgradeToAndCall方法,但是proxy里没有符合的函数,所以进入fallback,
    //在fallback中进一步判断msg.sender是否是proxy admin,满足的话判断signature是否是upgradeToAndCall,是的话再调用_dispatchUpgradeToAndCall方法来修改Implementation
    //地址的实现
    function _fallback() internal virtual override {
        if (msg.sender == _proxyAdmin()) {
            if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
                revert ProxyDeniedAdminAccess();
            } else {
                _dispatchUpgradeToAndCall();
            }
        } else {
            super._fallback();
        }
    }

    function _dispatchUpgradeToAndCall() private {
        (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); //这里忽略了前4bytes,刚好是函数选择器的部分
        ERC1967Utils.upgradeToAndCall(newImplementation, data);
    }
}

附上原代码地址TransparentUpgradeableProxy

还有一种设计就是UUPS的实现,UUPS的代理合约也没有直接升级实现地址的方法,从而避免了选择器碰撞,他用的是在逻辑合约上实现升级操作,因为调用逻辑合约使用的是代理合约的存储空间,所以就可以达到更改代理合约中的插槽的目的。没什么深奥的设计,先不赘述。

UUPS

几起和代理合约相关的问题:

Staking Pool 部署后未调用initalize

TX
分析

Staking Pool 部署后未调用initalize
TX

大部分都是因为部署后没有调用Initalize导致可以被任何人重新初始化合约。