Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-3561: 信任最小化的可升级代理

在指定的升级生效前有延迟的代理

Authors Sam Porter (@SamPorter1984)
Created 2021-05-09
Discussion Link https://ethereum-magicians.org/t/trust-minimized-proxy/5742

摘要

从可升级性代理中移除信任对于匿名开发者来说是必须的。为了实现这一点,必须防止即时和潜在的恶意升级。本 EIP 引入了用于可升级性代理的额外存储槽,这些存储槽被认为可以降低与可升级智能合约交互的信任。由管理员定义的实现逻辑只有在允许零信任期过后才能成为活跃的实现逻辑。

动机

使用可升级性代理的匿名开发者通常难以获得社区的信任。

对于人类来说,更公平、更好的未来绝对需要一些开发者保持匿名,同时仍然吸引人们对他们提出的解决方案的关注,并同时利用可能的可升级性的好处。

规范

该规范是对标准 EIP-1967 透明代理设计的补充。 该规范侧重于它添加的插槽。所有与信任最小化代理的管理员交互都必须发出一个事件,以使管理员操作可跟踪,并且所有管理员操作都必须使用 onlyAdmin() 修饰符进行保护。

下一个逻辑合约地址

存储槽 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685 (通过 bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1) 获得)。 期望的实现逻辑地址必须首先定义为下一个逻辑,然后才能作为存储在 EIP-1967 IMPLEMENTATION_SLOT 中的实际逻辑实现。 与下一个逻辑合约地址的管理员交互对应于以下方法和事件:

// 设置下一个逻辑合约地址。发出 NextLogicDefined 事件
// 如果当前实现是 address(0),则立即升级到 IMPLEMENTATION_SLOT
// 因此将数据作为参数
function proposeTo(address implementation, bytes calldata data) external IfAdmin
// 一旦 UPGRADE_BLOCK_SLOT 允许,将存储为下一个实现的地址
// 作为当前的 IMPLEMENTATION_SLOT 并对其进行初始化。
function upgrade(bytes calldata data) external IfAdmin
// 只要没有为给定的下一个逻辑调用 upgrade(),就可以取消
// 发出 NextLogicCanceled 事件
function cancelUpgrade() external onlyAdmin;

event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock); // 重要的是要有
event NextLogicCanceled(address indexed oldLogic);

升级区块

存储槽 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee (通过 bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1) 获得)。 在此区块上/之后,可以将下一个逻辑合约地址设置为 EIP-1967 IMPLEMENTATION_SLOT,或者换句话说,可以调用 upgrade()。根据零信任期自动更新,在事件 NextLogicDefined 中显示为 earliestArrivalBlock

提议区块

存储槽 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388 (通过 bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1) 获得)。 定义在此区块上/之后,提议下一个逻辑是可能的。为了方便起见,需要这样做;例如,可以手动设置为距给定时间一年。可以设置为最大数量以完全密封代码。 与此插槽的管理员交互对应于以下方法和事件:

function prolongLock(uint b) external onlyAdmin;
event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival);

零信任期

存储槽 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720 (通过 bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1) 获得)。 以区块数量表示的零信任期,只能设置为高于先前值的值。当它处于默认值 (0) 时,代理的行为与标准 EIP-1967 透明代理完全相同。设置零信任期后,将强制执行以上所有规范。 与此插槽的管理员交互应对应于以下方法和事件:

function setZeroTrustPeriod(uint blocks) external onlyAdmin;
event ZeroTrustPeriodSet(uint blocks);

实现示例

pragma solidity >=0.8.0; //important

// EIP-3561 信任最小化代理实现 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3561.md
// 基于 EIP-1967 可升级性代理: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1967.md

