Organization
- @Aztec-Labs
Engagement Type
Cantina Solo
Period
-
Repositories
Researchers
Findings
Low Risk
1 findings
0 fixed
1 acknowledged
Informational
2 findings
0 fixed
2 acknowledged
Gas Optimizations
1 findings
1 fixed
0 acknowledged
Low Risk1 finding
Governance could bypass insider gate by lowering ATP registry timestamp when colluding with registry owner
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
relay()recomputesinsiderCanActTimestamp()every call by readingATP_REGISTRY.getExecuteAllowedAt()and adding seven days. The ATP registry owner, however, can decreaseexecuteAllowedAtarbitrarily viaRegistry.setExecuteAllowedAt. If that owner cooperates with governance (or is compromised by it), they can:- Lower
executeAllowedAtfor a single transaction so thatinsiderCanActTimestamp()is satisfied immediately. - Execute
relay()to drain funds or push approvals before insiders were genuinely allowed to act.
This completely defeats the stated guarantee that insiders receive at least a week of participation time before governance can use the relayer. Any trust in the gate devolves into “trust the registry owner not to cheat,” which is the very scenario the relayer was meant to prevent.
Recent design notes indicate the insider ATP registry is operated by a separate entity rather than governance. That setup prevents unilateral governance bypasses, but it still leaves a single mutable timestamp whose guardian can (intentionally or accidentally) invalidate the delay guarantees. If governance colludes with, compromises, or exerts pressure on that operator, the same three-step attack above becomes possible. Lowering
executeAllowedAtalso shortens insiders’ own “time-to-act,” so the timestamp remains a powerful lever that should not be mutable once insiders have deposited funds.Recommendation
Decouple the relayer from future registry mutations. Either (a) pass the final unlock timestamp at deployment and store it immutably (adding the 7-day buffer internally), or (b) cache the first-observed
executeAllowedAtand refuse to ever decrease the cached value, optionally only allowing increases. Additionally, consider transferring registry ownership to an independent contract that cannot collude with governance before insiders unlock.Aztec Labs: Acknowledged. The ATP registry owner (of the one where insider funds are) is not the governance, but a separate entity. Collusion could still create the issue though, but less likely. Moreover, it is not possible to restore the
executeAllowedAtsince it can only be decreased, e.g., if it is lowered it will stay low. Anyway, they could still allow the execution of a already queued proposal to execute as if it was possible for ATP's following that registry to act (without actually haven been able to).However, the insiders (ATP's following registry) have significant trust assumptions towards the registry owner already. As he will need to add an implementation that they can use to participate in governance for the period after
executeAllowedAtuntil the assets are liquid. So if compromised, he could avoid them participating anyway by simply not adding a way to deposit.In short, it seems fine to me that we rely on the trust assumption for honest
executeAllowedAtvalues, since we already rely on it being honest for more powerful actions.
Informational2 findings
ProtocolTreasury inefective bootstrap on mature governance
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
markNext()only processes a single proposal per transaction and enforces that the proposal is already in a terminal state before incrementingmarkedProposalsCount. Because the counter is initialized to zero, deploying the relayer against an existing governance with N historical proposals requires N separate transactions, one for each proposal, to advance the pointer, since each invocation touches storage and the contract provides no batching primitive. For a live system that has already created thousands of proposals, this means thousands of keeper calls (e.g., 10000 proposals -> 10000 distinctmarkNexttransactions) must be executed before the relayer can even consider forwarding a call. Until the backlog is cleared,relayalways sees an unmarked, pre-insider proposal and reverts on the insider-check. Impact: onboardingProtocolTreasuryto a mature governance can entail a prohibitive amount of operational work and may be infeasible in practice if keepers cannot reliably submit that many transactions.Recommendation
Add an initialization override so a trusted role can set
markedProposalsCount, or extendmarkNextwith a batched variant (markRange(uint256 count)) that iterates in-contract and emits events for the processed proposals while staying within block gas limits.Aztec Labs: Acknowledged. It is already possible to do a batch by using a multicall or similar, so we don't think that we need a
markRange(though it was considered at first, but left out for simplicity). We don't think it is too much a concern in our setup because it is deployed along with the governance, so there are existing proposals and also how new proposals are created in our system. Adding some context, a governance proposal can be made in 2 ways:- Locking a large amount for a significant time period (~2% of supply and locked for 90 days).
- Going through the "governance proposer" which is based on validator actions and at most one proposal can be made every 20 hours. So even a "spam" of proposals would grow very slow here and should have a higher cost to create than consume. Agree for a mature governance though that if there are a lot of proposals, but even then I think it should be no big hurdle.
Relay strips governance caller identity
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
relayexecutes targets via a standardcalloriginating from the relayer contract. Any contract that used to rely onmsg.sender == GOVERNANCEwill now see theProtocolTreasuryaddress instead. Once ownership or privileged roles are transferred to the relayer, the governance account no longer has those rights; the relayer becomes the effective admin. If downstream contracts still gate logic on the governance address and cannot be reconfigured to trust the relayer, those privileges are effectively lost when relaying.Recommendation
Before routing calls through
ProtocolTreasury, ensure all target contracts explicitly trust theProtocolTreasury’s address (e.g., by transferring ownership to it or adding it to ACLs).Aztec Labs: Acknowledged.
Gas Optimizations1 finding
Zero-value relays pay unnecessary BALANCE gas
Description
relayalways executesrequire(address(this).balance >= value, Errors.InsufficientBalance(...)). Even whenvalue == 0, the contract performs a BALANCE opcode and allocates memory for the revert data, which costs ~400 gas per governance action, the common case if most relays don’t transfer native assets. Since a zero transfer can never underflow the relayer’s balance, the check can safely be skipped in that branch; alternatively, rely on the target call reverting naturally if balance is insufficient.Recommendation
Wrap the balance check in
if (value != 0)before executing it, so zero-value relays bypass the BALANCE opcode and associated memory work. This yields a small but recurring gas savings for governance executions that only call functions without native asset transfers.Aztec Labs: Fixed in PR #690.
Cantina: Fix verified.