Skip to content

Commit

Permalink
feat: initial gas oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Nov 13, 2024
1 parent d82934e commit a999582
Show file tree
Hide file tree
Showing 11 changed files with 47,453 additions and 26,303 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
4 changes: 3 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,8 @@ 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"},
{access = "read", path = "./test/fixtures/fee_data_point.json"}
]

[fmt]
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/lib/openzeppelin-contracts
8 changes: 8 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 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;
60 changes: 53 additions & 7 deletions l1-contracts/test/fees/FeeModelTestPoints.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
// 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;
Expand All @@ -25,6 +32,12 @@ struct OracleInput {
int256 proving_cost_modifier;
}

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

struct ManaBaseFeeComponents {
uint256 congestion_cost;
uint256 congestion_multiplier;
Expand All @@ -35,31 +48,64 @@ struct ManaBaseFeeComponents {

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;
}

// Something with the l2 are not getting populated as they should.
struct TestPoint {
uint256 l1_block_number;
L1Fees l1_fees;
Header header;
uint256 l1_block_number;
uint256 l2_block_number;
uint256 l2_slot_number;
OracleInput oracle_input;
TestPointOutputs outputs;
Header parent_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 < dataPoints.length; i++) {
points.push(dataPoints[i]);
for (uint256 i = 0; i < data.points.length; i++) {
l1Metadata.push(data.l1_metadata[i]);
points.push(data.points[i]);
}
}

function logL1Metadata(uint256 _index) public {
L1Metadata memory metadata = l1Metadata[_index];
emit log_named_uint("base_fee", metadata.base_fee);
emit log_named_uint("blob_fee", metadata.blob_fee);
emit log_named_uint("block_number", metadata.block_number);
emit log_named_uint("timestamp", metadata.timestamp);
}

function logPoint(uint256 _index) public {
logPoint(points[_index]);
}

function logPoint(TestPoint memory point) public {
emit log("");
emit log_named_uint("Logging point for slot", point.l2_slot_number);
emit log_named_uint("\tl2_block_number", point.l2_block_number);
emit log_named_uint("\tl1_block_number", point.l1_block_number);
emit log_named_uint("\tl2_slot_number", point.l2_slot_number);
emit log_named_uint("\t\tbase fee", point.outputs.l1_fee_oracle_output.base_fee);
emit log_named_uint("\t\tblob fee", point.outputs.l1_fee_oracle_output.blob_fee);
}
}
74 changes: 72 additions & 2 deletions l1-contracts/test/fees/MinimalFeeModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,22 @@
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;
}

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

Expand All @@ -13,11 +27,28 @@ contract MinimalFeeModel {
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")))));

// The L1 base fees are "fixed" for a period of 5 slots.
// and is lagging 5 slots behind the current slot.
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 +65,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 getCurrentFee() 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 a999582

Please sign in to comment.