contract TrustMinimizedProxy {
    event Upgraded(address indexed toLogic);
    event AdminChanged(address indexed previousAdmin, address indexed newAdmin);
    event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock);
    event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival);
    event NextLogicCanceled();
    event ZeroTrustPeriodSet(uint blocks);

    bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    bytes32 internal constant LOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 internal constant NEXT_LOGIC_SLOT = 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685;
    bytes32 internal constant NEXT_LOGIC_BLOCK_SLOT = 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee;
    bytes32 internal constant PROPOSE_BLOCK_SLOT = 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388;
    bytes32 internal constant ZERO_TRUST_PERIOD_SLOT = 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720;

    constructor() payable {
        require(
            ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) &&
                LOGIC_SLOT == bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) &&
                NEXT_LOGIC_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1) &&
                NEXT_LOGIC_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1) &&
                PROPOSE_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1) &&
                ZERO_TRUST_PERIOD_SLOT == bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1)
        );
        _setAdmin(msg.sender);
    }

    modifier IfAdmin() {
        if (msg.sender == _admin()) {
            _;
        } else {
            _fallback();
        }
    }

    function _logic() internal view returns (address logic) {
        assembly {
            logic := sload(LOGIC_SLOT)
        }
    }

    function _nextLogic() internal view returns (address nextLogic) {
        assembly {
            nextLogic := sload(NEXT_LOGIC_SLOT)
        }
    }

    function _proposeBlock() internal view returns (uint b) {
        assembly {
            b := sload(PROPOSE_BLOCK_SLOT)
        }
    }

    function _nextLogicBlock() internal view returns (uint b) {
        assembly {
            b := sload(NEXT_LOGIC_BLOCK_SLOT)
        }
    }

    function _zeroTrustPeriod() internal view returns (uint ztp) {
        assembly {
            ztp := sload(ZERO_TRUST_PERIOD_SLOT)
        }
    }

    function _admin() internal view returns (address adm) {
        assembly {
            adm := sload(ADMIN_SLOT)
        }
    }

    function _setAdmin(address newAdm) internal {
        assembly {
            sstore(ADMIN_SLOT, newAdm)
        }
    }

    function changeAdmin(address newAdm) external IfAdmin {
        emit AdminChanged(_admin(), newAdm);
        _setAdmin(newAdm);
    }

    function upgrade(bytes calldata data) external IfAdmin {
        require(block.number >= _nextLogicBlock(), 'too soon');
        address logic;
        assembly {
            logic := sload(NEXT_LOGIC_SLOT)
            sstore(LOGIC_SLOT, logic)
        }
        (bool success, ) = logic.delegatecall(data);
        require(success, 'failed to call');
        emit Upgraded(logic);
    }

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }

    function _fallback() internal {
        require(msg.sender != _admin());
        _delegate(_logic());
    }

    function cancelUpgrade() external IfAdmin {
        address logic;
        assembly {
            logic := sload(LOGIC_SLOT)
            sstore(NEXT_LOGIC_SLOT, logic)
        }
        emit NextLogicCanceled();
    }

    function prolongLock(uint b) external IfAdmin {
        require(b > _proposeBlock(), 'can be only set higher');
        assembly {
            sstore(PROPOSE_BLOCK_SLOT, b)
        }
        emit ProposingUpgradesRestrictedUntil(b, b + _zeroTrustPeriod());
    }

    function setZeroTrustPeriod(uint blocks) external IfAdmin {
        // before this set at least once acts like a normal eip 1967 transparent proxy
        // 在至少设置一次之前,其行为类似于正常的 eip 1967 透明代理
        uint ztp;
        assembly {
            ztp := sload(ZERO_TRUST_PERIOD_SLOT)
        }
        require(blocks > ztp, 'can be only set higher');
        assembly {
            sstore(ZERO_TRUST_PERIOD_SLOT, blocks)
        }
        _updateNextBlockSlot();
        emit ZeroTrustPeriodSet(blocks);
    }

    function _updateNextBlockSlot() internal {
        uint nlb = block.number + _zeroTrustPeriod();
        assembly {
            sstore(NEXT_LOGIC_BLOCK_SLOT, nlb)
        }
    }

    function _setNextLogic(address nl) internal {
        require(block.number >= _proposeBlock(), 'too soon');
        _updateNextBlockSlot();
        assembly {
            sstore(NEXT_LOGIC_SLOT, nl)
        }
        emit NextLogicDefined(nl, block.number + _zeroTrustPeriod());
    }

    function proposeTo(address newLogic, bytes calldata data) external payable IfAdmin {
        if (_zeroTrustPeriod() == 0 || _logic() == address(0)) {
            _updateNextBlockSlot();
            assembly {
                sstore(LOGIC_SLOT, newLogic)
            }
            (bool success, ) = newLogic.delegatecall(data);
            require(success, 'failed to call');
            emit Upgraded(newLogic);
        } else {
            _setNextLogic(newLogic);
        }
    }

    function _delegate(address logic_) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), logic_, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }
}

理由

当涉及到严重依赖人为因素的复杂系统时,参数“根本不要使此类合约可升级”会失败,这可能会以史无前例的方式表现出来。可能无法在第一次尝试时正确建模某些系统。在某些协议成熟并且掌握数据之前,使用去中心化治理进行升级管理以及 EIP-1967 代理可能会成为严重的瓶颈。

在实际升级之前没有时间延迟的代理显然是可滥用的。时间延迟可能是不可避免的,即使这意味着经验不足的开发者可能没有信心使用它。尽管这是此 EIP 的一个缺点,但它是当今智能合约开发中拥有的一个至关重要的选项。

安全注意事项

用户必须确保与其交互的信任最小化代理不允许溢出,理想情况下,它代表上面实现示例中代码的精确副本,并且他们还必须确保零信任期长度是合理的(如果升级通常是提前公开的,则至少两周;在大多数情况下,至少一个月)。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

Sam Porter (@SamPorter1984), "ERC-3561: 信任最小化的可升级代理 [DRAFT]," Ethereum Improvement Proposals, no. 3561, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3561.