Skip to content

feat: introduce a protocol fee margin (AZIP-23) - #25260

Open
aminsammara wants to merge 3 commits into
nextfrom
as/azip-23-protocol-fee-margin
Open

feat: introduce a protocol fee margin (AZIP-23)#25260
aminsammara wants to merge 3 commits into
nextfrom
as/azip-23-protocol-fee-margin

Conversation

@aminsammara

Copy link
Copy Markdown
Contributor

Adds a governance-set margin mu on the mana base fee. The fee becomes cost * (1 + mu) * congestionMultiplier: operators still receive exactly cost from the fee waterfall plus the unchanged block inflation, and the markup above cost goes to a governance-set protocolFeeRecipient, which defaults to the current burn address.

  • mu is a uint16 bps field packed into bits 224-239 of CompressedFeeConfig and deploys at 0, so the rollup is bit-identical to today.
  • setProtocolFeeMargin is governance-only and rate-limited to x3/2 on the fee multiplier per 30-day window, matching setProvingCostPerMana. Decreases are immediate and unrestricted; setting the current value is a no-op.
  • The (1 + mu) scaling is applied only to the fakeExponential factor in congestionMultiplier. The mulDiv divisor stays MINIMUM_CONGESTION_MULTIPLIER -- scaling both sites cancels the margin.
  • The fee header's uint64 congestionCost field becomes protocolFee and carries summedMinFee - sequencerCost - proverCost as a single subtraction clamped at 0, so fee - protocolFee = cost * manaUsed holds to the wei.
  • RewardLib.BURN_ADDRESS becomes the deploy-time default for a stored protocolFeeRecipient; getBurnAddress() is replaced by getProtocolFeeRecipient().
  • The TypeScript fee mirror is updated to match bit-exactly, with mu threaded through the L1 config read into the fee predictor.

Comment thread l1-contracts/src/core/interfaces/IRollup.sol Outdated
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?

}

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

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

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.

RewardLib.updateConfig(_config);
}

function updateProtocolFeeMargin(uint16 _bps) external returns (bool changed, uint16 oldBps) {

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.

needs to be gated so that callable by governance only. Here or in the FeeLib.sol

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.

I think this is already the case. The rollup entrypoint function is already gated.

return FeeLib.updateProtocolFeeMargin(_bps);
}

function updateProtocolFeeRecipient(address _recipient) external returns (address oldRecipient) {

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.

needs to be gated so that callable by governance only. Here or in RewardLib.sol

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.

same, gated in the rollup

// such as sacrificial hearts, during rituals performed within temples.
// Deploy-time default for `protocolFeeRecipient`: the protocol fee is burned until governance
// redirects it via {updateProtocolFeeRecipient}.
address public constant BURN_ADDRESS = address(bytes20("CUAUHXICALLI"));

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 we rename the BURN_ADDRESS constant?
Or we can remove this constant altogether and just set protocolFeeRecipient to address(bytes20("CUAUHXICALLI")) in initializeConfig

/// @notice Owner-gated post-deployment writer for the protocol fee recipient.
/// @param _recipient The new recipient of the protocol fee tranche
/// @return oldRecipient The recipient in effect before this call
function updateProtocolFeeRecipient(address _recipient) internal returns (address oldRecipient) {

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.

the event is not emitted from here, where is it emitted from? should it be emitted from here instead?

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.

okay it's emitted in the next file

Comment on lines +271 to +275
require(
(10_000 + uint256(_bps)) * PROTOCOL_FEE_MARGIN_STEP_DEN
<= (10_000 + uint256(oldBps)) * PROTOCOL_FEE_MARGIN_STEP_NUM,
Errors.FeeLib__ProtocolFeeMarginStepExceeded(oldBps, _bps)
);

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)

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.

}

v.sequencerFee = fee - burn - v.proverFee;
v.sequencerFee = fee - protocolFee - v.proverFee;

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.

we're going to get merge conflicts with #25312

aminsammara and others added 2 commits August 28, 2026 09:42
Adds a governance-set margin `mu` on the mana base fee. The fee becomes
`cost * (1 + mu) * congestionMultiplier`: operators still receive exactly
`cost` from the fee waterfall plus the unchanged block reward, and the markup
above cost goes to a governance-set `protocolFeeRecipient`, which defaults to
the current burn address.

- `mu` is a uint16 bps field packed into bits 224-239 of `CompressedFeeConfig`
  and deploys at 0, so the rollup is bit-identical to today.
- `setProtocolFeeMargin` is governance-only and rate-limited to x3/2 on the fee
  multiplier per 30-day window, matching `setProvingCostPerMana`. Decreases are
  immediate and unrestricted; setting the current value is a no-op.
- The `(1 + mu)` scaling is applied only to the `fakeExponential` factor in
  `congestionMultiplier`. The `mulDiv` divisor stays
  `MINIMUM_CONGESTION_MULTIPLIER` -- scaling both sites cancels the margin.
- The fee header's uint64 `congestionCost` field becomes `protocolFee` and
  carries `summedMinFee - sequencerCost - proverCost` as a single subtraction
  clamped at 0, so `fee - protocolFee = cost * manaUsed` holds to the wei.
- `RewardLib.BURN_ADDRESS` becomes the deploy-time default for a stored
  `protocolFeeRecipient`; `getBurnAddress()` is replaced by
  `getProtocolFeeRecipient()`.
- The TypeScript fee mirror is updated to match bit-exactly, with `mu` threaded
  through the L1 config read into the fee predictor.
@alexghr
alexghr force-pushed the as/azip-23-protocol-fee-margin branch from 901f44f to 209a142 Compare August 28, 2026 09:45
@alexghr
alexghr marked this pull request as ready for review August 28, 2026 13:58
@alexghr
alexghr requested a review from just-mitch as a code owner August 28, 2026 13:58
@alexghr

alexghr commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

I have addressed the review comments on this PR and rebased on top of latest next and moved the typescript changes to aztec-labs-eng/aztec-node#107

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants