Skip to content

fix(x/listing): enforce markets hard cap with >= (off-by-one) - #3377

Open
SashaMIT wants to merge 1 commit into
dydxprotocol:mainfrom
SashaMIT:fix/listing-hardcap-off-by-one
Open

fix(x/listing): enforce markets hard cap with >= (off-by-one)#3377
SashaMIT wants to merge 1 commit into
dydxprotocol:mainfrom
SashaMIT:fix/listing-hardcap-off-by-one

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Aug 8, 2026

Copy link
Copy Markdown

Summary

Sibling of the pair-case listing hardenings (#3375 / #3376). Distinct root cause.

MsgCreateMarketPermissionless rejected only when numPerpetuals > HardCapForMarkets. When the count already equalled the governance hard cap, a signed lister could still create one more market, leaving the chain at hardCap+1. Proto text describes a hard cap on the total number of markets listed.

Fix

Use >= so equality rejects with ErrMarketsHardCapReached.

Testing

cd protocol
go test ./x/listing/keeper/ -count=1 -run 'TestMsgCreateMarketPermissionless$' -timeout 120s

Includes a new case: hard cap equal to current perpetual count must fail.

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Corrected market creation limits to prevent new perpetual markets from being created when the configured hard cap has already been reached.
    • Added regression coverage to verify the hard-cap boundary condition.

CreateMarketPermissionless used `>` so a listing still succeeded when
perpetual count already equalled HardCapForMarkets, leaving the chain
at hardCap+1. Reject at equality; add regression coverage.

Signed-off-by: Sasha Mitchell <sash.t.mitchell@gmail.com>
@SashaMIT
SashaMIT requested a review from a team as a code owner August 8, 2026 12:09
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The permissionless market creation check now rejects requests when the perpetual count equals or exceeds the configured hard cap. A regression test covers the equality boundary and expects ErrMarketsHardCapReached.

Changes

Market hard-cap enforcement

Layer / File(s) Summary
Reject listings at the hard cap
protocol/x/listing/keeper/msg_create_market_permissionless.go, protocol/x/listing/keeper/msg_create_market_permissionless_test.go
The validation rejects market creation when the perpetual count reaches or exceeds the hard cap. The test configures the cap from the current count and verifies ErrMarketsHardCapReached.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: pml

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the off-by-one fix that enforces the markets hard cap with a greater-than-or-equal comparison.
Description check ✅ Passed The description explains the root cause, fix, and regression test; it omits the repository checklist but includes the critical required information.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
protocol/x/listing/keeper/msg_create_market_permissionless_test.go (1)

124-128: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Drive the boundary setup from table data, not the subtest name.

Line 125 selects behavior from the exact test name. If someone renames the case, hardCap remains 0, and the test can pass without verifying current count == hardCap. Add a boolean field to the test case and branch on that field.

Suggested refactor
 tests := map[string]struct {
 	ticker  string
 	hardCap uint32
 	balance *big.Int
+	hardCapEqualsCurrentCount bool
 	expectedErr error
 }{
 	"failure - hard cap equal to current perpetual count": {
 		ticker:  "TEST2-USD",
 		hardCap: 0,
 		balance: big.NewInt(10_000_000_000),
+		hardCapEqualsCurrentCount: true,
 		expectedErr: types.ErrMarketsHardCapReached,
 	},
 
-	if name == "failure - hard cap equal to current perpetual count" {
+	if tc.hardCapEqualsCurrentCount {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@protocol/x/listing/keeper/msg_create_market_permissionless_test.go` around
lines 124 - 128, The hard-cap boundary setup in the table-driven test currently
depends on the subtest name, allowing renames to disable the intended case. Add
a boolean field to the test-case structure, set it for the equal-current-count
scenario, and update the branch near hardCap initialization to use that field
instead of comparing name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@protocol/x/listing/keeper/msg_create_market_permissionless.go`:
- Around line 16-18: Update the market-cap check in the permissionless market
creation flow to avoid narrowing numPerpetuals to uint32; convert both
numPerpetuals and the value returned by Keeper.GetMarketsHardCap(ctx) to uint64
before comparing with the existing greater-than-or-equal condition.

---

Nitpick comments:
In `@protocol/x/listing/keeper/msg_create_market_permissionless_test.go`:
- Around line 124-128: The hard-cap boundary setup in the table-driven test
currently depends on the subtest name, allowing renames to disable the intended
case. Add a boolean field to the test-case structure, set it for the
equal-current-count scenario, and update the branch near hardCap initialization
to use that field instead of comparing name.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 196dc586-fc00-4c2c-ab81-5ec5c5ba287e

📥 Commits

Reviewing files that changed from the base of the PR and between 91316e6 and 67067d8.

📒 Files selected for processing (2)
  • protocol/x/listing/keeper/msg_create_market_permissionless.go
  • protocol/x/listing/keeper/msg_create_market_permissionless_test.go

Comment on lines +16 to +18
// Reject when already at or above HardCapForMarkets (`>` was off-by-one).
numPerpetuals := len(k.PerpetualsKeeper.GetAllPerpetuals(ctx))
if uint32(numPerpetuals) > k.Keeper.GetMarketsHardCap(ctx) {
if uint32(numPerpetuals) >= k.Keeper.GetMarketsHardCap(ctx) {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the count without narrowing it to uint32.

uint32(numPerpetuals) wraps when the perpetual count exceeds the largest uint32 value. A wrapped count can make a full state appear below the cap and allow another listing. Compare both values as uint64.

Suggested fix
-	if uint32(numPerpetuals) >= k.Keeper.GetMarketsHardCap(ctx) {
+	if uint64(numPerpetuals) >= uint64(k.Keeper.GetMarketsHardCap(ctx)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Reject when already at or above HardCapForMarkets (`>` was off-by-one).
numPerpetuals := len(k.PerpetualsKeeper.GetAllPerpetuals(ctx))
if uint32(numPerpetuals) > k.Keeper.GetMarketsHardCap(ctx) {
if uint32(numPerpetuals) >= k.Keeper.GetMarketsHardCap(ctx) {
// Reject when already at or above HardCapForMarkets (`>` was off-by-one).
numPerpetuals := len(k.PerpetualsKeeper.GetAllPerpetuals(ctx))
if uint64(numPerpetuals) >= uint64(k.Keeper.GetMarketsHardCap(ctx)) {
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 17-17: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(numPerpetuals)
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@protocol/x/listing/keeper/msg_create_market_permissionless.go` around lines
16 - 18, Update the market-cap check in the permissionless market creation flow
to avoid narrowing numPerpetuals to uint32; convert both numPerpetuals and the
value returned by Keeper.GetMarketsHardCap(ctx) to uint64 before comparing with
the existing greater-than-or-equal condition.

Source: Linters/SAST tools

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

Labels

Development

Successfully merging this pull request may close these issues.

1 participant