Skip to content
Open
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
8 changes: 6 additions & 2 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,12 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
return StakingLib.LEGACY_SLASHER_DRAIN_WINDOW;
}

function getBurnAddress() external pure override(IRollup) returns (address) {
return address(bytes20("CUAUHXICALLI"));
function getProtocolFeeRecipient() external view override(IRollup) returns (address) {
return RewardExtLib.getProtocolFeeRecipient();
}

function getProtocolFeeMargin() external view override(IRollup) returns (uint16) {
return RewardExtLib.getProtocolFeeMargin();
}

/**
Expand Down
24 changes: 24 additions & 0 deletions l1-contracts/src/core/RollupCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,30 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
FeeLib.updateProvingCostPerMana(_provingCostPerMana);
}

/**
* @notice Updates the protocol fee margin applied on top of operator cost in the mana base fee
* @dev Only callable by owner. Increases are rate-limited (30-day cooldown, x3/2 step on the fee
* multiplier); decreases are immediate. Setting the current value is a no-op and emits no
* event.
* @param _protocolFeeMarginBps The new margin in basis points
*/
function setProtocolFeeMargin(uint16 _protocolFeeMarginBps) external override(IRollupCore) onlyOwner {
(bool changed, uint16 oldBps) = RewardExtLib.updateProtocolFeeMargin(_protocolFeeMarginBps);
if (changed) {
emit IRollupCore.ProtocolFeeMarginUpdated(oldBps, _protocolFeeMarginBps);
}
}

/**
* @notice Updates the recipient of the protocol fee tranche of the reward waterfall
* @dev Only callable by owner. Rejects the zero address.
* @param _recipient The new protocol fee recipient
*/
function setProtocolFeeRecipient(address _recipient) external override(IRollupCore) onlyOwner {
address oldRecipient = RewardExtLib.updateProtocolFeeRecipient(_recipient);
emit IRollupCore.ProtocolFeeRecipientUpdated(oldRecipient, _recipient);
}

/**
* @notice Updates the configuration for the staking entry queue
* @dev Only callable by owner. Controls how validators enter the active set.
Expand Down
8 changes: 7 additions & 1 deletion l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ interface IRollupCore {
event RewardConfigUpdated(MutableRewardConfig rewardConfig);
event ManaTargetUpdated(uint256 indexed manaTarget);
event PrunedPending(uint256 provenCheckpointNumber, uint256 pendingCheckpointNumber);
event ProtocolFeeMarginUpdated(uint16 oldBps, uint16 newBps);
event ProtocolFeeRecipientUpdated(address oldRecipient, address newRecipient);

function claimSequencerRewards(address _recipient) external returns (uint256);
function claimProverRewards(address _recipient, Epoch[] memory _epochs) external returns (uint256);
Expand All @@ -127,6 +129,9 @@ interface IRollupCore {

function setProvingCostPerMana(EthValue _provingCostPerMana) external;

function setProtocolFeeMargin(uint16 _protocolFeeMarginBps) external;
function setProtocolFeeRecipient(address _recipient) external;

function propose(
ProposeArgs calldata _args,
CommitteeAttestations memory _attestations,
Expand Down Expand Up @@ -231,7 +236,8 @@ interface IRollup is IRollupCore, IHaveVersion {
function getFeeAsset() external view returns (IERC20);
function getFeeAssetPortal() external view returns (IFeeJuicePortal);
function getRewardDistributor() external view returns (IRewardDistributor);
function getBurnAddress() external view returns (address);
function getProtocolFeeRecipient() external view returns (address);
function getProtocolFeeMargin() external view returns (uint16);

function getInbox() external view returns (IInbox);
function getOutbox() external view returns (IOutbox);
Expand Down
3 changes: 3 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ library Errors {
error FeeLib__ProvingCostAboveCeiling(uint256 provided, uint256 maximum);
error FeeLib__ProvingCostCooldown(uint256 nextAllowed);
error FeeLib__ProvingCostStepExceeded(uint256 current, uint256 requested);
error FeeLib__ProtocolFeeMarginCooldown(uint256 nextAllowed);
error FeeLib__ProtocolFeeMarginStepExceeded(uint256 current, uint256 requested);

// SignatureLib (duplicated)
error SignatureLib__InvalidSignature(address, address); // 0xd9cbae6c
Expand All @@ -223,6 +225,7 @@ library Errors {

error RewardLib__InvalidSequencerBps();
error RewardLib__ZeroShares(address prover);
error RewardLib__InvalidProtocolFeeRecipient();

// SlashingProposer
error SlashingProposer__InvalidSignature();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ function subEthValue(EthValue _a, EthValue _b) pure returns (EthValue) {

using {addEthValue as +, subEthValue as -} for EthValue global;

// 32 bit manaTarget, 128 bit congestionUpdateFraction, 64 bit provingCostPerMana
// 16 bit protocolFeeMarginBps, 32 bit manaTarget, 128 bit congestionUpdateFraction, 64 bit provingCostPerMana
type CompressedFeeConfig is uint256;

struct FeeConfig {
uint256 manaTarget;
uint256 congestionUpdateFraction;
EthValue provingCostPerMana;
uint256 protocolFeeMarginBps;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

should this be uint16?

}

/// @notice Library for converting between ETH and fee asset values using the price oracle.
Expand Down Expand Up @@ -89,6 +90,7 @@ library PriceLib {
library FeeConfigLib {
using SafeCast for uint256;

uint256 private constant MASK_16_BITS = 0xFFFF;
uint256 private constant MASK_32_BITS = 0xFFFFFFFF;
uint256 private constant MASK_64_BITS = 0xFFFFFFFFFFFFFFFF;
uint256 private constant MASK_128_BITS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
Expand All @@ -105,11 +107,16 @@ library FeeConfigLib {
return EthValue.wrap(CompressedFeeConfig.unwrap(_compressedFeeConfig) & MASK_64_BITS);
}

function getProtocolFeeMarginBps(CompressedFeeConfig _compressedFeeConfig) internal pure returns (uint256) {
return (CompressedFeeConfig.unwrap(_compressedFeeConfig) >> 224) & MASK_16_BITS;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let's look at this carefully

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks ok to me

}

function compress(FeeConfig memory _config) internal pure returns (CompressedFeeConfig) {
uint256 value = 0;
value |= uint256(EthValue.unwrap(_config.provingCostPerMana).toUint64());
value |= uint256(_config.congestionUpdateFraction.toUint128()) << 64;
value |= uint256(_config.manaTarget.toUint32()) << 192;
value |= uint256(_config.protocolFeeMarginBps.toUint16()) << 224;

return CompressedFeeConfig.wrap(value);
}
Expand All @@ -118,7 +125,8 @@ library FeeConfigLib {
return FeeConfig({
provingCostPerMana: getProvingCostPerMana(_compressedFeeConfig),
congestionUpdateFraction: getCongestionUpdateFraction(_compressedFeeConfig),
manaTarget: getManaTarget(_compressedFeeConfig)
manaTarget: getManaTarget(_compressedFeeConfig),
protocolFeeMarginBps: getProtocolFeeMarginBps(_compressedFeeConfig)
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {SafeCast} from "@oz/utils/math/SafeCast.sol";
/*struct CompressedFeeHeader {
uint1 preHeat;
uint63 proverCost; Max value: 9.2233720369E18
uint64 congestionCost;
uint64 protocolFee;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

let's verify that this is both the congestionCost + protocolFeeMargin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

checks out

uint48 ethPerFeeAsset;
uint48 excessMana;
uint32 manaUsed;
Expand All @@ -22,7 +22,7 @@ struct FeeHeader {
uint256 excessMana;
uint256 manaUsed;
uint256 ethPerFeeAsset;
uint256 congestionCost;
uint256 protocolFee;
uint256 proverCost;
}

Expand Down Expand Up @@ -91,7 +91,7 @@ library FeeHeaderLib {
return (CompressedFeeHeader.unwrap(_compressedFeeHeader) >> 80) & MASK_48_BITS;
}

function getCongestionCost(CompressedFeeHeader _compressedFeeHeader) internal pure returns (uint256) {
function getProtocolFee(CompressedFeeHeader _compressedFeeHeader) internal pure returns (uint256) {
return (CompressedFeeHeader.unwrap(_compressedFeeHeader) >> 128) & MASK_64_BITS;
}

Expand All @@ -106,9 +106,9 @@ library FeeHeaderLib {
// Cap excessMana to uint48 max to prevent overflow during compression.
value |= Math.min(_feeHeader.excessMana, MASK_48_BITS) << 32;
value |= uint256(_feeHeader.ethPerFeeAsset.toUint48()) << 80;
// Cap congestionCost to uint64 max to prevent overflow during compression.
// Cap protocolFee to uint64 max to prevent overflow during compression.
// The uncapped value is still used for fee validation; this only affects storage.
value |= Math.min(_feeHeader.congestionCost, MASK_64_BITS) << 128;
value |= Math.min(_feeHeader.protocolFee, MASK_64_BITS) << 128;
// Cap proverCost to uint63 max to prevent overflow during compression.
value |= Math.min(_feeHeader.proverCost, MASK_63_BITS) << 192;

Expand All @@ -127,15 +127,15 @@ library FeeHeaderLib {
value >>= 48;
uint256 ethPerFeeAsset = value & MASK_48_BITS;
value >>= 48;
uint256 congestionCost = value & MASK_64_BITS;
uint256 protocolFee = value & MASK_64_BITS;
value >>= 64;
uint256 proverCost = value & MASK_63_BITS;

return FeeHeader({
manaUsed: uint256(manaUsed),
excessMana: uint256(excessMana),
ethPerFeeAsset: uint256(ethPerFeeAsset),
congestionCost: uint256(congestionCost),
protocolFee: uint256(protocolFee),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is supposed to be \mu(sequencerCostPerMana + proverCostPerMana) + congestionCost

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe this commment is out of date? The formula I see is

cost = sequencerCostPerMana + proverCostPerMana
protocolAndCongestionMultiplier = (1 + protocolFeePercentage) * congestionMultiplier
fee = cost * protocolAndCongestionMultiplier

basically the protocol margin compounds with congestion rather than adding on top.

proverCost: uint256(proverCost)
});
}
Expand Down
103 changes: 95 additions & 8 deletions l1-contracts/src/core/libraries/rollup/FeeLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ uint256 constant MAX_FEE_ASSET_PRICE_MODIFIER_BPS = 100;
uint256 constant L1_GAS_PER_CHECKPOINT_PROPOSED = 300_000;
uint256 constant L1_GAS_PER_EPOCH_VERIFIED = 3_600_000;

// The uncongested baseline of the congestion multiplier is (1 + mu) * 1e9, where mu is the
// protocol fee margin: congestionMultiplier scales this minimum by (10_000 + marginBps) / 10_000.
// At a margin of 0 the baseline is exactly 1e9.
uint256 constant MINIMUM_CONGESTION_MULTIPLIER = 1e9;

// The magic values are used to have the fakeExponential case where
Expand Down Expand Up @@ -94,12 +97,26 @@ uint256 constant MIN_PROVING_COST_PER_MANA = 2;
// that proving costs will go down.
uint256 constant MAX_INITIAL_PROVING_COST_PER_MANA = 2e8;

/*
* Protocol-fee-margin rate limit
*
* `setProtocolFeeMargin` multiplies the fee users pay, so increases are constrained to a bounded
* multiplicative step per cooldown, mirroring the proving-cost limiter above. The bounded quantity
* is the fee multiplier (10_000 + marginBps), not the margin itself, so each step raises the
* pinned fee by at most x3/2. Decreases are immediate and unrestricted (floor 0 is structural via
* uint16). `protocolMarginLastUpdate == 0` after `initialize`, so the first post-init update is
* not gated by the cooldown; the 30-day cadence engages after that (decreases stamp it too).
*/
uint256 constant PROTOCOL_FEE_MARGIN_UPDATE_INTERVAL = 30 days;
uint256 constant PROTOCOL_FEE_MARGIN_STEP_NUM = 3;
uint256 constant PROTOCOL_FEE_MARGIN_STEP_DEN = 2;

struct OracleInput {
int256 feeAssetPriceModifier;
}

struct ManaMinFeeComponents {
uint256 congestionCost;
uint256 protocolFee;
uint256 congestionMultiplier;
uint256 sequencerCost;
uint256 proverCost;
Expand All @@ -109,6 +126,7 @@ struct FeeStore {
CompressedFeeConfig config;
L1GasOracleValues l1GasOracleValues;
uint64 provingCostLastUpdate;
uint64 protocolMarginLastUpdate;
}

library FeeLib {
Expand Down Expand Up @@ -168,7 +186,8 @@ library FeeLib {
feeStore.config = FeeConfig({
manaTarget: _manaTarget,
congestionUpdateFraction: _manaTarget * MAGIC_CONGESTION_VALUE_MULTIPLIER / MAGIC_CONGESTION_VALUE_DIVISOR,
provingCostPerMana: _provingCostPerMana
provingCostPerMana: _provingCostPerMana,
protocolFeeMarginBps: 0
}).compress();

feeStore.l1GasOracleValues = L1GasOracleValues({
Expand Down Expand Up @@ -220,6 +239,49 @@ library FeeLib {
feeStore.provingCostLastUpdate = uint64(block.timestamp);
}

/**
* @notice Updates the protocol fee margin (in basis points) applied on top of operator cost.
* @dev Idempotent: setting the current value is a no-op (no state change, no cooldown stamp).
* Increases are gated by the 30-day cooldown (first-ever update exempt) and the x3/2 step
* on the fee multiplier `(10_000 + bps)`. Decreases are immediate and unrestricted but
* still stamp the cooldown. The uint16 parameter makes values above 65535 unrepresentable
* at the ABI boundary, so the reverting `toUint16` inside `compress` can never fire from
* this path and later `compress` round-trips (updateManaTarget, updateProvingCostPerMana)
* never see an out-of-range margin.
* @param _bps The new protocol fee margin in basis points
* @return changed Whether state was mutated (false for the idempotent no-op)
* @return oldBps The margin in effect before this call
*/
function updateProtocolFeeMargin(uint16 _bps) internal returns (bool changed, uint16 oldBps) {
FeeStore storage feeStore = getStorage();
FeeConfig memory config = feeStore.config.decompress();

oldBps = uint16(config.protocolFeeMarginBps);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isn't config.protocolFeeMarginBps already a uint16? Lower in this function we update it but we don't explicitly upcast.


if (_bps == oldBps) {
return (false, oldBps);
}

if (_bps > oldBps) {
uint256 nextAllowed = uint256(feeStore.protocolMarginLastUpdate) + PROTOCOL_FEE_MARGIN_UPDATE_INTERVAL;
require(
feeStore.protocolMarginLastUpdate == 0 || block.timestamp >= nextAllowed,
Errors.FeeLib__ProtocolFeeMarginCooldown(nextAllowed)
);
require(
(10_000 + uint256(_bps)) * PROTOCOL_FEE_MARGIN_STEP_DEN
<= (10_000 + uint256(oldBps)) * PROTOCOL_FEE_MARGIN_STEP_NUM,
Errors.FeeLib__ProtocolFeeMarginStepExceeded(oldBps, _bps)
);
Comment on lines +271 to +275

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is ok but I don't think adding the 10000 constant is necessary (if my maths is correct)

}

config.protocolFeeMarginBps = _bps;
feeStore.config = config.compress();
feeStore.protocolMarginLastUpdate = uint64(block.timestamp);

return (true, oldBps);
}

function updateL1GasFeeOracle() internal {
Slot slot = Timestamp.wrap(block.timestamp).slotFromTimestamp();
// The slot where we find a new queued value acceptable
Expand All @@ -242,7 +304,7 @@ library FeeLib {
uint256 _checkpointNumber,
int256 _feeAssetPriceModifierBps,
uint256 _manaUsed,
uint256 _congestionCost,
uint256 _protocolFee,
uint256 _proverCost
) internal view returns (FeeHeader memory) {
require(
Expand All @@ -254,7 +316,7 @@ library FeeLib {
excessMana: FeeLib.computeExcessMana(parentFeeHeader),
ethPerFeeAsset: FeeLib.computeNewEthPerFeeAsset(parentFeeHeader.getEthPerFeeAsset(), _feeAssetPriceModifierBps),
manaUsed: _manaUsed,
congestionCost: _congestionCost,
protocolFee: _protocolFee,
proverCost: _proverCost
});
}
Expand Down Expand Up @@ -312,7 +374,7 @@ library FeeLib {
FeeLib.clampedAdd(parentFeeHeader.getExcessMana() + parentFeeHeader.getManaUsed(), -int256(manaTarget));
uint256 congestionMultiplier_ = congestionMultiplier(excessMana);

EthValue congestionCost =
EthValue protocolFee =
EthValue.wrap(
Math.mulDiv(EthValue.unwrap(total), congestionMultiplier_, MINIMUM_CONGESTION_MULTIPLIER, Math.Rounding.Floor)
) - total;
Expand All @@ -324,7 +386,7 @@ library FeeLib {
return ManaMinFeeComponents({
sequencerCost: FeeAssetValue.unwrap(sequencerCostPerMana.toFeeAsset(ethPerFeeAsset)),
proverCost: FeeAssetValue.unwrap(proverCostPerMana.toFeeAsset(ethPerFeeAsset)),
congestionCost: FeeAssetValue.unwrap(congestionCost.toFeeAsset(ethPerFeeAsset)),
protocolFee: FeeAssetValue.unwrap(protocolFee.toFeeAsset(ethPerFeeAsset)),
congestionMultiplier: congestionMultiplier_
});
}
Expand All @@ -342,6 +404,10 @@ library FeeLib {
return getStorage().config.getProvingCostPerMana();
}

function getProtocolFeeMarginBps() internal view returns (uint16) {
return uint16(getStorage().config.getProtocolFeeMarginBps());
}

function getEthPerFeeAssetAtCheckpoint(uint256 _checkpointNumber) internal view returns (EthPerFeeAssetE12) {
return EthPerFeeAssetE12.wrap(STFLib.getFeeHeader(_checkpointNumber).getEthPerFeeAsset());
}
Expand All @@ -357,7 +423,11 @@ library FeeLib {
// Cap the exponent to prevent overflow in the Taylor series.
// At e^100, the multiplier is ~2.69e43 * MINIMUM_CONGESTION_MULTIPLIER, more than enough
uint256 cappedNumerator = Math.min(_numerator, denominator * 100);
return fakeExponential(MINIMUM_CONGESTION_MULTIPLIER, cappedNumerator, denominator);
// The protocol fee margin scales only this factor: (10_000 + bps) * 1e5 == (1 + mu) * 1e9,
// exactly 1e9 (== MINIMUM_CONGESTION_MULTIPLIER) at mu = 0. The mulDiv divisor in
// getManaMinFeeComponentsAt MUST stay MINIMUM_CONGESTION_MULTIPLIER — scaling both sites
// cancels the margin.
return fakeExponential((10_000 + feeStore.config.getProtocolFeeMarginBps()) * 1e5, cappedNumerator, denominator);
}

function computeManaLimit(uint256 _manaTarget) internal pure returns (uint256) {
Expand Down Expand Up @@ -394,7 +464,24 @@ library FeeLib {
// Cap at uint128 max to ensure the fee can always be represented in the proposal header's
// feePerL2Gas field (uint128). Without this cap, extreme congestion or parameter combinations
// could produce fees that no valid header can represent, causing a liveness failure.
return Math.min(_components.sequencerCost + _components.proverCost + _components.congestionCost, type(uint128).max);
return Math.min(_components.sequencerCost + _components.proverCost + _components.protocolFee, type(uint128).max);
}

/**
* @notice The per-mana protocol fee written to the fee header: the pinned fee minus the two
* converted operator costs, as one subtraction.
* @dev The single subtraction guarantees `fee - protocolFee == cost * manaUsed` holds exactly
* in the reward waterfall; converting the margin and congestion tranches separately could
* drift by a wei because the Ceil conversion is not additive. The subtraction can go
* negative only when the uint128 cap in {summedMinFee} binds, in which case the protocol
* fee is clamped to 0 and operators stay whole.
* @param _components The mana min fee components (in fee asset)
* @return The per-mana protocol fee
*/
function protocolFeePerMana(ManaMinFeeComponents memory _components) internal pure returns (uint256) {
uint256 manaMinFee = summedMinFee(_components);
uint256 operatorCost = _components.sequencerCost + _components.proverCost;
return manaMinFee > operatorCost ? manaMinFee - operatorCost : 0;
}

function getStorage() internal pure returns (FeeStore storage storageStruct) {
Expand Down
Loading
Loading