Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP: Minimal Upgradable Proxy Contract #7229

Closed
wants to merge 14 commits into from
249 changes: 249 additions & 0 deletions EIPS/eip-7229.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
---
eip: 7229
title: Minimal Upgradable Proxy Contract
Copy link
Contributor

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:

Suggested change
title: Minimal Upgradable Proxy Contract
title: Gas Optimized Proxy Contract

description: A lightweight contract upgrade pattern designed to save gas costs while providing the ability to upgrade contracts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably mention custom assembly in here somewhere.

author: xiaobaiskill (@xiaobaiskill)
discussions-to: https://ethereum-magicians.org/t/eip-xxxx-minimal-upgradable-proxy/14754
status: Draft
type: Standards Track
category: ERC
created: 2023-06-24
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
---
requires: 3855
---


## 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract should be a terse technical summary of the proposal. The reader should be able to get a high level understanding of how the proposal accomplishes its goal (in this case, handrolled assembly.)


## Motivation

Current upgradable contract solutions, such as OpenZeppelin's [ERC-1967](./eip-1967.md) 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably mention what the "slot" and "address" mean. To someone very familiar with solidity/proxies, it's obvious that the address is the implementation contract, but it's good to be explicit.


## 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.
Comment on lines +36 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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 [ERC-1967](./eip-1967.md) 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)](../assets/eip-7229/img/minimal-upgradable-proxy-32slot.png)
- [Transaction deploying the Minimal Upgradable Contract (1bytes slot)](../assets/eip-7229/img/minimal-upgradable-proxy-1slot.png)
- [Transaction deploying using OpenZeppelin's ERC-1967](../assets/eip-7229/img/openzepplin-ERC-1967.png)

### 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)](../assets/eip-7229/contracts/mock/mock32/Example.sol)
- [deploy proxy contract (32bytes slot)](../assets/eip-7229/contracts/mock/mock32/DeployContract.sol)

- [deploy proxy contract when deploying logic contract (1bytes slot)](../assets/eip-7229/contracts/mock/mock1/Example.sol)
- [deploy proxy contract (1bytes slot)](../assets/eip-7229/contracts/mock/mock1/DeployContract.sol)
Comment on lines +55 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should should move this to the Reference Implementation section (after Test Cases.)


#### 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](./eip-3855.md). It achieves this by using the `PUSH0` instruction to push the zero value.

#### Storage slot of 1 byte optimization

To further optimize the minimal upgradeable proxy by controlling the slot value for the logic address within 1 ~ 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~0x38(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~0x19: 0x39~0x48(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
Comment on lines +122 to +177
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're introducing new content in your rationale section. The rationale section should only explain the stuff in the specification section, and shouldn't introduce any new requirements.



#### Optimization for Logic Address Storage in Slot 0

To further optimize the minimal upgradeable proxy by controlling the slot value for the logic address to 0, you can use the following opcode to reduce gas consumption:

```
# store logic address to slot of proxy contract
PUSH20 <logicAddress> [logicAddress]
PUSH0 [00 logicAddress]
SSTORE [] => storage(00 => logicAddress)

# return deployedCode
PUSH1 0x9 [0x9]
PUSH1 0x28 [0x28 0x9]
PUSH0 [00 0x28 0x9]
CODECOPY [] ==> memory(0x00~0x8: 0x28~0x30(deployedCode1stPart))
PUSH1 0x10 [0x10]
PUSH1 0x31 [0x31 0x10]
PUSH1 0x9 [0x9 0x31 0x10]
CODECOPY [] ==> memory(0x9~0x19: 0x31~0x41(deployedCode2ndPart))
PUSH1 0x19 [0x19]
PUSH0 [00 0x19]
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]
PUSH0 [00 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 0x17 [0x17 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 `73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3`, replace `yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` to a address of 20bytes before deploying contract
Comment on lines +180 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.



## Test Cases

You can use the following commands:

```
cd ../assets/eip-7229
npm install
npx hardhat test
```

## Security Considerations

None.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there are at least some security considerations 🙃

Suggested change
None.
<!-- needs discussion -->


## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
5 changes: 5 additions & 0 deletions assets/eip-7229/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
artifacts
cache
node_modules
package-lock.json
typechain
43 changes: 43 additions & 0 deletions assets/eip-7229/contracts/mock/PayableTokenV1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: UNLICENSED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: CC0-1.0

We can't have UNLICENSED code in the EIPs repository.

pragma solidity ^0.8.13;

contract PayableTokenV1 {
address private implementation;
address public owner;
uint256 public number;

event Upgraded(address indexed implementation);

receive() external payable {
number += 1;
}

fallback() external payable {
number += 2;
}

modifier OnlyOwner() {
require(owner == msg.sender, "only owner");
_;
}

function init() external {
owner = msg.sender;
}

function getImplementSlot() external pure returns (bytes32 slot) {
assembly {
slot := implementation.slot
}
}

function upgrade(address _newImplementation) external OnlyOwner {
implementation = _newImplementation;
emit Upgraded(_newImplementation);
}

function setNumber(uint256 _number) external payable {
require(msg.value >= 1 ether, "Insufficient amount");
number = _number;
}
}
35 changes: 35 additions & 0 deletions assets/eip-7229/contracts/mock/PayableTokenV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: UNLICENSED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No UNLICENSED.

pragma solidity ^0.8.13;

contract PayableTokenV2 {
address private implementation;
address public owner;
uint256 public number;

event Upgraded(address indexed implementation);

modifier OnlyOwner() {
require(owner == msg.sender, "only owner");
_;
}

function init() external {
owner = msg.sender;
}

function getImplementSlot() external pure returns (bytes32 slot) {
assembly {
slot := implementation.slot
}
}

function upgrade(address _newImplementation) external OnlyOwner {
implementation = _newImplementation;
emit Upgraded(_newImplementation);
}

function setNumber(uint256 _number) external payable {
require(msg.value >= 1 ether, "Insufficient amount");
number = _number;
}
}
38 changes: 38 additions & 0 deletions assets/eip-7229/contracts/mock/SimV1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer CC0-1.0 but MIT is fine too.

pragma solidity ^0.8.13;

contract SimV1 {
address public owner;
uint256 public number;
address private implementation;

event Upgraded(address indexed implementation);

constructor(uint256 _number) {
number = _number;
}

modifier OnlyOwner() {
require(owner == msg.sender, "only owner");
_;
}

function init() external {
owner = msg.sender;
}

function getImplementSlot() external pure returns (bytes32 slot) {
assembly {
slot := implementation.slot
}
}

function upgrade(address _newImplementation) external OnlyOwner {
implementation = _newImplementation;
emit Upgraded(_newImplementation);
}

function setNumber(uint256 _number) external OnlyOwner {
number = _number;
}
}
Loading