eip | title | author | discussions-to | status | type | category | created |
---|---|---|---|---|---|---|---|
7229 | Minimal Upgradable Proxy Contract | xiaobaiskill (@xiaobaiskill) | Draft | Standards Track | ERC | 2023-06-24 |
Abstract
This proposal introduces the Minimal Upgradable Contract, a lightweight alternative to the existing upgradable contract implementation provided by OpenZeppelin. The Minimal Upgradable Contract aims to significantly reduce gas consumption during deployment while maintaining upgradability features.
Motivation
Current upgradable contract solutions, such as OpenZeppelin's EIP-1967 implementation, often incur high gas costs during deployment. The goal of this proposal is to present an alternative approach that offers a significant reduction in gas consumption while maintaining the ability to upgrade contract logic.
Specification
The exact bytecode of the standard minimal upgradable contract is this: 7fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3
; In this bytecode, the 1st to 32nd byte (inclusive) needs to be replaced with a 32-byte slot, and the 34th to 53rd byte (inclusive) needs to be replaced with a 20-byte address. Please note that the placeholders xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
represent the 32-byte slot and yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
represents the 20-byte address.
Rationale
The Minimal Upgradeable Contract proposal introduces a novel approach to minimize gas consumption while preserving the upgradability feature of smart contracts. By predefining the slot that stores the logic contract during the contract creation phase, we can optimize gas efficiency during contract execution by directly accessing and performing delegate calls to the logic contract stored in the designated slot.
The rationale behind this approach is to eliminate the need for additional storage operations or costly lookups, which are commonly required in traditional upgradeable contract implementations. By predefining the slot and accessing the logic contract directly, we achieve significant gas savings and improve overall contract performance.
Key considerations for adopting this rationale include:
1、 Gas Optimization: By eliminating redundant storage operations and lookup procedures, we reduce gas consumption during contract execution. This optimization ensures that the contract remains cost-effective, especially when performing frequent contract calls or upgrades.
2、 Deterministic Contract Logic: By fixing the slot for storing the logic contract during contract creation, we establish a deterministic relationship between the contract and its logic. This deterministic mapping enables seamless contract upgrades without requiring additional storage or lookup mechanisms.
3、 Contract Compatibility: The rationale aligns with existing Ethereum Virtual Machine (EVM) environments and does not introduce any compatibility issues or modifications to the underlying infrastructure. It can be easily integrated into the current Ethereum ecosystem, ensuring seamless adoption by developers and users.
4、 Developer-Friendly Implementation: The rationale simplifies the implementation process for upgradeable contracts. By providing a straightforward mechanism to access and delegate calls to the logic contract, developers can focus on contract logic and functionality rather than complex storage management.
By adopting this rationale, we aim to strike a balance between gas efficiency and contract upgradability. It allows developers to deploy upgradeable contracts with minimal gas consumption, making them more accessible and economically viable for a wide range of applications.
Gas Efficiency
Compared to existing upgradable contract solutions, the Minimal Upgradable Contract demonstrates a significant reduction in gas consumption during deployment. While OpenZeppelin's EIP-1967 implementation may consumes nearly several hundred thousand gas for deployment, the Minimal Upgradable Contract can be deployed with just a few tens of thousands of gas, resulting in substantial cost savings.
- Transaction deploying the Minimal Upgradable Contract (32bytes slot)
- Transaction deploying the Minimal Upgradable Contract (1bytes slot)
- Transaction deploying using OpenZeppelin's EIP-1967
Implementation
A reference implementation of the Minimal Upgradable Contract, including the proxy contract and an example implementation contract, will be provided as open-source code. This implementation will serve as a starting point for developers to adopt and customize the Minimal Upgradable Contract in their projects.
Example implementations
-
deploy proxy contract when deploying logic contract (32bytes slot)
-
deploy proxy contract when deploying logic contract (1bytes slot)
Standard Proxy
The disassembly of the standard deployed proxy contract code
# store logic address to slot of proxy contract
PUSH32 <slot> [slot]
PUSH20 <logicAddress> [logicAddress slot]
DUP2 [slot logicAddress slot]
SSTORE [slot] => storage(slot => logicAddress)
# return deployedCode
PUSH1 0x9 [0x9 slot]
PUSH1 0x4c [0x4c 0x9 slot]
PUSH0 [00 0x4c 0x9 slot]
CODECOPY [slot] ==> memory(0x00~0x8: 0x4c~0x54(deployedCode1stPart))
PUSH1 0x9 [0x9 slot]
MSTORE [] ==> memory(0x9~0x28: slot(deployedCode2ndPart))
PUSH1 0x10 [0x10]
PUSH1 0x55 [0x55 0x10]
PUSH1 0x29 [0x29 0x55 0x10]
CODECOPY [] ==> memory(0x29~0x38: 0x55~0x64(deployedCode3rdPart))
PUSH1 0x39 [0x39]
PUSH0 [00 0x39]
RETURN
# proxy contract (deployedcode)
CALLDATASIZE [calldatasize]
PUSH0 [00 calldatasize]
PUSH0 [00 00 calldatasize]
CALLDATACOPY [] ==> memory(00~(calldatasize-1) => codedata)
PUSH0 [00]
PUSH0 [00 00]
CALLDATASIZE [calldatasize 00 00]
PUSH0 [00 calldatasize 00 00]
PUSH32 [slot 00 calldatasize 00 00]
SLOAD [logicAddress 00 calldatasize 00 00]
GAS [gas logicAddress 00 calldatasize 00 00]
DELEGATECALL [result]
RETURNDATASIZE [returnDataSize result]
PUSH0 [00 returnDataSize result]
PUSH0 [00 00 returnDataSize result]
RETURNDATACOPY [result] => memory(00~(RETURNDATASIZE - 1) => RETURNDATA)
RETURNDATASIZE [returnDataSize result]
PUSH0 [00 returnDataSize result]
DUP3 [result 00 returnDataSize result]
PUSH1 0x37 [0x37 result 00 returnDataSize result]
JUMPI [00 returnDataSize result]
REVERT [result]
JUMPDEST [00 returnDataSize result]
RETURN [result]
NOTE: To push a zero value onto the stack without abusing the RETURNDATASIZE
opcode, the above code utilizes EIP-3855. It achieves this by using the PUSH0
instruction to push the zero value.
Storage slot of logic address optimization
To further optimize the minimal upgradeable proxy by controlling the slot value for the logic address within the range of 255(inclusive), you can use the following opcode to reduce gas consumption:
# store logic address to slot of proxy contract
PUSH1 <slot> [slot]
PUSH20 <logicAddress> [logicAddress slot]
DUP2 [slot logicAddress slot]
SSTORE [slot] => storage(slot => logicAddress)
# return deployedCode
PUSH1 0x9 [0x9 slot]
PUSH1 0x30 [0x30 0x9 slot]
PUSH0 [00 0x30 0x9 slot]
CODECOPY [slot] ==> memory(0x00~0x8: 0x30~0x54(deployedCode1stPart))
PUSH1 0xf8 [0xf8 slot]
SHL [slotAfterShl]
PUSH1 0x9 [0x9 slotAfterShl]
MSTORE [] ==> memory(0x9: slotAfterShl(deployedCode2ndPart))
PUSH1 0x10 [0x10]
PUSH1 0x39 [0x39 0x10]
PUSH1 0xa [0xa 0x39 0x10]
CODECOPY [] ==> memory(0xa~0x38: 0x39~0x64(deployedCode3rdPart))
PUSH1 0x1a [0x1a]
PUSH0 [00 0x1a]
RETURN
# proxy contract (deployedcode)
CALLDATASIZE [calldatasize]
PUSH0 [00 calldatasize]
PUSH0 [00 00 calldatasize]
CALLDATACOPY [] ==> memory(00~(calldatasize-1) => codedata)
PUSH0 [00]
PUSH0 [00 00]
CALLDATASIZE [calldatasize 00 00]
PUSH0 [00 calldatasize 00 00]
PUSH1 [slot 00 calldatasize 00 00]
SLOAD [logicAddress 00 calldatasize 00 00]
GAS [gas logicAddress 00 calldatasize 00 00]
DELEGATECALL [result]
RETURNDATASIZE [returnDataSize result]
PUSH0 [00 returnDataSize result]
PUSH0 [00 00 returnDataSize result]
RETURNDATACOPY [result] => memory(00~(RETURNDATASIZE - 1) => RETURNDATA)
RETURNDATASIZE [returnDataSize result]
PUSH0 [00 returnDataSize result]
DUP3 [result 00 returnDataSize result]
PUSH1 0x18 [0x18 result 00 returnDataSize result]
JUMPI [00 returnDataSize result]
REVERT [result]
JUMPDEST [00 returnDataSize result]
RETURN [result]
The bytecode generated by the above opcodes is as follows 60xx73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3
, replace xx
to a slot of 1byte and replace yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
to a address of 20bytes before deploying contract
Test Cases
Test cases are available at https://github.com/xiaobaiskill/minimal-upgradable-proxy/tree/main/test
Security Considerations
None.
References
- OpenZeppelin EIP-1967: Standard Proxy Storage Slots
- EIP-1822: Universal Upgradeable Proxy Standard (UUPS)
- EIP-1167: Minimal Proxy Contract
Copyright
Copyright and related rights waived via CC0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your title doesn't differentiate itself enough form the other proxy contract standards. Maybe something like: