dydx Q3 2025 - Proposer Selection Updates Audit Report Final
Security Audit Report
DYDX Q3 2025: PROPOSER SELECTION UPDATES
Last Revised Authors: 2025/09/19 Martin Hutle, Vukašin Dokmanović dYdX Q3 2025 Proposer Selection Updates
Contents Audit overview 2 The project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Scope of this report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Audit plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Audit Dashboard 4 Target Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Engagement Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Severity Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
System Overview 5 Architecture Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Protocol Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Threat Model 15 Integration Model & Threats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Protocol Model & Threats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Implementation Threats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Findings 23 Redelegations, slashing and jailing can lead to scenarios where there is no correct proposer . . . . . . . . 24 Recommendation to enforce uniqueness in proposer set validation . . . . . . . . . . . . . . . . . . . . . . . . . 25 Miscellaneous code improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Appendix A: Additional Tests for Proposer Set Validation 27
Appendix B: Vulnerability classification 29
Disclaimer 32
Informal Systems © 2025 < Table of Contents 1 dYdX Q3 2025 Proposer Selection Updates
Audit overview The project In September 2025, dYdX engaged Informal Systems to perform a security audit of a new feature that modifies the block production mechanism to provide finer-grained control over which validators are eligible to propose blocks. Instead of all active validators participating equally in the proposer rotation, the feature introduces a validator-level configuration flag (ProposeDisabled) that determines eligibility. A new governance-controlled message type has been introduced to manage this mechanism. When executed via a governance proposal, the message stores an explicit list of validator addresses permitted to participate in block production. Whenever the proposer set needs to be refreshed, each validator’s ProposeDisabled flag is recalculated based on this list and then propagated to CometBFT, ensuring proposer rotation reflects the governance-approved configuration.
Scope of this report The audit focused on evaluating the correctness and security properties of the changes to the features in dYdX’s forks of Cosmos SDK and CometBFT.
Audit plan The audit was conducted between September 10th to September 19th, 2025, by the following personnel:
● Martin Hutle ● Vukašin Dokmanović
Conclusions The audit team has thoroughly reviewed the changes to the proposer set management logic. We commend the development team for the high quality of their implementation. Overall, no critical security or liveness issues were identified during this audit. That said, we highlight two notable areas where additional safeguards could further strengthen the robustness of the system:
- Assumption on Proposer Availability ● Without further checks, redelegations, slashing, or jailing could reduce the active proposer set to a single validator. In such a scenario, if that validator fails during proposal rounds, consensus may stall indefinitely. While this risk is mitigated by the assumption that there is always at least one correct active proposer, this assumption is stricter than the standard CometBFT safety model. We recommend formalizing this assumption and implementing safeguards to detect violations (e.g., by enforcing a minimum proposer stake threshold or triggering fallback mechanisms such as automatically re-enabling all validators as proposers).
- Lack of Uniqueness Enforcement in Proposer Sets ● The current checkProposerSetInvariants() implementation does not enforce uniqueness, meaning du- plicate proposers can be included in the proposer set and still pass validation. While this does not affect
Informal Systems © 2025 < Table of Contents 2 dYdX Q3 2025 Proposer Selection Updates
current CometBFT behavior, it weakens governance semantics and could increase computational costs in certain loops. We recommend adding explicit checks to enforce uniqueness when setting proposer sets. In summary, the audit confirms that the system’s proposer selection and consensus integration are secure, de- terministic, and free from critical vulnerabilities. The identified findings are non-critical and primarily concern long-term safety guarantees and robustness against unlikely edge cases. Addressing them will further strengthen the system’s resilience against governance misconfigurations and unexpected validator dynamics.
Informal Systems © 2025 < Table of Contents 3 dYdX Q3 2025 Proposer Selection Updates
Audit Dashboard Target Summary ● Type: Protocol and implementation ● Platform: Go ● Artifacts: The following commits
– Commits on CometBFT fork: ➞ commit 1 ↗ ➞ commit 2 ↗ – Commits on Cosmos SDK fork: ➞ commit 1 ↗ ➞ commit 2 ↗ – Commit on v4-chain: ➞ commit 1 ↗
Engagement Summary ● Dates: September 10th, 2025 → September 19th, ● Method: Manual code review
Severity Summary
Finding Severity Number
Critical 0 High 1 Medium 0 Low 0 Informational 2 Total 3
Informal Systems © 2025 < Table of Contents 4 dYdX Q3 2025 Proposer Selection Updates
System Overview The Proposer Selection Updates modify the block production mechanism in dYdX’s consensus layer to allow more control over which validators are eligible to propose blocks. Instead of all active validators participating equally in the proposer rotation, the system introduces a configuration flag (a boolean field) at the validator level. Originally introduced as a CanPropose flag, the mechanism was later inverted into a ProposeDisabled flag to ensure backward compatibility and simplify integration with existing code paths. With the original CanPropose design, validators would need to explicitly set the flag to true in order to propose blocks. This defaulted all existing validators to a “disabled” state, requiring a tightly coordinated network-wide upgrade to restore block production. In contrast, the ProposeDisabled approach defaults to false for validators that do not explicitly set the field, meaning they remain eligible to propose blocks, as intended. This approach ensures that the default behavior remains unchanged for validators, while still allowing dYdX to explicitly restrict certain validators from proposing blocks. To achieve this, coordinated changes were applied across both the CometBFT and Cosmos SDK forks to align pro- poser selection logic with the new flag. The change also highlights a separation of concerns between the Cosmos SDK and CometBFT layers. While the SDK is responsible for validator set management and governance-driven updates (through the MsgSetProposers flow), CometBFT applies the proposer selection logic during consensus. From a protocol perspective, these changes are based on several assumptions about proposer availability and system safety. The design assumes that the selected proposer set is generally more reliable and engaged than the wider validator set, and therefore more likely to ensure liveness. However, if all validators in the proposer set become unavailable (either by downtime or by exiting the active validator set through redelegation), the network may halt. This risk is considered acceptable under the reasoning that “being outside the active set” is equivalent to being down. A minimum threshold of five proposers was chosen as a safeguard: if the proposer set falls below this number, all active validators automatically regain proposer eligibility. This mechanism prioritizes liveness over performance, ensuring the network can continue producing blocks even if the curated proposer set becomes too small. With respect to fairness, the standard CometBFT proposer priority algorithm was chosen, where priorities are adjusted for all validators, but only enabled validators are eligible to propose. While this can lead to oscillations and raise questions about fairness across the proposer set, internal tests suggest that excluded validators do not gain outsized influence if later re-enabled.
Informal Systems © 2025 < Table of Contents 5 dYdX Q3 2025 Proposer Selection Updates
Architecture Overview
Figure 1: Architecture Overview Diagram
Informal Systems © 2025 < Table of Contents 6 dYdX Q3 2025 Proposer Selection Updates
Protocol Overview FinalizeBlock → x/staking EndBlocker Flow:
CometBFT BaseApp Runtime Module Manager x/staking
FinalizeBlock()
ApplyBlock() logic
internalFinalizeBlock()
endBlock()
endBlocker()
EndBlock()
Iterate through modules
EndBlock()
Validator logic
return
return
return
ResponseFinalizeBlock()
CometBFT BaseApp Runtime Module Manager x/staking
Figure 2: FinalizeBlock → x/staking EndBlocker Flow
The ApplyBlock() logic implemented in the CometBFT’s BlockExecutor is described in one of the following diagrams. The FinalizeBlock() function serves as the ABCI interface orchestrator that manages optimistic execution by checking if pre-computed results from a previous optimistic run can be reused, or if the block needs to be re- executed from scratch. It handles concurrency control, streaming service hooks, and delegates the actual block execution to internalFinalizeBlock() while managing the overall execution flow and response formatting. The internalFinalizeBlock() function performs the core block execution by setting up the execution context and sequentially running PreBlock(), BeginBlock(), transaction processing, and EndBlock() phases. It handles the actual state transitions, transaction validation, event collection, and gas metering while supporting cancellation for optimistic execution scenarios. On the level of Runtime, the EndBlocker() method delegates to ModuleManager.EndBlock(ctx). Module Manager calls EndBlock() on all modules that implement HasABCIEndBlock interface and executes mod- ules in the configured order (from app config). The x/staking module implements the module.HasABCIEndBlock interface. The EndBlock() method calls the
Informal Systems © 2025 < Table of Contents 7 dYdX Q3 2025 Proposer Selection Updates
keeper.EndBlocker(ctx) method. The rest of the validator logic implemented in the x/staking module is de- scribed in the next diagram. Validator logic in x/staking:
Module Manager Keeper
EndBlocker()
BlockValidatorUpdates()
ApplyAndReturnValidatorSetUpdates()
✨ GetSendFullProposerSetAbciUpdate() ✨ GetProposers()
Module Manager Keeper
Figure 3: Entry Point and Proposer Retrieval
The EndBlocker() function is called by the Module Manager at the end of each block to update the validator set. It simply delegates to BlockValidatorUpdates() and handles telemetry measurement for performance monitoring. The main orchestrator that calculates and applies all validator set changes for the current block is the BlockVal- idatorUpdate() function. It coordinates validator power updates, handles unbonding/redelegation completions, and returns the final validator updates to CometBFT. This ensures the consensus engine stays synchronized with staking state changes. The ApplyAndReturnValidatorSetUpdates() function processes the active validator set by iterating through valida- tors by power (highest first) and determining state transitions. It handles bonding/unbonding validators, calculates power changes, and prepares ABCI validator updates for CometBFT. sendFullProposerSetUpdate is obtained by calling the GetSendFullProposerSetAbciUpdate() function. This func- tion checks the SendFullProposerSetAbciUpdateKey form the x/staking module’s state store, which is updated by sending the adequate MsgSetProposer message (described in the later diagram). GetProposers() fetches the current list of validators eligible to propose blocks. This list is used to set the Pro- poseDisabled field in validator updates, controlling which validators can propose blocks.
Informal Systems © 2025 < Table of Contents 8 dYdX Q3 2025 Proposer Selection Updates
Keeper Validator
loop [For each validator (by power, highest first)]
alt [Power changed OR sendFullProposerSetUpdate]
✨ proposeDisabled() ✨ ABCIValidatorUpdate() ValidatorUpdate
SetLastValidatorPower()
loop [For each validator that is no longer bonded]
✨ proposeDisabled() ✨ ABCIValidatorUpdateZero() ValidatorUpdate
Keeper Validator
Figure 4: Processing Active and Leaving Validators
Informal Systems © 2025 < Table of Contents 9 dYdX Q3 2025 Proposer Selection Updates
proposeDisabled() A closure function that determines if a validator should have block proposal rights disabled. Returns true if a proposer set exists and the validator is not in it, used for governance-controlled block proposal permissions. ABCIValidatorUpdate function returns an abci.ValidatorUpdate from a staking validator type with the full validator power, while the ABCIValidatorUpdateZero returns an abci.ValidatorUpdate from a staking validator type with zero power used for validator updates. The only change here is the addition of the proposeDisabled flag. The SetLastValidatorPower() persists the validator’s current voting power to the store for use in the next block’s validator set calculations. This maintains the historical power information needed to detect power changes between blocks. ABCIValidatorUpdateZero() creates an ABCI validator update with zero voting power to remove a validator from CometBFT’s active set. This effectively tells the consensus engine to stop considering this validator for block validation and proposal. The only change here is the addition of the proposeDisabled flag.
Keeper Module Manager
alt [ ✨ sendFullProposerSetUpdate] ✨ checkProposerSetInvariants() ✨ SetSendFullProposerSetAbciUpdate(false)
setValidatorUpdates()
return validatorUpdates, nil
Keeper Module Manager
Figure 5: Finalization and Return
checkProposerSetInvariants() validates that the updated proposer set maintains the required invariants (e.g., no duplicate validators, valid addresses). If validation fails, it logs errors and defaults to allowing all validators to propose blocks as a safety mechanism. The SetSendFullProposerSetAbciUpdate(false) function resets the full proposer set update flag back to false after processing. This ensures subsequent blocks return to incremental update behavior rather than sending
Informal Systems © 2025 < Table of Contents 10 dYdX Q3 2025 Proposer Selection Updates
complete validator sets every block. MsgSetProposers Execution Flow:
x/gov Message Server Keeper
✨ SetProposers(MsgSetProposers) Validate authority
alt [k.authority != msg.Authority]
ErrInvalidSigner: invalid authority
✨ SetProposers() ✨ checkProposerSetInvariants() Marshal and store the new proposer set
✨ SetSendFullProposerSetAbciUpdate(true) return
Emit events
MsgSetProposersResponse
x/gov Message Server Keeper
Figure 6: MsgSetProposers Execution Flow
When the SetProposers() method from the Message Server is called, it first makes sure that the authority is valid. If that is the case, it delegates the call to the SetProposers() method implemented in the Keeper. Pay attention to the true flag in the SetSendFullProposerSetAbciUpdate() function. The flag is being stored in the staking module’s state, waiting for the next EndBlock. When the very next block is processed, the ABCI EndBlock sequence ends up in the ApplyAndReturnValidatorSe- tUpdates() function that checks if a full proposer set update is needed by calling the GetSendFullProposerSetAb- ciUpdate() function. If the update is needed, each validator has its ProposeDisabled flag updated. It is set to false if the validator is in the new proposer set, and true if it is not. In the end, the SetSendFullProposerSetAbciUpdate() function is called to reset the flag to false, so this update only happens once.
Informal Systems © 2025 < Table of Contents 11 dYdX Q3 2025 Proposer Selection Updates
CometBFT ApplyBlock() Execution Flow:
Consensus BlockExecutor Mempool ProxyApp State Store
ApplyBlock(state, blockID, block)
Lock()
FinalizeBlock(RequestFinalizeBlock)
Execute all transactions
abciResponse
SaveFinalizeBlockResponse()
ValidatorUpdates()
updateState(state, blockID, header, response)
newState
Commit()
appHash
Update()
Save(newState)
Unlock()
newState, nil
Consensus BlockExecutor Mempool ProxyApp State Store
Figure 7: CometBFT ApplyBlock() Execution Flow Diagram
The FinalizeBlock() function sends block to the application layer for processing via ABCI. The SaveFinalizeBlockResponse() function saves the ABCI response before committing; by doing so, it ensures crash recovery capability. The ValidatorUpdates() function converts the ABCI ValidatorUpdate messages to internal CometBFT Validator types. The updateState() function creates a new blockchain state by applying changes from a processed block, including validator set updates and consensus parameter changes. It handles the rotation of validator sets (current → last, next → current, updated → next) and implements a delayed activation system where validator changes at height H take effect at height H+2, while consensus parameter changes at height H take effect at height H+1. The Commit() function commits application state changes, updates the mempool after commit, and returns height for potential block pruning.
Informal Systems © 2025 < Table of Contents 12 dYdX Q3 2025 Proposer Selection Updates
CometBFT Proposer Selection Flow:
Block Executor Validator Set Validator
alt [validatorUpdates exist]
UpdateWithChangeSet() (H+2 rule)
IncrementProposerPriority()
RescalePriorities()
shiftByAvgProposerPriority()
incrementProposerPriority()
getValWithMostPriority()
CompareProposerPriority()
set new proposer
return updated set
Block Executor Validator Set Validator
Figure 8: CometBFT Proposer Selection Flow Diagram
The UpdateWithChangeSet() function applies validator updates (additions, removals, voting power changes) to the validator set while enforcing the H+2 rule, where changes at height H take effect at height H+2. It validates updates, computes new priorities for added validators, and maintains the integrity of the validator set. The IncrementProposerPriority() function is the main entry point for proposer rotation that rescales priorities to prevent overflow, normalizes them around zero, and then executes the core proposer selection algorithm. It calls the internal incrementProposerPriority() method and updates the validator set’s proposer field. The RescalePriorities() function prevents integer overflow by rescaling all validator priorities when the difference between the max and min priorities becomes too large. It divides all priorities by a calculated ratio to keep them within safe bounds while maintaining their relative ordering. The shiftByAvgProposerPriority() function normalizes validator priorities by subtracting the average
Excerpt (19992 of 69714 characters). Read the whole page on informalsystems/audits ↗