ERC-7511: Minimal Proxy Contract with PUSH0

Optimizes the previous Minimal Proxy Contract with the PUSH0 opcode


Metadata
Status: DraftStandards Track: ERCCreated: 2023-09-04
Authors
0xAA (@AmazingAng), vectorized (@Vectorized), 0age (@0age)

Abstract


With the PUSH0 opcode (EIP-3855), introduced with the Shanghai upgrade, we optimized the previous Minimal Proxy Contract (ERC-1167) by 200 gas at deployment and 5 gas at runtime, while retaining the same functionality.

Motivation


  1. Reduce the contract bytecode size by 1 byte by removing a redundant SWAP opcode.
  2. Reduce the runtime gas by replacing two DUP (cost 3 gas each) with two PUSH0 (cost 2 gas each).
  3. Increase the readability of the proxy contract by redesigning it from first principles with PUSH0.

Specification


Standard Proxy Contract

The exact runtime code for the minimal proxy contract with PUSH0 is:


where the bytes at indices 9 - 28 (inclusive) are replaced with the 20-byte address of the master implementation contract. The length of the runtime code is 44 bytes.

The disassembly of the new minimal proxy contract code is:

pcopopcodestack
[00]36CALLDATASIZEcds
[01]5fPUSH00 cds
[02]5fPUSH00 0 cds
[03]37CALLDATACOPY
[04]5fPUSH00
[05]5fPUSH00 0
[06]36CALLDATASIZEcds 0 0
[07]5fPUSH00 cds 0 0
[08]73bebe.PUSH20 0xbebe.0xbebe. 0 cds 0 0
[1d]5aGASgas 0xbebe. 0 cds 0 0
[1e]f4DELEGATECALLsuc
[1f]3dRETURNDATASIZErds suc
[20]5fPUSH00 rds suc
[21]5fPUSH00 0 rds suc
[22]3eRETURNDATACOPYsuc
[23]5fPUSH00 suc
[24]3dRETURNDATASIZErds 0 suc
[25]91SWAP2suc 0 rds
[26]602aPUSH1 0x2a0x2a suc 0 rds
[27]57JUMPI0 rds
[29]fdREVERT
[2a]5bJUMPDEST0 rds
[2b]f3RETURN

Minimal Creation Code

The minimal creation code of the minimal proxy contract is:


where the first 9 bytes are the initcode:


And the rest are runtime/contract code of the proxy. The length of the creation code is 53 bytes.

Deploy with Solidity

The minimal proxy contract can be deployed with Solidity using the following contract:


Rationale


The optimized contract is constructed with essential components of the proxy contract and incorporates the recently added PUSH0 opcode. The core elements of the minimal proxy include:

  1. Copy the calldata with CALLDATACOPY.
  2. Forward the calldata to the implementation contract using DELEGATECALL.
  3. Copy the returned data from the DELEGATECALL.
  4. Return the results or revert the transaction based on whether the DELEGATECALL is successful.

Step 1: Copy the Calldata

To copy the calldata, we need to provide the arguments for the CALLDATACOPY opcodes, which are [0, 0, cds], where cds represents calldata size.

pcopopcodestack
[00]36CALLDATASIZEcds
[01]5fPUSH00 cds
[02]5fPUSH00 0 cds
[03]37CALLDATACOPY

Step 2: Delegatecall

To forward the calldata to the delegate call, we need to prepare arguments for the DELEGATECALL opcodes, which are [gas 0xbebe. 0 cds 0 0], where gas represents the remaining gas, 0xbebe. represents the address of the implementation contract, and suc represents whether the delegatecall is successful.

pcopopcodestack
[04]5fPUSH00
[05]5fPUSH00 0
[06]36CALLDATASIZEcds 0 0
[07]5fPUSH00 cds 0 0
[08]73bebe.PUSH20 0xbebe.0xbebe. 0 cds 0 0
[1d]5aGASgas 0xbebe. 0 cds 0 0
[1e]f4DELEGATECALLsuc

Step 3: Copy the Returned Data from the DELEGATECALL

To copy the returndata, we need to provide the arguments for the RETURNDATACOPY opcodes, which are [0, 0, red], where rds represents size of returndata from the DELEGATECALL.

pcopopcodestack
[1f]3dRETURNDATASIZErds suc
[20]5fPUSH00 rds suc
[21]5fPUSH00 0 rds suc
[22]3eRETURNDATACOPYsuc

Step 4: Return or Revert

Lastly, we need to return the data or revert the transaction based on whether the DELEGATECALL is successful. There is no if/else in opcodes, so we need to use JUMPI and JUMPDEST instead. The arguments for JUMPI is [0x2a, suc], where 0x2a is the destination of the conditional jump.

We also need to prepare the argument [0, rds] for REVERT and RETURN opcodes before the JUMPI, otherwise we have to prepare them twice. We cannot avoid the SWAP operation, because we can only get rds after the DELEGATECALL.

pcopopcodestack
[23]5fPUSH00 suc
[24]3dRETURNDATASIZErds 0 suc
[25]91SWAP2suc 0 rds
[26]602aPUSH1 0x2a0x2a suc 0 rds
[27]57JUMPI0 rds
[29]fdREVERT
[2a]5bJUMPDEST0 rds
[2b]f3RETURN

In the end, we arrived at the runtime code for Minimal Proxy Contract with PUSH0:


The length of the runtime code is 44 bytes, which reduced 1 byte from the previous Minimal Proxy Contract. Moreover, it replaced the RETURNDATASIZE and DUP operations with PUSH0, which saves gas and increases the readability of the code. In summary, the new Minimal Proxy Contract reduces 200 gas at deployment and 5 gas at runtime, while remaining the same functionalities as the old one.

Backwards Compatibility


Because the new minimal proxy contract uses PUSH0 opcode, it can only be deployed after the Shanghai Upgrade. It behaves the same as the previous Minimal Proxy Contract.

Security Considerations


The new proxy contract standard is identical to the previous one (ERC-1167). Here are the security considerations when using minimal proxy contracts:

  1. Non-Upgradability: Minimal Proxy Contracts delegate their logic to another contract (often termed the "implementation" or "logic" contract). This delegation is fixed upon deployment, meaning you can't change which implementation contract the proxy delegates to after its creation.

  2. Initialization Concerns: Proxy contracts lack constructors, so you need to use an initialization function after deployment. Skipping this step could leave the contract unsafe.

  3. Safety of Logic Contract: Vulnerabilities in the logic contract affect all associated proxy contracts.

  4. Transparency Issues: Because of its complexity, users might see the proxy as an empty contract, making it challenging to trace back to the actual logic contract.

Copyright


Copyright and related rights waived via CC0.