Skip to content

Commit

Permalink
Add RIP: Gas to Ether precompile (#37)
Browse files Browse the repository at this point in the history
* Initial draft

* T

* Update RIPS/rip-9999.md

Co-authored-by: Andrew B Coathup <[email protected]>

* Update RIPS/rip-9999.md

Co-authored-by: Andrew B Coathup <[email protected]>

* Rename file

* New deployment

---------

Co-authored-by: Andrew B Coathup <[email protected]>
  • Loading branch information
Vectorized and abcoathup authored Oct 7, 2024
1 parent b100f8c commit 83d5fcf
Showing 1 changed file with 285 additions and 0 deletions.
285 changes: 285 additions & 0 deletions RIPS/rip-7767.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
---
rip: 7767
title: Gas to Ether precompile
description: A precompile that burns gas and returns a portion of the gas burned to the caller as Ether
author: Vectorized (@Vectorized), Mark Tyneway (@tynes), Zak Cole (@zscole), Charles Cooper (@charles-cooper)
discussions-to: https://ethereum-magicians.org/t/rip-7767-gas-to-ether-precompile/21005
status: Draft
type: Standards Track
category: Core
created: 2024-09-12
---

## Abstract

This proposal describes a precompile that allows the caller to burn a specific amount of gas, and returns a portion of the gas burned to the caller as Ether.

To facilitate adoption, this precompile may be either implemented as a `CREATE2` preinstall or as a precompile at the core layer.

## Motivation

Contract Secured Revenue (CSR) is a way of enabling smart contract developers to claim a percentage of the gas fees paid by users when using their smart contracts.

CSR enables new revenues streams in a composable, interoperable, and enforceable way. It works with any existing token standard and onchain applications. It does not require developers to break or create new smart contract standards for the sake of capturing value.

However, CSR can lead to developers intentionally writing bloated smart contracts, sometimes with gas guzzling loops. This can result in unnecessary state bloat and undesirable network congestion.

Having a precompile that burns gas with a low constant compute cost removes the bad externalities, while keeping the benefits of CSR.

With this proposal, the more efficient developers can make their smart contracts, the more gas room they have for the constant cost precompile.

## Specification

For nomenclature, we will refer to this precompile as the `GASBACK` precompile.

### Behavior

The `GASBACK` precompile takes in calldata input that represents the amount of gas to burn, `bytes32(uint256(gasToBurn))`.

This amount is a suggestion, and the precompile may choose to burn a different amount to manage network congestion and sequencer fees.

The precompile MUST be constant in state and compute consumption with respect to the amount of gas burned.

It is RECOMMENDED that the precompile adds a flat overhead gas that will be burned without being converted to Ether.

It is RECOMMENDED that the precompile reduces the amount of gas burned if the basefee ges too high.

The resultant Ether from the gas burned MUST be forced transferred to the caller of the precompile, similar to the Ether force transfer behavior of `SELFDESTRUCT`.

The resultant Ether from the gas burned MUST not exceed `basefee * actualGasBurned`, where `actualGasBurned` is the actual amount of gas burned by the precompile after adjustments.

The precompile MUST revert (and MUST NOT transfer any Ether) in the following cases:

- Input calldata is not exactly 32 bytes long.
- Arithmetic error (overflow, division by zero).

In the case where the precompile can be executed safely and Ether has been transferred to the caller, the precompile MUST return `bytes32(uint256(etherMintedInWei))`.

### Precompile Address

The `PRECOMPILED_ADDRESS` for `GASBACK` is chosen to be `TBD` as it is the next available precompiled address set that is reserved for the RIP precompiles.

### Preinstall

A preinstall may be used instead of a precompile.

The precompile MUST be deployed at address `0x0000000000005533198cFd0b749148d5e3bd685d` using Nick's Factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with salt `0x0000000000000000000000000000000000000000d3a6e641651beb027832a915`.

The registry can be deployed to any EVM-compatible chain using the following transaction:

```
{
"to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
"value": "0x0",
"data": "0x0000000000000000000000000000000000000000d3a6e641651beb027832a9156080604052670c7d713b49da000060005560001960015561027a806100256000396000f3fe6080604052610064565b808202821583820483141761001d57600080fd5b92915050565b81600152607f60005360ff6021536060600082f061004057600080fd5b5050565b60008160051b6004016020810136101561005d57600080fd5b3592915050565b6002600160a01b03330361012e5760003560e01c62f714ce81036100a35761008c6000610044565b6100966001610044565b6100a08183610023565b50505b636467c4d781036100d1576100b86000610044565b670de0b6b3a76400008111156100cd57600080fd5b6000555b63b6351b36810361010a576100e66000610044565b6100f06001610044565b8060801c818311171561010257600080fd5b60801b176001555b63c53468f081036101235761011f6000610044565b6002555b506000805260206000f35b30330361013757fe5b6020361461014457600080fd5b60003560006002546001548060801b60801c80481061017b578160801c488111488203028282036101758289610009565b04965050505b505080600081146101c45763e97cf883600052836020524860405247606052604060006064601c855afa8060403d14166101b457600080fd5b50600051925060205193506101fd565b6101ce8448610009565b92506101dc83600054610009565b670de0b6b3a7640000900492504783106101fd57479250476101fd57600093505b5050811561022a575a6000806000803087fa1561021957600080fd5b825a8203101561022857600080fd5b505b801561023a5761023a3382610023565b8060005260206000f3fea264697066735822122097e072ef4f39d697a9ee91122228b63b7610b552fd84a84912fb5359fd22291364736f6c634300081b0033",
}
```

## Rationale

### Force transfer of Ether

This is so that the transfer will not be blocked in the case of a contract that does not implement a fallback function, and to keep transfer costs constant regardless of contract code.

### Reverting on error

This is to enable calling smart contracts to differentiate between actual errors versus receiving zero Ether.

## Backwards Compatibility

This precompile is backwards compatible with [EIP-6968](../eip-6968). The gas fees burned in the precompile and preinstall will not overlap with the regular CSR fees.

## Reference Implementation

The Solidity implementation of the preinstall is provided.

This example serves as a reference for implementation of the precompile which will be implemented in op-geth.

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.7;
/// @dev A contract that converts a portion of the gas burned into Ether.
/// This contract holds Ether deposited by the sequencer, which will be
/// redistributed to callers.
///
/// This contract is to be called with `abi.encode(uint256(gasToBurn))`.
/// It does a best effort attempt to consume `gasToBurn` amount of gas,
/// and gives up to `basefee * adjustedGasToBurn` amount of ETH to the
/// caller without invoking a new call context.
///
/// The `adjustedGasToBurn` is the amount of variable gas burned after
/// adjustments to manage the basefee. In this code, we will simply
/// call it `gasToBurn` for brevity.
///
/// The actual gas burned includes a constant flat overhead on top of
/// `adjustedGasToBurn`.
contract Gasback {
/// @dev The address authorized to configure the contract.
address internal constant _SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
/// @dev The denominator of the gasback ratio.
uint256 internal constant _GASBACK_RATIO_DENOMINATOR = 1 ether;
/// @dev Storage slot for the numerator for the ratio of
/// amount of gas burned to the amount of ETH given back.
uint256 internal constant _GASBACK_RATIO_NUMERATOR_SLOT = 0;
/// @dev Storage slot for the minimum and maximum bounds of the basefee
/// where the amount of gas burned will be linearly interpolated to zero.
uint256 internal constant _TAPER_BASEFEE_SLOT = 1;
/// @dev Storage slot for a custom smart contract with bespoke logic to determine
/// the final amount of gas to burn, and the final amount of ETH to give.
uint256 internal constant _CALCULATOR_SLOT = 2;
constructor() payable {
uint256 defaultGasbackRatioNumerator = 0.9 ether;
assembly {
// Initialize the default configurations.
sstore(_GASBACK_RATIO_NUMERATOR_SLOT, defaultGasbackRatioNumerator)
sstore(_TAPER_BASEFEE_SLOT, not(0))
}
}
fallback() external payable {
assembly {
function checkedMul(x, y) -> z {
z := mul(x, y)
if iszero(or(iszero(y), eq(div(z, y), x))) { revert(0x00, 0x00) }
}
function forceTransfer(amount, recipient) {
// We will use `SELFDESTRUCT` to send the ETH
// to ensure that it will never get blocked.
mstore(0x01, recipient) // Store the address in scratch space.
mstore8(0x00, 0x7f) // Opcode `PUSH32`.
mstore8(0x21, 0xff) // Opcode `SELFDESTRUCT`.
// For gas estimation.
if iszero(create(amount, 0x00, 0x60)) { revert(0x00, 0x00) }
}
function calldataArg(i) -> result {
let offset := add(0x04, shl(5, i))
if lt(calldatasize(), add(offset, 0x20)) { revert(0x00, 0x00) }
result := calldataload(offset)
}
// If it is called by the system address.
if eq(caller(), _SYSTEM_ADDRESS) {
let fnSel := shr(224, calldataload(0x00))
// `withdraw(uint256,address)`
if eq(fnSel, 0x00f714ce) {
let amount := calldataArg(0)
let recipient := calldataArg(1)
forceTransfer(amount, recipient)
}
// `setGasbackRatioNumerator(uint256)`
if eq(fnSel, 0x6467c4d7) {
let numerator := calldataArg(0)
if gt(numerator, _GASBACK_RATIO_DENOMINATOR) { revert(0x00, 0x00) }
sstore(_GASBACK_RATIO_NUMERATOR_SLOT, numerator)
}
// `setTaperBasefee(uint256,uint256)`
if eq(fnSel, 0xb6351b36) {
let min := calldataArg(0)
let max := calldataArg(1)
if or(gt(min, max), shr(128, max)) { revert(0x00, 0x00) }
sstore(_TAPER_BASEFEE_SLOT, or(min, shl(128, max)))
}
// `setCalculator(address)`
if eq(fnSel, 0xc53468f0) {
let calculator := calldataArg(0)
sstore(_CALCULATOR_SLOT, calculator)
}
// Return empty bytes to denote success.
mstore(0x00, 0)
return(0x00, 0x20)
}
// If it is a self-call, burn all gas efficiently with the invalid opcode.
if eq(caller(), address()) { invalid() }
// Revert if the calldatasize is not exactly 32.
if iszero(eq(calldatasize(), 0x20)) { revert(0x00, 0x00) }
let gasToBurn := calldataload(0x00) // This is also `adjustedGasToBurn`.
let amountToGive := 0
let calculator := sload(_CALCULATOR_SLOT)
// If the current basefee is high enough, taper off the `gasToBurn`.
let packed := sload(_TAPER_BASEFEE_SLOT)
let min := shr(128, shl(128, packed))
if iszero(lt(basefee(), min)) {
let max := shr(128, packed)
let diff := mul(sub(max, basefee()), gt(max, basefee()))
gasToBurn := div(checkedMul(gasToBurn, diff), sub(max, min))
}
switch calculator
case 0 {
// Compute the amount of ETH to give.
amountToGive := checkedMul(basefee(), gasToBurn)
amountToGive := checkedMul(sload(_GASBACK_RATIO_NUMERATOR_SLOT), amountToGive)
amountToGive := div(amountToGive, _GASBACK_RATIO_DENOMINATOR)
// So that the call will not revert if the contract has ran out of ETH.
if iszero(lt(amountToGive, selfbalance())) {
amountToGive := selfbalance()
// If the contract is empty, skip the gas burn
// so that callers don't waste gas. The last call to empty
// this contract may overpay in gas, but that is okay.
if iszero(selfbalance()) { gasToBurn := 0 }
}
}
default {
// The function on the calculator is:
// ```
// function calculate(
// uint256 gasToBurn,
// uint256 currentBasefee,
// uint256 currentSelfbalance
// ) external view returns (uint256 amountToGive, uint256 gasToBurn)
// ```
// If the calculator is a non-zero address, it will be responsible for
// making sure the `amountToGive` is low enough to prevent abuse.
mstore(0x00, 0xe97cf883) // `calculate(uint256,uint256,uint256)`.
mstore(0x20, gasToBurn)
mstore(0x40, basefee())
mstore(0x60, selfbalance())
let success := staticcall(gas(), calculator, 0x1c, 0x64, 0x00, 0x40)
if iszero(and(eq(returndatasize(), 0x40), success)) { revert(0x00, 0x00) }
amountToGive := mload(0x00)
gasToBurn := mload(0x20)
}
if gasToBurn {
let gasBefore := gas()
// Make a self-call to burn `gasToBurn`.
// Make sure that the staticcall reverts.
if staticcall(gasToBurn, address(), 0x00, 0x00, 0x00, 0x00) { revert(0x00, 0x00) }
// Require that the amount of gas burned is greater or equal to `gasToBurn`.
if lt(sub(gasBefore, gas()), gasToBurn) { revert(0x00, 0x00) }
}
if amountToGive { forceTransfer(amountToGive, caller()) }
// Return the `amountToGive`.
// A successful conversion will always result in a returndatasize of 32.
// This will help the caller to efficiently know if the call is successful.
mstore(0x00, amountToGive)
return(0x00, 0x20)
}
}
}
```

## Security Considerations

Implementers must make sure that the precompile does not cause a net increase in the total amount of Ether on the network.

EVM chains hosting any retroactive funding based on gas burned metrics should subtract the total amount of gas burned via the `GASBACK` precompile.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

0 comments on commit 83d5fcf

Please sign in to comment.