Organization
- @Aztec-Labs
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
High Risk
1 findings
1 fixed
0 acknowledged
Medium Risk
6 findings
5 fixed
1 acknowledged
Low Risk
5 findings
2 fixed
3 acknowledged
Informational
13 findings
6 fixed
7 acknowledged
Gas Optimizations
3 findings
2 fixed
1 acknowledged
High Risk1 finding
Escape hatch updates can retroactively change historical epoch classification
Description
The function
verifyEpochProoffrom libraryEpochProofLibconditionally skips committee attestation verification when the escape hatch reports the hatch is open for the epoch. This decision is made by reading the current escape hatch address and callingescapeHatch.isHatchOpen(epoch).The rollup does not persist which escape hatch contract was active when a checkpoint was proposed. As a result, governance updates to the escape hatch address can retroactively change how past epochs are interpreted. This impacts multiple security decisions that query the current escape hatch pointer for historical epochs:
- Proof submission can start requiring attestations for a past epoch or stop requiring them, depending on the new escape hatch address and its
isHatchOpenresult. - Invalidation rules can change for already proposed checkpoints, potentially blocking invalidation that would otherwise be allowed.
- Slashing eligibility can change at tally time, shifting which epochs and actors are considered slashable.
This creates a situation where protocol behavior for a historical epoch depends on a mutable configuration rather than the state that was in effect at proposal time.
Recommendation
Consider to make escape hatch classification epoch stable by snapshotting the escape hatch address or hatch status per epoch. One approach is to activate escape hatch updates at epoch boundaries so that an updated escape hatch becomes effective starting from the next epoch. Another approach is to expose a
getEscapeHatchAt(epoch)style access pattern backed by a snapshotted history, so proof verification, invalidation, and slashing can consistently use the escape hatch that was active for that epoch.- Proof submission can start requiring attestations for a past epoch or stop requiring them, depending on the new escape hatch address and its
Medium Risk6 findings
BOND_TOKENs from taxes and punishments are permanently locked in EscapeHatch
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
Arno
Description
In
EscapeHatch.sol,WITHDRAWAL_TAXis deducted from a candidate's bond refund when they leave the set, andFAILED_HATCH_PUNISHMENTis deducted if a proposer fails to fulfill their duties. While these amounts are subtracted from the user's payout, the correspondingBOND_TOKENs remain held by theEscapeHatchcontract. There is no mechanism to withdraw, burn, or sweep these accumulated funds, causing them to be permanently locked in the contract.Recommendation
Add a restricted
withdraworsweepfunction to allow a governance entity to retrieve accumulated tokens.Updating EscapeHatch can invalidate an already selected proposer
Description
The function
updateEscapeHatchfrom contractRollupCoreupdates the configured escape hatch address viaupdateEscapeHatchfunction and emitsEscapeHatchUpdatedwithout enforcing any timing constraints relative to an in progress hatching cycle atRollupCorecontract.If the escape hatch address is changed after a proposer has already been selected in the previous escape hatch instance, the rollup will start interacting exclusively with the new escape hatch contract. The previously selected proposer remains recorded in the old escape hatch contract, but the rollup no longer advances that contract’s state. As a result, the proposer can be treated as having failed to propose, even though they were correctly selected and ready to act under the prior escape hatch configuration.
Recommendation
Consider to restrict
updateEscapeHatchso it can only be executed when no hatching cycle is active. Alternatively, consider to ensure that any proposer already selected under the previous escape hatch remains observable and cannot be penalized until the cycle is cleanly finalized or transitioned.Inactive escape hatch contracts can still select and punish candidates
Description
The function
selectCandidatesfrom contractEscapeHatchis permissionless and remains callable even after the rollup has switched to a different escape hatch contract.When governance updates the escape hatch address on the rollup, the previously configured escape hatch contract becomes inactive from the rollup’s perspective, but it is not disabled internally. Any account can still call
selectCandidateson the old contract, transitioning candidates into the proposing state. Since the rollup no longer interacts with that contract, proposals originating from it will not be accepted, and the selected candidates can be punished for failing to propose despite the contract no longer being active.In addition, there is no mechanism to automatically transition candidates to an exitable state or otherwise protect their bonded funds when the escape hatch is replaced. Candidates who were selected but not yet able to act at the time of the update may remain stuck unless they actively intervene, which relies on timely user behavior rather than protocol guarantees.
Recommendation
Consider to explicitly gate escape hatch operations on whether the contract is currently active for the rollup. For example, consider to prevent
selectCandidatesfrom progressing state when the escape hatch is no longer the one configured on the rollup, or to allow candidates to exit directly without risk of punishment once the contract becomes inactive. This would reduce reliance on candidate activity and prevent unintended punishment in deactivated escape hatch instances.Slashing round execution can revert when a targeted epoch committee is empty
Description
The function
executeRoundfrom contractTallySlashingProposerbuilds slashing actions by indexing into_committees[epochIndex][validatorIndex]and recording the action.Slashing votes target past epochs and are encoded as a fixed-size byte array covering
COMMITTEE_SIZEvalidator slots per epoch acrossROUND_SIZE_IN_EPOCHS. The vote encoding does not depend on whether a committee exists for a given targeted epoch, and a quorum can be reached for a slot even if the corresponding epoch has no valid committee.When calldata is constructed for
executeRound, an epoch with no committee can only be represented as an empty array for that epoch. If_committees[epochIndex]is empty, indexing into_committees[epochIndex][...]reverts, causingexecuteRoundto fail. This means a single round can become blocked if quorum is reached for any slot in an epoch that does not have a valid committee array.Recommendation
Consider to defensively skip epochs that do not provide a valid committee array before indexing into
_committees. This can be done by checking that the committee for the computed epoch index exists and has lengthCOMMITTEE_SIZE, and skipping processing for that epoch when it does not. Ex:uint256 epochIndex = i / COMMITTEE_SIZE; if (escapeHatchEpochs[epochIndex]) continue; if (_committees[epochIndex].length != COMMITTEE_SIZE) continue;Fee header compression can revert if congestion or prover costs exceed field size
Description
The function
computeFeeHeaderfrom libraryFeeLibreturns aFeeHeaderthat includes_congestionCostand_proverCost. These values are later compressed into the on chain fee header representation, where the corresponding fields have fixed bit sizes._congestionCostand_proverCostare computed in fee asset units using a conversion of the formfeeAssetCostequalsethCosttimes1e12divided byethPerFeeAsset. When the fee asset price is low, when L1 fees are high, or when parameters such as the mana target are small, these computed values can grow large enough to exceed the representable range. In that case, fee header compression reverts, which causes checkpoint proposal to revert.This creates a configuration and market-dependent liveness risk, where checkpoint proposals can fail due to costs exceeding encoding limits rather than being handled as a bounded input.
Recommendation
Consider to enforce explicit upper bounds for
_congestionCostand_proverCostbefore fee header compression. This can be implemented by clamping to the maximum representable value, or by reverting with a clear error earlier in the flow when costs exceed the allowed range. This would make the encoding constraint explicit and avoid unexpected reverts during compression.Unbounded excess mana can overflow congestion multiplier computation and block proposals
Description
The function
congestionMultiplierfrom libraryFeeLibcomputes the congestion multiplier by callingfakeExponentialwithexcessManaas the numerator.fakeExponentialuses checked arithmetic while iteratively updating the Taylor series terms. AsexcessManagrows, intermediate multiplications in the series can overflow and revert. Since fee computation is part of the checkpoint proposal flow, a revert in the congestion multiplier computation can block checkpoint proposals.excessManais derived from prior fee headers and there is no explicit cap applied before using it as thefakeExponentialnumerator, so sustained congestion can push it into ranges where overflow becomes possible.Recommendation
Consider to bound
excessManato a safe maximum before using it infakeExponential, or consider to implement a capped variant of the exponential approximation that saturates to a maximum multiplier instead of reverting. This would avoid proposal liveness depending onfakeExponentialnot overflowing under prolonged congestion.
Low Risk5 findings
NatSpec claims _slashAmounts “must be > 0” but constructor doesn’t enforce it
Severity
- Severity: Low
Submitted by
Arno
Description
The constructor docs say
_slashAmountsentries “must be > 0”, but the constructor only checks ordering (_slashAmounts[0] <= _slashAmounts[1] <= _slashAmounts[2]) and does notrequire(_slashAmounts[i] > 0). As written, zero slash amounts are allowed.If any amount is0, slashing can reach quorum but slash nothing. On the other hand, if any amount exceedsuint96,executeRoundwill revert when building the payload, permanently disabling slashing.SLASH_AMOUNT_SMALL = _slashAmounts[0];SLASH_AMOUNT_MEDIUM = _slashAmounts[1];SLASH_AMOUNT_LARGE = _slashAmounts[2];// ...require(_slashAmounts[0] <= _slashAmounts[1], Errors.TallySlashingProposer__InvalidSlashAmounts(_slashAmounts));require(_slashAmounts[1] <= _slashAmounts[2], Errors.TallySlashingProposer__InvalidSlashAmounts(_slashAmounts));Recommendation
Add explicit validation (e.g.,
require(_slashAmounts[i] > 0)for all 3)Zero bond size allows free participation in escape hatch
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
constructorfrom contractEscapeHatchassignsBOND_SIZE = _bondSizewithout explicitly validating that_bondSize > 0.If the contract is deployed with
_bondSize == 0, any address can join the escape hatch set at zero cost while remaining eligible for selection. This removes the intended economic gating described in the documentation and weakens the assumptions around the escape hatch mechanism.Recommendation
Consider to add an explicit constructor check that
_bondSizeis greater than zero, so a misconfigured deployment cannot silently disable the bond requirement.Escape hatch proposals can skip epoch setup and leave RANDAO checkpoints stale
State
- Acknowledged
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
proposefrom libraryProposeLibsets up the epoch by callingValidatorSelectionLib.setupEpoch(v.currentEpoch)after deriving the current epoch fromblock.timestamp.In the updated logic, escape hatch proposals do not call
setupEpoch. This means that for escape hatch epochs the protocol may not refresh epoch specific randomness checkpoints and related epoch initialization state. As a result, subsequent epochs may derive selection randomness from an older checkpoint than intended, making the randomness effectively known earlier and reducing the intended unpredictability for committee and proposer selection.This is particularly relevant because epoch setup is currently triggered by the first checkpoint of the epoch. If the first checkpoint is proposed through the escape hatch path and epoch initialization is skipped, the refresh may never occur for that epoch.
Recommendation
Consider to ensure the randomness checkpoint is refreshed even when the first checkpoint of an epoch is proposed through the escape hatch path, while still avoiding committee sampling. One approach is to checkpoint the RANDAO for the current epoch during escape hatch proposals and call full epoch setup during non-escape hatch proposals.
Aztec: Acknowledged. It is a minor potential issue, but it is possible to checkpoint the randao whenever desired.
Cantina Managed: Acknowledged by Aztec team
EscapeHatch address can be updated to an incompatible contract
State
- Acknowledged
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
updateEscapeHatchfrom contractRollupCoreupdates the escape hatch address by callingValidatorOperationsExtLib.updateEscapeHatch(_escapeHatch)and emitsEscapeHatchUpdated.The update does not validate that the new escape hatch contract is correctly configured to work with this rollup instance. If governance sets an address that is not wired to this rollup, escape hatch proposals can fail or cause rollup interactions with the escape hatch to revert, including calls that assume a compatible interface and correct rollup linkage.
This is primarily a governance configuration risk, but the failure mode can impact liveness for the escape hatch path and create operational risk during upgrades.
Recommendation
Consider to validate the new escape hatch during
updateEscapeHatchbefore applying it. This can be done by requiring that the new contract reports it is configured for this rollup, or by performing a minimal compatibility check that exercises the expected interface and confirms the rollup linkage, then reverting if the check fails.Aztec: Acknowledged. It requires a bad governance proposal, and can be undone.
Cantina Managed: Acknowledged by Aztec team
Missing explicit bounds/sanity checks for updateProvingCostPerMana
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Arno
Description
FeeLib.updateProvingCostPerManaupdatesFeeConfig.provingCostPerManawith no explicit domain validation (unlikemanaTarget, which is validated viacomputeManaLimit). The only effective constraint is implicit: when recompressing the config,provingCostPerManais downcast touint64(toUint64()), so values abovetype(uint64).maxrevert, but no “sane range” bound is enforced.Recommendation
Add an explicit upper bound (and optionally a lower bound) for
_provingCostPerManaconsistent with intended economics, similar in spirit tocomputeManaLimitformanaTarget.
Informational13 findings
Incorrect NatSpec EIP-712 Vote struct field order in VOTE_TYPEHASH comment
Severity
- Severity: Informational
Submitted by
Arno
Description
In
TallySlashingProposer.sol, the NatSpec forVOTE_TYPEHASHstates the EIP-712 struct asVote(uint256 slot,bytes votes), but the actual type hash is computed fromkeccak256("Vote(bytes votes,uint256 slot)"), i.e. the field order isvotesthenslot.This is a documentation-only mismatch.Recommendation
Update the NatSpec to reflect the correct EIP-712 struct definition:
Vote(bytes votes,uint256 slot).Dead stale-round check in getRound
Severity
- Severity: Informational
Submitted by
Arno
Description
In
getRound, the guardif (roundData.roundNumber != _round)is unreachable._getRoundDataalways returns aRoundDatawithroundNumber: _round, even when the circular buffer contains data for a different (overwritten) round (it returns a zeroed struct but still setsroundNumber = _round).Recommendation
Remove the
roundData.roundNumber != _roundbranch and rely on_getRoundData’s default(executed=false, voteCount=0)behavior.getVotes can return stale votes for overwritten rounds
Severity
- Severity: Informational
Submitted by
Arno
Description
roundVotesis stored in a circular buffer (ROUNDABOUT_SIZE), indexed byround % ROUNDABOUT_SIZE. When the buffer wraps, older rounds’ vote slots are overwritten/reused. Staleness detection exists in_getRoundDataviaroundDatas[...](compressedroundNumber), butgetVotesdoes not call it and instead reads vote slots directly, so callers can receive vote data that actually belongs to a different (newer) round.Recommendation
In
getVotes, validate round freshness before reading votes (e.g., call_getRoundData(_round, getCurrentRound())and/or checkroundDatas[idx].roundNumber.decompress() == _round). If stale, revert or return empty bytesIncorrect bit-width comment for CompressedFeeConfig.manaTarget
Severity
- Severity: Informational
Submitted by
Arno
Description
In
FeeConfig.sol, the comment saysmanaTargetis 64 bits, but the compression usestoUint32()and extraction masks withMASK_32_BITS, meaningmanaTargetis 32 bits inCompressedFeeConfig.Recommendation
Update the comment to reflect the actual layout, e.g. “32 bit manaTarget, 128 bit congestionUpdateFraction, 64 bit provingCostPerMana”.
Dead storage field: unused feeHeaders mapping in FeeLib.FeeStore
Severity
- Severity: Informational
Submitted by
Arno
Description
FeeLib.FeeStoredeclaresfeeHeadersbut it is never read from or written to anywhere in the codebase. Fee header lookups instead useSTFLib.getFeeHeader(...), making this mapping dead code/storage.Recommendation
Remove
feeHeadersfromFeeStore.Zero or minimal RANDAO lag can allow proposer influence over committee and proposer selection
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
initializefrom libraryValidatorSelectionLibstores_lagInEpochsForRandaowithout enforcing a minimum value beyond the requirement that_lagInEpochsForValidatorSetis greater than or equal to_lagInEpochsForRandao.If
_lagInEpochsForRandaois configured as zero, the randomness used for selection can depend onblock.prevrandaofrom the current epoch context. This gives the current epoch proposer more opportunity to influence the randomness input and bias committee selection and escape hatch proposer selection.If
_lagInEpochsForRandaois configured equal to_lagInEpochsForValidatorSet, the design still permits minimal separation between the selected validator set and the randomness used to select from it, which can reduce the intended unpredictability and increase the value of proposer influence.While this is primarily a deployment configuration risk, the impact is security-relevant because it affects the integrity of validator committee and proposer selection.
Recommendation
Consider to enforce a minimum value for
_lagInEpochsForRandao, such as requiring it to be at least one epoch, and consider to require that_lagInEpochsForValidatorSetis strictly greater than_lagInEpochsForRandaoif the protocol relies on separation between the validator set snapshot and the randomness snapshot. This would prevent misconfiguration that makes selection more biasable.Aztec: Acknowledged. Used potentially low values to speed up testing, but for a real deployment will be using larger values.
Cantina Managed: Acknowledged by Aztec team
Reward accounting can revert if burn exceeds collected fee
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function that accounts rewards from contract
RewardLibcomputes the prover and sequencer fees by subtractingburnfromfee.The logic assumes that
feeis always greater than or equal toburn. If this assumption is violated, for example due to malformed proof inputs or an upstream circuit bug, the expressionfee - burnunderflows and causes proof submission to revert. This introduces a hard failure mode in reward accounting rather than a controlled rejection with a clear error.While this situation may not be expected under correct circuit behavior, the assumption is implicit and not enforced at the contract level.
Recommendation
Consider to add an explicit validation that
feeis greater than or equal toburnbefore performing the subtraction, and revert with a clear error if the invariant is violated. This would make the assumption explicit and improve robustness against unexpected inputs.Aztec: Acknowledged. Should be impossible to hit, unless there issues in the circuits.
Cantina Managed: Acknowledged by Aztec team
Outbox roots can be overwritten without resetting nullifier state
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
insertfrom contractOutboxwritesroots[_checkpointNumber].root = _rootatl1-contracts/src/core/messagebridge/Outbox.sol:47and emitsRootAdded. The function only checks that the caller is the rollup and that_checkpointNumberis greater than the rollup proven checkpoint number.There is no guard preventing reinsertion for the same checkpoint number. If the rollup ever calls
insertagain for a checkpoint number that already has a stored root, the root can be overwritten while any message consumption state that depends on the previous root remains unchanged. In particular, if messages were already consumed under the old root, the associated nullifier bitmap is not reset or migrated, which can block messages under the new root or desynchronize consumption state from the active root.This relies on a protocol assumption that outbox roots are written once and never updated, and that leaf identifiers remain stable. The contract does not enforce this assumption.
Recommendation
Consider to enforce one-time insertion per checkpoint number by reverting if a root is already set for
_checkpointNumber. If root updates are intended to be supported, consider to define and implement explicit state transition logic that keeps nullifier tracking consistent across root changes.Aztec: Acknowledged. Allowing multiple writes without rewriting the nullifiers is intentional. The nullifiers can only be written to if the root was proven, and at that point the rollup should not overwrite it again. But if a prune happens (lack of proof) then we might need to rewrite the root.
This component also altered in hatch 2 sections (indiretly at least) because of the outhash changes.
Cantina Managed: Acknowledged by Aztec team
Large lag configuration can underflow epoch sample time computation and block early epoch setup
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The functions
stableEpochToRandaoSampleTimeandstableEpochToValidatorSetSampleTimefrom libraryValidatorSelectionLibcompute a sample timestamp by subtractinglagInEpochsmultiplied byepochDurationfrom the epoch start timestamp, usinguint32arithmetic.If
lagInEpochsForRandaoorlagInEpochsForValidatorSetis configured too large relative to the genesis time and the early epoch start timestamps, the subtraction underflows and reverts. This can preventsetupEpochand other functions that depend on these sample time computations, such asgetSampleSeed, from working during early epochs after deployment.This is primarily a configuration risk, but the failure mode is a hard revert that can block epoch setup.
Recommendation
Consider to add initialization time validation that the genesis time and configured lags are compatible with the epoch duration. One approach is to ensure that the genesis time offset is at least
lagInEpochsmultiplied byepochDuration, or otherwise enforce bounds on the configured lags to prevent underflow in early epochs.Aztec: Acknowledged. The lag would need to be VERY large for this to happen as the underflow must be with current time, so won't be fixed as that kinda delay would anyway mean that the rollup also has delays of ~50 years from entry to usage.
Cantina Managed: Acknowledged by Aztec team
Use of magic numbers reduces readability and maintainability
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The contract
EscapeHatchcomputes the next target hatch using the literal value1in arithmetic withLAG_IN_HATCHESatl1-contracts/src/core/EscapeHatch.sol:184, and a similar literal is used again shortly after. The meaning of this increment is implicit and not documented in code.Similarly, the contract
TallySlashingProposerallocates a fixed-sizebytesarray using the literal expression4 * 32atl1-contracts/src/core/slashing/TallySlashingProposer.sol:957. The significance of this length is not encoded in a named constant, making it less clear what structure or expectation the value represents.In both cases, the use of raw numeric literals makes the code harder to reason about and more error-prone during future modifications.
Recommendation
Consider to replace these literal values with named constants that reflect their semantic meaning. This would improve readability, reduce the risk of accidental misuse, and make future changes easier to apply safely.
Aztec: Acknowledged. The 1 generally used for the next, and the votes size is easily follow for the 4 slots.
Cantina Managed: Acknowledged by Aztec team
Misconfigured committee size can block normal proposal flow
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function that validates committee sampling from library
ValidatorSelectionLibrequiresvalidatorSetSizeto be greater than or equal totargetCommitteeSize, and reverts otherwise.If governance configures
targetCommitteeSizeonRollupCoreto a value greater than the size of the active validator set, normal proposal paths that depend on committee formation will revert. This can block committee queries and standard checkpoint proposals, effectively forcing progress to rely on the escape hatch path.This behavior relies on correct governance configuration and does not provide a graceful degradation or early validation when the configuration becomes incompatible with the validator set size.
Recommendation
Consider to validate committee size configuration changes at the time they are applied, ensuring that
targetCommitteeSizedoes not exceed the current validator set size. Alternatively, consider to define explicit behavior for this case, such as clamping the effective committee size or preventing configuration updates that would block the normal proposal flow.Aztec: Acknowledged. The configuration would need to be specified at deployment of the rollup and then added as the new rollup to take effect. But if that is the case, yes it could stall forever. However, as those kinda of stalls are also possible in other cases where rollup is updated to broken code etc, it don't seems particularly likely. It relies on no-one validating and if done maliciously worse things could happen.
Cantina Managed: Acknowledged by Aztec team
initiateExit() can self-revert when it selects the caller as proposer
Severity
- Severity: Informational
Submitted by
Arno
Description
initiateExit()callsselectCandidates()first (EscapeHatch.sol:169). If that internal call selectsmsg.senderas the designated proposer, it updates the caller’s state toPROPOSINGand removes them from$activeCandidates. Control then returns toinitiateExit(), which immediately requires the caller is still in$activeCandidatesand hasStatus.ACTIVE, so the transaction reverts (typically with a misleadingNotInCandidateSet/InvalidStatus).This makes the behavior/commentary ambiguous: the inline comment suggests
selectCandidates()just “simplifies” subsequent checks, but it can also mutate state such that those checks intentionally fail. Separately, theselectCandidates()comment about handlingStatus.EXITINGis subtle: it only applies when a candidate initiated exit in an earlier transaction before the selection window for that hatch, and is later selected from the snapshot (not within the sameinitiateExit()call).Recommendation
- Update NatSpec/comments to explicitly state that a candidate cannot initiate exit if they become the designated proposer; they must follow the
PROPOSING -> validateProofSubmission -> EXITING -> leaveCandidateSetflow. - Consider adding an explicit post-
selectCandidates()check that reverts with a dedicated error (e.g., “selected proposer cannot exit”) instead of relying on the later membership/statusrequires, to avoid confusing revert reasons.
Slashing payloads are not epoch-attributable
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Arno
Description
TallySlashingProposertallies votes over a flattened set of committee positions across all epochs in the round (COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) and converts any position that reaches quorum into aSlashActionby mapping the position indexito an address via_committees[i / COMMITTEE_SIZE][i % COMMITTEE_SIZE].Because actions are created per position and there is no de-duplication by validator address, the same validator address can appear multiple times in the resulting
actions[]if it appears in multiple epoch committees within the same slashing round. Each action becomes a separateIStakingCore.slash(validator, amount)call viaSlashPayloadCloneable, which encodes only(validator, amount)and carries no epoch/offense identifier.As a result, the onchain execution path cannot distinguish whether a validator is being slashed for epoch 0 vs epoch 1 (or any specific offense); it only reflects that the validator was slashed one or more times. Any intended policy like “slash multiple times only if they offended multiple times (in different epochs)” is therefore not enforceable onchain and relies on proposers voting correctly per position rather than “blanket voting” across all appearances.
Recommendation
If epoch attribution matters (e.g., to ensure “only slash for the specific epoch(s) of misbehavior”)
Gas Optimizations3 findings
CandidateJoined event redundantly emits immutable bond size
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function
joinfrom contractEscapeHatchemits theCandidateJoinedevent withBOND_SIZEas an argument.Since
BOND_SIZEis an immutable value set at construction time, emitting it on everyCandidateJoinedevent does not convey new information. Indexers and off-chain consumers can already derive the bond size directly from the contract configuration, making this event field redundant.Recommendation
Consider to remove
BOND_SIZEfrom theCandidateJoinedevent to reduce redundancy and simplify event consumption.Candidate bond amount is redundantly stored despite being immutable
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function
joinfrom contractEscapeHatchassignsdata.amount = BOND_SIZEwhen a candidate joins the set.Since
BOND_SIZEis an immutable value shared by all candidates, storing the same bond amount per candidate is redundant and increases storage usage without adding expressiveness. The value never diverges per user unless modified by later punishment logic, which is not currently reflected in the stored structure.This design also limits flexibility in how penalties are represented, as the full bond amount is always stored even though only partial deductions may apply during the candidate lifecycle.
Recommendation
Consider to avoid storing the full bond amount per candidate when it is invariant. An alternative approach is to store only the penalty applied to a candidate when they fail to propose valid checkpoints, and derive the withdrawable amount at exit by subtracting the accumulated penalty and the withdrawal tax from
BOND_SIZE. This would preserve correctness while reducing redundant storage and making penalty application more explicit.Aztec: Acknowledged. Logic simple to follow this way.
Cantina Managed: Acknowledged by Aztec team
Proposer index is computed but unused during validator selection
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function that derives proposer information from library
ValidatorSelectionLibcomputes aproposerIndexusingcomputeProposerIndexbut the computed value is not subsequently used.This introduces dead logic in the selection flow and makes it unclear whether proposer selection is intended to rely on this value or whether the computation is a leftover from an earlier design. Leaving unused selection logic in place increases maintenance burden and can cause confusion when reasoning about proposer selection correctness.
Recommendation
Consider to either remove the unused
proposerIndexcomputation or explicitly use it as part of proposer selection if it is intended to affect protocol behavior. Clarifying this intent in code will improve readability and reduce the risk of incorrect assumptions in future changes.