Skip to content

Commit

Permalink
feat: initial gas oracle (#9952)
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind authored Nov 20, 2024
1 parent 4c560ab commit e740d42
Show file tree
Hide file tree
Showing 11 changed files with 51,521 additions and 26,311 deletions.
17 changes: 17 additions & 0 deletions l1-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ To run the linter, simply run:
yarn lint
```

If the output is something to the tune of:

```bash
$ solhint --config ./.solhint.json "src/**/*.sol"
[solhint] Warning: Rule 'custom-errors' doesn't exist
[solhint] Warning: Rule 'private-func-leading-underscore' doesn't exist
[solhint] Warning: Rule 'private-vars-no-leading-underscore' doesn't exist
[solhint] Warning: Rule 'func-param-name-leading-underscore' doesn't exist
[solhint] Warning: Rule 'strict-override' doesn't exist
```
It is likely that it is a old cached version of the linter that is being used, you can update it as:
```bash
yarn add https://github.com/LHerskind/solhint\#master
```
---
# Slither & Slitherin
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'src'
out = 'out'
libs = ['lib']
solc = "0.8.27"
evm_version = 'cancun'

remappings = [
"@oz/=lib/openzeppelin-contracts/contracts/",
Expand All @@ -17,7 +18,7 @@ fs_permissions = [
{access = "read", path = "./test/fixtures/mixed_block_2.json"},
{access = "read", path = "./test/fixtures/empty_block_1.json"},
{access = "read", path = "./test/fixtures/empty_block_2.json"},
{access = "read", path = "./test/fixtures/fee_data_points.json"}
{access = "read", path = "./test/fixtures/fee_data_points.json"},
]

[fmt]
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/lib/openzeppelin-contracts
51 changes: 51 additions & 0 deletions l1-contracts/src/core/libraries/FeeMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ library FeeMath {
return true;
}

/**
* @notice Clamps the addition of a signed integer to a uint256
* Useful for running values, whose minimum value will be 0
* but should not throw if going below.
* @param _a The base value
* @param _b The value to add
* @return The clamped value
*/
function clampedAdd(uint256 _a, int256 _b) internal pure returns (uint256) {
if (_b >= 0) {
return _a + _b.toUint256();
Expand All @@ -62,6 +70,49 @@ library FeeMath {
return fakeExponential(MINIMUM_FEE_ASSET_PRICE, _numerator, FEE_ASSET_PRICE_UPDATE_FRACTION);
}

/**
* @notice An approximation of the exponential function: factor * e ** (numerator / denominator)
*
* The function is the same as used in EIP-4844
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md
*
* Approximated using a taylor series.
* For shorthand below, let `a = factor`, `x = numerator`, `d = denominator`
*
* f(x) = a
* + (a * x) / d
* + (a * x ** 2) / (2 * d ** 2)
* + (a * x ** 3) / (6 * d ** 3)
* + (a * x ** 4) / (24 * d ** 4)
* + (a * x ** 5) / (120 * d ** 5)
* + ...
*
* For integer precision purposes, we will multiply by the denominator for intermediary steps and then
* finally do a division by it.
* The notation below might look slightly strange, but it is to try to convey the program flow below.
*
* e(x) = ( a * d
* + a * d * x / d
* + ((a * d * x / d) * x) / (2 * d)
* + ((((a * d * x / d) * x) / (2 * d)) * x) / (3 * d)
* + ((((((a * d * x / d) * x) / (2 * d)) * x) / (3 * d)) * x) / (4 * d)
* + ((((((((a * d * x / d) * x) / (2 * d)) * x) / (3 * d)) * x) / (4 * d)) * x) / (5 * d)
* + ...
* ) / d
*
* The notation might make it a bit of a pain to look at, but f(x) and e(x) are the same.
* Gotta love integer math.
*
* @dev Notice that as _numerator grows, the computation will quickly overflow.
* As long as the `_denominator` is fairly small, it won't bring us back down to not overflow
* For our purposes, this is acceptable, as if we have a fee that is so high that it would overflow and throw
* then we would have other problems.
*
* @param _factor The base value
* @param _numerator The numerator
* @param _denominator The denominator
* @return The approximated value `_factor * e ** (_numerator / _denominator)`
*/
function fakeExponential(uint256 _factor, uint256 _numerator, uint256 _denominator)
private
pure
Expand Down
7 changes: 6 additions & 1 deletion l1-contracts/src/core/libraries/TimeMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ function addSlot(Slot _a, Slot _b) pure returns (Slot) {
return Slot.wrap(Slot.unwrap(_a) + Slot.unwrap(_b));
}

function subSlot(Slot _a, Slot _b) pure returns (Slot) {
return Slot.wrap(Slot.unwrap(_a) - Slot.unwrap(_b));
}

function eqSlot(Slot _a, Slot _b) pure returns (bool) {
return Slot.unwrap(_a) == Slot.unwrap(_b);
}
Expand Down Expand Up @@ -195,5 +199,6 @@ using {
gtSlot as >,
lteSlot as <=,
ltSlot as <,
addSlot as +
addSlot as +,
subSlot as -
} for Slot global;
56 changes: 45 additions & 11 deletions l1-contracts/test/fees/FeeModelTestPoints.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,41 @@
// solhint-disable var-name-mixedcase
pragma solidity >=0.8.27;

import {Test} from "forge-std/Test.sol";
import {TestBase} from "../base/Base.sol";

// Remember that foundry json parsing is alphabetically done, so you MUST
// sort the struct fields alphabetically or prepare for a headache.

struct L1Metadata {
uint256 base_fee;
uint256 blob_fee;
uint256 block_number;
uint256 timestamp;
}

struct L1Fees {
uint256 base_fee;
uint256 blob_fee;
}

struct Header {
struct FeeHeader {
uint256 excess_mana;
uint256 fee_asset_price_numerator;
uint256 mana_used;
uint256 proving_cast_per_mana_numerator;
uint256 proving_cost_per_mana_numerator;
}

struct OracleInput {
int256 fee_asset_price_modifier;
int256 proving_cost_modifier;
}

struct L1GasOracleValues {
L1Fees post;
L1Fees pre;
uint256 slot_of_change;
}

struct ManaBaseFeeComponents {
uint256 congestion_cost;
uint256 congestion_multiplier;
Expand All @@ -33,33 +46,54 @@ struct ManaBaseFeeComponents {
uint256 proving_cost;
}

struct BlockHeader {
uint256 blobs_needed;
uint256 block_number;
uint256 l1_block_number;
uint256 mana_spent;
uint256 size_in_fields;
uint256 slot_number;
uint256 timestamp;
}

struct TestPointOutputs {
uint256 fee_asset_price_at_execution;
L1Fees l1_fee_oracle_output;
L1GasOracleValues l1_gas_oracle_values;
ManaBaseFeeComponents mana_base_fee_components_in_fee_asset;
ManaBaseFeeComponents mana_base_fee_components_in_wei;
}

struct TestPoint {
uint256 l1_block_number;
L1Fees l1_fees;
Header header;
BlockHeader block_header;
FeeHeader fee_header;
OracleInput oracle_input;
TestPointOutputs outputs;
Header parent_header;
FeeHeader parent_fee_header;
}

contract FeeModelTestPoints is Test {
struct FullFeeData {
L1Metadata[] l1_metadata;
TestPoint[] points;
}

contract FeeModelTestPoints is TestBase {
L1Metadata[] public l1Metadata;
TestPoint[] public points;

constructor() {
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/test/fixtures/fee_data_points.json");
string memory json = vm.readFile(path);
bytes memory jsonBytes = vm.parseJson(json);
TestPoint[] memory dataPoints = abi.decode(jsonBytes, (TestPoint[]));
FullFeeData memory data = abi.decode(jsonBytes, (FullFeeData));

for (uint256 i = 0; i < data.l1_metadata.length; i++) {
l1Metadata.push(data.l1_metadata[i]);
}

for (uint256 i = 0; i < dataPoints.length; i++) {
points.push(dataPoints[i]);
for (uint256 i = 0; i < data.points.length; i++) {
points.push(data.points[i]);
}
}
}
80 changes: 74 additions & 6 deletions l1-contracts/test/fees/MinimalFeeModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,50 @@
pragma solidity >=0.8.27;

import {FeeMath, OracleInput} from "@aztec/core/libraries/FeeMath.sol";
import {Timestamp, TimeFns, Slot} from "@aztec/core/libraries/TimeMath.sol";
import {Vm} from "forge-std/Vm.sol";

contract MinimalFeeModel {
struct BaseFees {
uint256 baseFee;
uint256 blobFee;
}

// This actually behaves pretty close to the slow updates.
struct L1BaseFees {
BaseFees pre;
BaseFees post;
Slot slotOfChange;
}

struct DataPoint {
uint256 provingCostNumerator;
uint256 feeAssetPriceNumerator;
}

contract MinimalFeeModel is TimeFns {
using FeeMath for OracleInput;
using FeeMath for uint256;

struct DataPoint {
uint256 provingCostNumerator;
uint256 feeAssetPriceNumerator;
}
// This is to allow us to use the cheatcodes for blobbasefee as foundry does not play nice
// with the block.blobbasefee value if using cheatcodes to alter it.
Vm internal constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

Slot public constant LIFETIME = Slot.wrap(5);
Slot public constant LAG = Slot.wrap(2);
Timestamp public immutable GENESIS_TIMESTAMP;

uint256 public populatedThrough = 0;
mapping(uint256 _slotNumber => DataPoint _dataPoint) public dataPoints;

constructor() {
L1BaseFees public l1BaseFees;

constructor(uint256 _slotDuration, uint256 _epochDuration) TimeFns(_slotDuration, _epochDuration) {
GENESIS_TIMESTAMP = Timestamp.wrap(block.timestamp);
dataPoints[0] = DataPoint({provingCostNumerator: 0, feeAssetPriceNumerator: 0});

l1BaseFees.pre = BaseFees({baseFee: 1 gwei, blobFee: 1});
l1BaseFees.post = BaseFees({baseFee: block.basefee, blobFee: _getBlobBaseFee()});
l1BaseFees.slotOfChange = LIFETIME;
}

// See the `add_slot` function in the `fee-model.ipynb` notebook for more context.
Expand All @@ -34,11 +63,50 @@ contract MinimalFeeModel {
});
}

/**
* @notice Take a snapshot of the l1 fees
* @dev Can only be called AFTER the scheduled change has passed.
* This is to ensure that the block proposers have time to react and it will not change
* under their feet, while also ensuring that the "queued" will not be waiting indefinitely.
*/
function photograph() public {
Slot slot = getCurrentSlot();
// The slot where we find a new queued value acceptable
Slot acceptableSlot = l1BaseFees.slotOfChange + (LIFETIME - LAG);

if (slot < acceptableSlot) {
return;
}

// If we are at or beyond the scheduled change, we need to update the "current" value
l1BaseFees.pre = l1BaseFees.post;
l1BaseFees.post = BaseFees({baseFee: block.basefee, blobFee: _getBlobBaseFee()});
l1BaseFees.slotOfChange = slot + LAG;
}

function getFeeAssetPrice(uint256 _slotNumber) public view returns (uint256) {
return FeeMath.feeAssetPriceModifier(dataPoints[_slotNumber].feeAssetPriceNumerator);
}

function getProvingCost(uint256 _slotNumber) public view returns (uint256) {
return FeeMath.provingCostPerMana(dataPoints[_slotNumber].provingCostNumerator);
}

function getCurrentL1Fees() public view returns (BaseFees memory) {
Slot slot = getCurrentSlot();
if (slot < l1BaseFees.slotOfChange) {
return l1BaseFees.pre;
}
return l1BaseFees.post;
}

function getCurrentSlot() public view returns (Slot) {
Timestamp currentTime = Timestamp.wrap(block.timestamp);
return TimeFns.slotFromTimestamp(currentTime - GENESIS_TIMESTAMP);
}

function _getBlobBaseFee() internal view returns (uint256) {
// This should really be `block.blobbasefee` but that does NOT play well with forge and cheatcodes :)
return VM.getBlobBaseFee();
}
}
Loading

0 comments on commit e740d42

Please sign in to comment.