Skip to content

test(contract): add invariant coverage for fee_bps upper bound - #1410

Open
Ajibose wants to merge 3 commits into
Epta-Node:mainfrom
Ajibose:fix/set-fee-upper-bound-enforcement
Open

test(contract): add invariant coverage for fee_bps upper bound#1410
Ajibose wants to merge 3 commits into
Epta-Node:mainfrom
Ajibose:fix/set-fee-upper-bound-enforcement

Conversation

@Ajibose

@Ajibose Ajibose commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #1244

Summary

set_fee (lib.rs:2715) is the admin entry point for updating the protocol fee. The issue reported that it did not clamp/reject fee_bps values above 10000 (100%), which would make fee computation (amount * fee_bps / 10_000) exceed the transferred amount and could brick tip/pool flows.

Findings

On inspection, set_fee already calls validate_protocol_fee(&env, fee_bps) (lib.rs:2720), which delegates to validate_u32_range(env, "fee_bps", fee_bps, 0, MAX_FEE_BPS) (validation.rs:159-161) with MAX_FEE_BPS = 10_000 (validation.rs:11). This already rejects (panics on) any fee_bps > 10_000 and leaves the previously stored value untouched, since the panic occurs before env.storage().instance().set(&FEE_BPS, &fee_bps) is reached.

So the enforcement described in the issue is already present in the production code. What was missing was explicit test coverage proving the invariant 0 <= fee_bps <= 10_000 holds, per the issue's acceptance criteria ("Invariant test added"). This PR adds that coverage.

Files Changed

Modified:

  • packages/contracts/contracts/linkora-contracts/src/test.rs — unit tests for set_fee boundary/rejection behavior
  • packages/contracts/contracts/linkora-contracts/src/tests/invariants.rs — dedicated invariant tests for the fee_bps bound

No changes were needed to lib.rs, since the guard already existed there.

Tests Added

In src/test.rs:

  • test_set_fee_max_boundary_valid — setting fee_bps = 10_000 (the maximum, 100%) succeeds and is stored correctly.
  • test_set_fee_rejects_value_above_max — setting fee_bps = 20_000 panics with "fee_bps must be between 0 and 10000".
  • test_set_fee_rejects_value_above_max_leaves_stored_fee_unchanged — after a valid fee is set, an out-of-range update panics and the previously stored fee is left untouched (no partial/corrupted state).

In src/tests/invariants.rs:

  • test_invariant_fee_bps_bounded_at_boundaries — asserts both boundary values (0 and 10_000) are accepted and stored as-is.
  • test_invariant_fee_bps_never_exceeds_max — asserts an admin misconfiguration (fee_bps = 20_000) is rejected outright (not clamped) and that the stored fee remains at its last valid value.

How to Test

From packages/contracts/contracts/linkora-contracts:

cargo test --lib fee_bps
cargo test --lib set_fee

All new tests pass. The full cargo test --lib suite has 16 pre-existing failures in unrelated tip_* tests (confirmed present on main prior to this change, unaffected by this PR).

CI Green-up (unrelated pre-existing breakage)

This branch was rebased onto the current main (was 46 commits behind), which surfaced that main's HEAD is itself red on Unit Tests / Lint / JS-TS typecheck, unrelated to fee_bps. Fixed alongside this PR so it can merge green:

  • rustfmt drift: CI's rustc 1.98.0 formats a few unrelated spots in lib.rs/test.rs differently than the version these files were last formatted with. Reformatted under 1.98.0 to match CI.
  • packages/sdk lint errors: unused imports in events-drift.test.ts, a dead callCount in health.test.ts, and any/require() usage in write.test.ts (scoped with the same eslint-disable convention already used elsewhere in the package for legitimate mock reflection).
  • packages/sdk typecheck errors: client.ts was missing the scvU64/scvSymbol helpers already present in generated/client.ts, used by prepareLikePostTx/prepareTipTx/preparePoolDepositTx. events/types.ts declared post_id as number on three event interfaces while every other post_id in the codebase (including elsewhere in this same file) is bigint.
  • packages/sdk test failures, unmasked once typecheck was fixed (previously the JS/TS job never got far enough to run pnpm test):
    • write.test.ts's @stellar/stellar-base mock had Account: jest.fn() with no implementation, so the asserted _accountId was never actually set on constructed instances.
    • ConnectionHealthMonitor.scheduleCheck's initial-check jitter used Math.random() * backoffMs (default 1000ms) instead of a small interval-scaled jitter, so the "immediate" first health check could be delayed up to a full second — racing past health.test.ts's 500ms waitFor and, once one test timed out before reaching monitor.stop(), leaking a live real-timer loop that polluted call counts in every later test in the file.

None of this is related to the fee_bps invariant fix itself — it's cleanup needed to get a green CI run on top of current main.

@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Ajibose Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Ajibose is attempting to deploy a commit to the Jaja's projects Team on Vercel.

A member of the Team first needs to authorize it.

…Node#1244)

set_fee already enforces 0 <= fee_bps <= 10000 via validate_protocol_fee,
but this invariant lacked explicit test coverage. Add unit tests
verifying boundary values (0, 10000) succeed, values above 10000 panic
and leave the previously stored fee untouched, plus a dedicated
invariant test asserting the bound holds across the admin fee-update
flow.
Rebasing Epta-Node#1244's PR onto current upstream main surfaced several
pre-existing CI failures that also affect main's HEAD, unrelated to
the fee_bps invariant work but blocking a green PR:

- rustfmt drift: CI runs rustc 1.98.0, one point release ahead of what
  formatted these files; reformat lib.rs/test.rs under 1.98.0 so
  `cargo fmt --check` matches CI.
- packages/sdk lint errors: unused imports in events-drift.test.ts,
  a dead `callCount` in health.test.ts, and any/require usage in
  write.test.ts (now scoped with the same eslint-disable convention
  used elsewhere in the package for legitimate mock reflection).
- packages/sdk typecheck errors: client.ts was missing the scvU64/
  scvSymbol helpers (present in generated/client.ts) used by
  prepareLikePostTx/prepareTipTx/preparePoolDepositTx; events/types.ts
  declared post_id as number on three interfaces while every other
  post_id in the codebase (including this same file) is bigint.
- packages/sdk test failures, now unmasked by the typecheck fix: the
  write.test.ts stellar-base mock's `Account` was a bare jest.fn()
  with no implementation, so asserted _accountId never matched; and
  ConnectionHealthMonitor.scheduleCheck's initial-check jitter used
  `Math.random() * backoffMs` (default 1000ms) instead of a small
  interval-scaled jitter, so the "immediate" first health check could
  be delayed up to a full second — racing past health.test.ts's
  500ms waitFor and, once one test timed out without reaching
  monitor.stop(), leaking a live real-timer loop that polluted call
  counts in every subsequent test in the file.
@Ajibose
Ajibose force-pushed the fix/set-fee-upper-bound-enforcement branch from 8d266d1 to 8b1679c Compare August 28, 2026 22:13
CI's clippy -D warnings failed on unnecessary_cast: author_posts.len()
already returns u32, so the trailing `as u32` was a no-op that only
newer clippy started flagging.
@devJaja

devJaja commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

@Ajibose

Resolve the conflicts please

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.

Contract: set_fee accepts fee_bps > 10000 (100%), allowing a fee that exceeds the tip/pool amount

2 participants