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

feat: initial gas oracle #9952

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

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

is this function taken from an existing library? if so can you add the source

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same function described in EIP-4844. Can add a ref to it.

*
* 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