📖 This EIP is in the review stage. It is subject to changes and feedback is appreciated.

EIP-3561: Trust Minimized Upgradeability Proxy Source

proxy with a delay before specified upgrade goes live

AuthorSam Porter
Discussions-Tohttps://ethereum-magicians.org/t/trust-minimized-proxy/5742
StatusReview
TypeStandards Track
CategoryERC
Created2021-05-09

Abstract

Removing trust from upgradeability proxy is required for anonymous developers. To achieve that, disallowing instant, potentially malicious upgrades is required. This EIP introduces additional storage slots for upgradeability proxy which are assumed to decrease trust in interaction with upgradeable smart contracts. Defined by the admin implementation becomes an active implementation only after Zero Trust Period allows.

Motivation

It’s usually not possible for anonymous developers who uses upgradeability proxies to gain community trust.

Fairer, better future for humanity absolutely requires some developers to stay anonymous while still attract vital attention to solutions they propose and at the same time leverage the benefits of possible upgradeability.

Specification

The specification is an addition to the standard EIP-1967 transparent proxy design. The specification focuses on the slots it adds. All admin interactions with trust minimized proxy must emit an event to make admin actions trackable, and all admin actions must be guarded with onlyAdmin() modifier.

Next Logic Contract Address

Storage slot 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685 (obtained as bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1)). Logic address must be first defined as next logic, before it can function as actual logic implementation stored in EIP-1967 IMPLEMENTATION_SLOT. Admin interactions with next logic contract address correspond with these methods and events:

// sets next logic contract address and initializes it. Emits NextLogicDefined
// 0x as calldata is an equivalent of proposeTo()
// calldata is ignored here if zero trust period, described below, was already set
function proposeTo(address implementation, bytes calldata data) external onlyAdmin;
// sets the address stored as next implementation as current IMPLEMENTATION_SLOT
// as soon UPGRADE_BLOCK_SLOT allows
function upgrade() external onlyAdmin;
// cancelling is possible for as long as upgrade() for given next logic was not called
// emits NextLogicCanceled
function cancelUpgrade() external onlyAdmin;

event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock); // important to have
event NextLogicCanceled(address indexed oldLogic);

Upgrade Block

Storage slot 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee (obtained as bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1)). On/after this block next logic contract address can be set to EIP-1967 IMPLEMENTATION_SLOT or, in other words, start to function as current logic. Updated automatically according to Zero Trust Period, shown as earliestArrivalBlock in the event NextLogicDefined.

Propose Block

Storage slot 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388 (obtained as bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1)). Defines after/on which block proposing next logic is possible. Required for convenience, for example can be manually set to a year from given time. Can be set to maximum number to completely seal the code. Admin interactions with this slot correspond with this method and event:

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

Zero Trust Period

Storage slot 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720 (obtained as bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1)). Zero Trust Period in amount of blocks, can only be set higher than previous value. While it is at default value(0), the proxy operates exactly as standard EIP-1967 transparent proxy. After zero trust period is set, all above specification is enforced. Admin interactions with this slot should correspond with this method and event:

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

Implementation Example

pragma solidity >=0.8.0;//important

// EIP-3561 trust minimized proxy implementation https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3561.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 bl) {
		assembly { bl := sload(PROPOSE_BLOCK_SLOT) }
	}

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

	function _zeroTrustPeriod() internal view returns (uint tm) {
		assembly { tm := 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(),"get maxxed");
		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
		uint ztp;
		assembly { ztp := sload(ZERO_TRUST_PERIOD_SLOT) }
		require(blocks>ztp,"can be only set higher");
		assembly{ sstore(ZERO_TRUST_PERIOD_SLOT, blocks) }
		emit ZeroTrustPeriodSet(blocks);
	}
	
	function _updateBlockSlot() internal {
		uint nlb = block.number + _zeroTrustPeriod();
		assembly {sstore(NEXT_LOGIC_BLOCK_SLOT,nlb)}
	}

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

	function proposeTo(address newLogic, bytes calldata data) payable external ifAdmin {
		if (_zeroTrustPeriod() == 0) {
			_updateBlockSlot();
			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()) }
		}
	}
}

Rationale

An argument “just don’t make such contracts upgadeable at all” fails when it comes to complex systems which do or do not heavily reliant on human factor which might manifest itself in unprecedented ways. It might be impossible to model some systems right on first try. Using decentralized governance for upgrade management coupled with EIP-1967 proxy could also become a serious bottleneck for certain protocols before they mature and data is at hand.

A proxy without a time delay before an actual upgrade is obviously abusable. A time delay is probably unavoidable, even if it means that inexperienced developers might not have confidence using it. Albeit this is a downside of this EIP, it’s a critically important option to have in smart contract development today.

Propose block adds to convenience if used, so should be kept.

Security Considerations

Users must ensure that a trust-minimized proxy they interact with does not allow overflows, ideally represents the exact copy of the code in implementation example above, and also they must ensure that Zero Trust Period length is reasonable(at the very least two weeks if finalized upgrades are usually being revealed beforehand, and in most cases at least a month).

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Sam Porter, "EIP-3561: Trust Minimized Upgradeability Proxy [DRAFT]," Ethereum Improvement Proposals, no. 3561, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3561.