diff --git a/contracts-bedrock/.gas-snapshot b/contracts-bedrock/.gas-snapshot index bdef81c0d1e94..eb44a7f3af228 100644 --- a/contracts-bedrock/.gas-snapshot +++ b/contracts-bedrock/.gas-snapshot @@ -1,4 +1,17 @@ CrossDomainHashing_Test:test_l2TransactionHash() (gas: 78639) +GasPriceOracle_Test:test_baseFee() (gas: 11216) +GasPriceOracle_Test:test_gasPrice() (gas: 11205) +GasPriceOracle_Test:test_l1BaseFee() (gas: 10626) +GasPriceOracle_Test:test_onlyOwnerSetDecimals() (gas: 10575) +GasPriceOracle_Test:test_onlyOwnerSetOverhead() (gas: 10599) +GasPriceOracle_Test:test_onlyOwnerSetScalar() (gas: 10640) +GasPriceOracle_Test:test_owner() (gas: 9762) +GasPriceOracle_Test:test_setDecimals() (gas: 36798) +GasPriceOracle_Test:test_setGasPriceReverts() (gas: 11659) +GasPriceOracle_Test:test_setL1BaseFeeReverts() (gas: 11658) +GasPriceOracle_Test:test_setOverhead() (gas: 36767) +GasPriceOracle_Test:test_setScalar() (gas: 36840) +GasPriceOracle_Test:test_storageLayout() (gas: 86683) L1BlockTest:test_basefee() (gas: 7575) L1BlockTest:test_hash() (gas: 7552) L1BlockTest:test_number() (gas: 7651) diff --git a/contracts-bedrock/contracts/L2/GasPriceOracle.sol b/contracts-bedrock/contracts/L2/GasPriceOracle.sol new file mode 100644 index 0000000000000..8e1c65184c6f1 --- /dev/null +++ b/contracts-bedrock/contracts/L2/GasPriceOracle.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/* External Imports */ +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Lib_BedrockPredeployAddresses } from "../libraries/Lib_BedrockPredeployAddresses.sol"; +import { L1Block } from "../L2/L1Block.sol"; + +/** + * @title GasPriceOracle + * @dev This contract maintains the variables responsible for computing the L1 + * portion of the total fee charged on L2. The values stored in the contract + * are looked up as part of the L2 state transition function and used to compute + * the total fee paid by the user. + * The contract exposes an API that is useful for knowing how large the L1 + * portion of their transaction fee will be. + * This predeploy is found at 0x420000000000000000000000000000000000000F in the + * L2 state. + * This contract should be behind an upgradable proxy such that when the gas + * prices change, the values can be updated accordingly. + */ +contract GasPriceOracle is Ownable { + /************* + * Variables * + *************/ + + // backwards compatibility + uint256 internal spacer0; + uint256 internal spacer1; + + // Amortized cost of batch submission per transaction + uint256 public overhead; + // Value to scale the fee up by + uint256 public scalar; + // Number of decimals of the scalar + uint256 public decimals; + + /*************** + * Constructor * + ***************/ + + /** + * @param _owner Address that will initially own this contract. + */ + constructor(address _owner) Ownable() { + transferOwnership(_owner); + } + + /********** + * Events * + **********/ + + event OverheadUpdated(uint256); + event ScalarUpdated(uint256); + event DecimalsUpdated(uint256); + + /******************** + * Public Functions * + ********************/ + + // legacy backwards compat + function gasPrice() public returns (uint256) { + return block.basefee; + } + + function baseFee() public returns (uint256) { + return block.basefee; + } + + function l1BaseFee() public view returns (uint256) { + return L1Block(Lib_BedrockPredeployAddresses.L1_BLOCK_ATTRIBUTES).basefee(); + } + + /** + * Allows the owner to modify the overhead. + * @param _overhead New overhead + */ + // slither-disable-next-line external-function + function setOverhead(uint256 _overhead) public onlyOwner { + overhead = _overhead; + emit OverheadUpdated(_overhead); + } + + /** + * Allows the owner to modify the scalar. + * @param _scalar New scalar + */ + // slither-disable-next-line external-function + function setScalar(uint256 _scalar) public onlyOwner { + scalar = _scalar; + emit ScalarUpdated(_scalar); + } + + /** + * Allows the owner to modify the decimals. + * @param _decimals New decimals + */ + // slither-disable-next-line external-function + function setDecimals(uint256 _decimals) public onlyOwner { + decimals = _decimals; + emit DecimalsUpdated(_decimals); + } + + /** + * Computes the L1 portion of the fee + * based on the size of the RLP encoded tx + * and the current l1BaseFee + * @param _data Unsigned RLP encoded tx, 6 elements + * @return L1 fee that should be paid for the tx + */ + // slither-disable-next-line external-function + function getL1Fee(bytes memory _data) public view returns (uint256) { + uint256 l1GasUsed = getL1GasUsed(_data); + uint256 l1Fee = l1GasUsed * l1BaseFee(); + uint256 divisor = 10**decimals; + uint256 unscaled = l1Fee * scalar; + uint256 scaled = unscaled / divisor; + return scaled; + } + + // solhint-disable max-line-length + /** + * Computes the amount of L1 gas used for a transaction + * The overhead represents the per batch gas overhead of + * posting both transaction and state roots to L1 given larger + * batch sizes. + * 4 gas for 0 byte + * https://github.com/ethereum/go-ethereum/blob/9ada4a2e2c415e6b0b51c50e901336872e028872/params/protocol_params.go#L33 + * 16 gas for non zero byte + * https://github.com/ethereum/go-ethereum/blob/9ada4a2e2c415e6b0b51c50e901336872e028872/params/protocol_params.go#L87 + * This will need to be updated if calldata gas prices change + * Account for the transaction being unsigned + * Padding is added to account for lack of signature on transaction + * 1 byte for RLP V prefix + * 1 byte for V + * 1 byte for RLP R prefix + * 32 bytes for R + * 1 byte for RLP S prefix + * 32 bytes for S + * Total: 68 bytes of padding + * @param _data Unsigned RLP encoded tx, 6 elements + * @return Amount of L1 gas used for a transaction + */ + // solhint-enable max-line-length + function getL1GasUsed(bytes memory _data) public view returns (uint256) { + uint256 total = 0; + uint256 length = _data.length; + for (uint256 i = 0; i < length; i++) { + if (_data[i] == 0) { + total += 4; + } else { + total += 16; + } + } + uint256 unsigned = total + overhead; + return unsigned + (68 * 16); + } +} diff --git a/contracts-bedrock/contracts/test/GasPriceOracle.t.sol b/contracts-bedrock/contracts/test/GasPriceOracle.t.sol new file mode 100644 index 0000000000000..9ef3614afa42b --- /dev/null +++ b/contracts-bedrock/contracts/test/GasPriceOracle.t.sol @@ -0,0 +1,170 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import { CommonTest } from "./CommonTest.t.sol"; +import { GasPriceOracle } from "../L2/GasPriceOracle.sol"; +import { L1Block } from "../L2/L1Block.sol"; +import { Lib_BedrockPredeployAddresses } from "../libraries/Lib_BedrockPredeployAddresses.sol"; + +import { console } from "forge-std/console.sol"; + +contract GasPriceOracle_Test is CommonTest { + + event OverheadUpdated(uint256); + event ScalarUpdated(uint256); + event DecimalsUpdated(uint256); + + GasPriceOracle gasOracle; + L1Block l1Block; + address depositor; + + function setUp() external { + // place the L1Block contract at the predeploy address + vm.etch( + Lib_BedrockPredeployAddresses.L1_BLOCK_ATTRIBUTES, + address(new L1Block()).code + ); + + l1Block = L1Block(Lib_BedrockPredeployAddresses.L1_BLOCK_ATTRIBUTES); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + + // We are not setting the gas oracle at its predeploy + // address for simplicity purposes. Nothing in this test + // requires it to be at a particular address + gasOracle = new GasPriceOracle(alice); + + // set the initial L1 context values + uint64 number = 10; + uint64 timestamp = 11; + uint256 basefee = 100; + bytes32 hash = bytes32(uint256(64)); + uint64 sequenceNumber = 0; + + vm.prank(depositor); + l1Block.setL1BlockValues( + number, + timestamp, + basefee, + hash, + sequenceNumber + ); + } + + function test_owner() external { + // alice is passed into the constructor of the gasOracle + assertEq(gasOracle.owner(), alice); + } + + function test_storageLayout() external { + // the overhead is at slot 3 + vm.prank(gasOracle.owner()); + gasOracle.setOverhead(456); + assertEq( + 456, + uint256(vm.load(address(gasOracle), bytes32(uint256(3)))) + ); + + // scalar is at slot 4 + vm.prank(gasOracle.owner()); + gasOracle.setScalar(333); + assertEq( + 333, + uint256(vm.load(address(gasOracle), bytes32(uint256(4)))) + ); + + // decimals is at slot 5 + vm.prank(gasOracle.owner()); + gasOracle.setDecimals(222); + assertEq( + 222, + uint256(vm.load(address(gasOracle), bytes32(uint256(5)))) + ); + } + + function test_l1BaseFee() external { + uint256 l1BaseFee = gasOracle.l1BaseFee(); + assertEq(l1BaseFee, 100); + } + + function test_gasPrice() external { + vm.fee(100); + uint256 gasPrice = gasOracle.gasPrice(); + console.log(gasPrice); + assertEq(gasPrice, 100); + } + + function test_baseFee() external { + vm.fee(64); + uint256 gasPrice = gasOracle.baseFee(); + console.log(gasPrice); + assertEq(gasPrice, 64); + } + + function test_setGasPriceReverts() external { + vm.prank(gasOracle.owner()); + (bool success, bytes memory returndata) = address(gasOracle).call( + abi.encodeWithSignature( + "setGasPrice(uint256)", + 1 + ) + ); + + assertEq(success, false); + assertEq(returndata, hex""); + } + + function test_setL1BaseFeeReverts() external { + vm.prank(gasOracle.owner()); + (bool success, bytes memory returndata) = address(gasOracle).call( + abi.encodeWithSignature( + "setL1BaseFee(uint256)", + 1 + ) + ); + + assertEq(success, false); + assertEq(returndata, hex""); + } + + function test_setOverhead() external { + vm.expectEmit(true, true, true, true); + emit OverheadUpdated(1234); + + vm.prank(gasOracle.owner()); + gasOracle.setOverhead(1234); + assertEq(gasOracle.overhead(), 1234); + } + + function test_onlyOwnerSetOverhead() external { + vm.expectRevert("Ownable: caller is not the owner"); + gasOracle.setOverhead(0); + } + + function test_setScalar() external { + vm.expectEmit(true, true, true, true); + emit ScalarUpdated(666); + + vm.prank(gasOracle.owner()); + gasOracle.setScalar(666); + assertEq(gasOracle.scalar(), 666); + } + + function test_onlyOwnerSetScalar() external { + vm.expectRevert("Ownable: caller is not the owner"); + gasOracle.setScalar(0); + } + + function test_setDecimals() external { + vm.expectEmit(true, true, true, true); + emit DecimalsUpdated(18); + + vm.prank(gasOracle.owner()); + gasOracle.setDecimals(18); + assertEq(gasOracle.decimals(), 18); + } + + function test_onlyOwnerSetDecimals() external { + vm.expectRevert("Ownable: caller is not the owner"); + gasOracle.setDecimals(0); + } +}