forked from DecentralizedClimateFoundation/DCIPs
283 lines
12 KiB
Markdown
283 lines
12 KiB
Markdown
|
---
|
||
|
eip: 3561
|
||
|
title: Trust Minimized Upgradeability Proxy
|
||
|
description: proxy with a delay before specified upgrade goes live
|
||
|
author: Sam Porter (@SamPorter1984)
|
||
|
discussions-to: https://ethereum-magicians.org/t/trust-minimized-proxy/5742
|
||
|
status: Review
|
||
|
type: Standards Track
|
||
|
category: ERC
|
||
|
created: 2021-05-09
|
||
|
---
|
||
|
|
||
|
## Abstract
|
||
|
|
||
|
Removing trust from upgradeability proxy is necessary for anonymous developers. In order to accomplish this, instant and potentially malicious upgrades must be prevented. 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 logic can be made an active implementation logic only after Zero Trust Period allows.
|
||
|
|
||
|
## Motivation
|
||
|
|
||
|
Anonymous developers who utilize upgradeability proxies typically struggle to earn the trust of the community.
|
||
|
|
||
|
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](./eip-1967.md) 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)`).
|
||
|
Desirable implementation 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:
|
||
|
|
||
|
```solidity
|
||
|
// Sets next logic contract address. Emits NextLogicDefined
|
||
|
// If current implementation is address(0), then upgrades to IMPLEMENTATION_SLOT
|
||
|
// immedeatelly, therefore takes data as an argument
|
||
|
function proposeTo(address implementation, bytes calldata data) external IfAdmin
|
||
|
// As soon UPGRADE_BLOCK_SLOT allows, sets the address stored as next implementation
|
||
|
// as current IMPLEMENTATION_SLOT and initializes it.
|
||
|
function upgrade(bytes calldata data) external IfAdmin
|
||
|
// 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, `upgrade()` can be called. 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:
|
||
|
|
||
|
```solidity
|
||
|
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:
|
||
|
|
||
|
```solidity
|
||
|
function setZeroTrustPeriod(uint blocks) external onlyAdmin;
|
||
|
event ZeroTrustPeriodSet(uint blocks);
|
||
|
```
|
||
|
|
||
|
### Implementation Example
|
||
|
|
||
|
```solidity
|
||
|
pragma solidity >=0.8.0; //important
|
||
|
|
||
|
// EIP-3561 trust minimized proxy implementation https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3561.md
|
||
|
// Based on EIP-1967 upgradeability proxy: 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
|
||
|
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())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## 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 rely 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 might 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.
|
||
|
|
||
|
## 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 upgrades are usually being revealed beforehand, and in most cases at least a month).
|
||
|
|
||
|
## Copyright
|
||
|
|
||
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|