Skip to content

fix: harden weighted-average rounding and support custody - #24

Open
jbernal87 wants to merge 2 commits into
masterfrom
fix/BB-311-hotfix
Open

fix: harden weighted-average rounding and support custody#24
jbernal87 wants to merge 2 commits into
masterfrom
fix/BB-311-hotfix

Conversation

@jbernal87

@jbernal87 jbernal87 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add explicit migration paths for the deployed Code 67 and Code 1962 lineages
  • preserve exact order-book remainders with checked 512-bit arithmetic until price and quantity tick rounding
  • enforce a per-step spendable-balance floor so atomic settlement cannot consume pre-existing contract support
  • validate minimum output against the integer bank amount actually delivered
  • update the Injective 1.20 TestTube stack, Rust toolchain, and release optimizer

Root cause

Fixed-point multiplication and division truncated the exact weighted-average remainder before upward tick rounding. A crafted ask book could therefore make a value just above a tick appear exactly aligned with the lower tick, understate the average price, and oversize an atomic buy. The estimator admitted the order against the contract's complete quote balance, allowing successful settlement to debit support that the caller did not provide.

Summary by CodeRabbit

  • New Features

    • Added migration paths for Code 67 and Code 1962 deployments.
    • Improved swap calculations with precise liquidity selection and tick-aware rounding.
    • Added safeguards to preserve support funds during swaps.
    • Minimum-output checks now use actual integer coin amounts.
  • Bug Fixes

    • Corrected orderbook rounding, atomic swap fund usage, and minimum-output validation.
    • Improved handling of fractional coin conversions.
  • Tooling

    • Updated Rust and release tooling for more reliable builds and testing.
    • Added expanded coverage for migrations, custody, arithmetic, and rounding scenarios.
  • Documentation

    • Added release notes for version 1.1.3.

- bump the Injective stack to 1.20.0 and Rust to 1.86.0
- update release builds to CosmWasm optimizer 0.17.0
- add TestTube migration coverage for both mainnet deployments
- preserve FPDecimal coin conversion semantics under CosmWasm 3
@linear

linear Bot commented Aug 1, 2026

Copy link
Copy Markdown

BB-311

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release updates Rust and dependency tooling, adds migrations from two legacy contract versions, centralizes exact swap arithmetic, and validates support-fund preservation and integer output amounts. Tests cover migrations, arithmetic, custody, conversions, and a rounding regression.

Changes

Swap contract release

Layer / File(s) Summary
Release tooling and package updates
.github/workflows/rust.yaml, Cargo.toml, build_release.sh, contracts/swap/Cargo.toml, rust-toolchain.toml, Changelog.md
Rust 1.86.0, updated dependencies, optimizer tooling, integration features, package version 1.1.3, and release notes are added.
Contract migration support
contracts/swap/src/contract.rs, contracts/swap/src/helpers.rs, contracts/swap/src/testing/migration_test.rs, contracts/swap/src/testing/test_utils.rs
Migrations support Code 67 and Code 1962 state, preserve configuration and routes, record migration metadata, and test release artifacts.
Exact arithmetic and swap estimation
contracts/swap/src/exact_math.rs, contracts/swap/src/queries.rs, contracts/swap/src/lib.rs, contracts/swap/src/state.rs
Checked liquidity selection, weighted pricing, ratio conversion, and directional tick rounding replace the previous estimation calculations.
Fund preservation and output conversion
contracts/swap/src/swap.rs, contracts/swap/src/types.rs, contracts/swap/src/error.rs, contracts/swap/src/testing/*
Swap execution tracks bank and subaccount funds, rejects support-fund consumption, floors output coins, validates minimum output, and adds custody, conversion, arithmetic, and rounding regression tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SwapContract
  participant BankModule
  participant InjectiveSubaccount
  participant OrderReply
  participant SwapRecipient
  SwapContract->>BankModule: query bank balance
  SwapContract->>InjectiveSubaccount: query available deposit
  SwapContract->>OrderReply: place atomic order
  OrderReply-->>SwapContract: return order result
  SwapContract->>BankModule: re-query spendable funds
  SwapContract->>SwapRecipient: send floored output coin
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the two primary changes: weighted-average rounding hardening and support-fund custody protection.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/BB-311-hotfix

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

@jbernal87
jbernal87 marked this pull request as ready for review August 1, 2026 07:53
@jbernal87
jbernal87 requested a review from gorgos August 1, 2026 07:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
contracts/swap/src/swap.rs (1)

292-310: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import Coin instead of using the fully qualified path.

The logic in both helpers is correct. ensure_minimum_output spells the type as cosmwasm_std::Coin while line 10 already imports other cosmwasm_std items. Add Coin to that import list for consistency.

🤖 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 `@contracts/swap/src/swap.rs` around lines 292 - 310, Update the existing
cosmwasm_std import list to include Coin, then change ensure_minimum_output’s
parameter type to use the imported Coin directly instead of cosmwasm_std::Coin;
leave both helper implementations unchanged.
contracts/swap/src/exact_math.rs (1)

126-134: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider rejecting a zero rounded ratio explicitly.

ratio_to_tick can return FPDecimal::ZERO when tick_count truncates to zero in the Down direction. weighted_average_to_tick guards this case at line 112, but ratio_to_tick does not. estimate_execution_buy_from_source uses the Down result as result_quantity, so a dust input produces a zero-quantity atomic order and an opaque exchange rejection. An explicit error keeps the failure diagnosable.

♻️ Proposed guard
     let rounded_raw = tick_count
         .checked_mul(U512::from(tick.num))
         .ok_or_else(|| arithmetic_error("rounded ratio overflow"))?;
 
+    if rounded_raw.is_zero() {
+        return Err(arithmetic_error("rounded ratio must be positive"));
+    }
+
     Ok(FPDecimal::from(to_u256(rounded_raw)?))
🤖 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 `@contracts/swap/src/exact_math.rs` around lines 126 - 134, Update
ratio_to_tick around the tick_count and rounded_raw calculation to reject a zero
rounded ratio before returning FPDecimal::from(...). Match the existing
zero-result validation used by weighted_average_to_tick, and return a
descriptive arithmetic error so Down-direction dust inputs cannot produce a
zero-quantity order.
contracts/swap/src/queries.rs (1)

146-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the raw tick constant.

FPDecimal::from(primitive_types::U256::one()) appears here and again at line 331 in estimate_execution_sell_from_target. Both express "one raw fixed-point unit". A named helper or constant in exact_math makes the intent explicit and prevents the two sites from diverging.

🤖 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 `@contracts/swap/src/queries.rs` around lines 146 - 148, Extract the repeated
one-raw-fixed-point-unit value into a named helper or constant in exact_math,
then replace the FPDecimal::from(primitive_types::U256::one()) usages in the
current query flow and estimate_execution_sell_from_target with it. Preserve the
existing FPDecimal value and calculation behavior.
contracts/swap/src/testing/migration_test.rs (1)

60-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the missing-config migration failure path.

assert_storage_migration always saves CONFIG before calling migrate, so it never exercises the get_config(deps.storage)? validation branch in contracts/swap/src/contract.rs (Line 134). Add a test that sets a supported legacy cw2::ContractVersion without saving CONFIG, and asserts that migrate returns an error. This closes a coverage gap on a validation path meant to guard a mainnet migration.

🤖 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 `@contracts/swap/src/testing/migration_test.rs` around lines 60 - 91, Add a
migration test alongside assert_storage_migration that stores a supported legacy
cw2::ContractVersion without saving CONFIG, invokes migrate, and asserts it
returns an error. Keep the existing successful migration helper unchanged and
target the get_config validation path in migrate.
contracts/swap/src/contract.rs (1)

20-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Migration-source constants are duplicated across production and test code. The legacy contract name/version literals are declared once in contracts/swap/src/contract.rs and re-declared independently in contracts/swap/src/testing/migration_test.rs. A future version bump in one place will not propagate to the other, letting tests validate a stale version pairing without failing.

  • contracts/swap/src/contract.rs#L20-L24: change CODE_67_LEGACY_CONTRACT_NAME, CODE_67_LEGACY_CONTRACT_VERSION, CODE_1962_CONTRACT_NAME, and CODE_1962_CONTRACT_VERSION to pub(crate).
  • contracts/swap/src/testing/migration_test.rs#L23-L26: remove the locally duplicated CODE_67_CW2_NAME, CODE_67_CW2_VERSION, CODE_1962_CW2_NAME, CODE_1962_CW2_VERSION constants and import the pub(crate) constants from contract.rs instead.
🤖 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 `@contracts/swap/src/contract.rs` around lines 20 - 24, The migration contract
constants are duplicated between production and test code; make the four
constants in contracts/swap/src/contract.rs#L20-L24 pub(crate), then remove the
duplicate constants in contracts/swap/src/testing/migration_test.rs#L23-L26 and
import and reuse the production constants in the migration tests.
🤖 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 @.github/workflows/rust.yaml:
- Around line 32-36: Update the Rust CI workflow by replacing the archived
actions-rs/toolchain@v1 step with dtolnay/rust-toolchain, preserving the minimal
profile and Rust 1.86.0 toolchain settings. Replace every actions-rs/cargo@v1
usage in the workflow with equivalent direct cargo commands while retaining the
existing CI operations and arguments.

In `@contracts/swap/src/state.rs`:
- Line 29: Update the error message in the visible map_err closure to use a
single negation, such as indicating that no swap route was found between
source_denom and target_denom. Before changing it, check tests for assertions
against the current message and update only those expected strings as needed.

---

Nitpick comments:
In `@contracts/swap/src/contract.rs`:
- Around line 20-24: The migration contract constants are duplicated between
production and test code; make the four constants in
contracts/swap/src/contract.rs#L20-L24 pub(crate), then remove the duplicate
constants in contracts/swap/src/testing/migration_test.rs#L23-L26 and import and
reuse the production constants in the migration tests.

In `@contracts/swap/src/exact_math.rs`:
- Around line 126-134: Update ratio_to_tick around the tick_count and
rounded_raw calculation to reject a zero rounded ratio before returning
FPDecimal::from(...). Match the existing zero-result validation used by
weighted_average_to_tick, and return a descriptive arithmetic error so
Down-direction dust inputs cannot produce a zero-quantity order.

In `@contracts/swap/src/queries.rs`:
- Around line 146-148: Extract the repeated one-raw-fixed-point-unit value into
a named helper or constant in exact_math, then replace the
FPDecimal::from(primitive_types::U256::one()) usages in the current query flow
and estimate_execution_sell_from_target with it. Preserve the existing FPDecimal
value and calculation behavior.

In `@contracts/swap/src/swap.rs`:
- Around line 292-310: Update the existing cosmwasm_std import list to include
Coin, then change ensure_minimum_output’s parameter type to use the imported
Coin directly instead of cosmwasm_std::Coin; leave both helper implementations
unchanged.

In `@contracts/swap/src/testing/migration_test.rs`:
- Around line 60-91: Add a migration test alongside assert_storage_migration
that stores a supported legacy cw2::ContractVersion without saving CONFIG,
invokes migrate, and asserts it returns an error. Keep the existing successful
migration helper unchanged and target the get_config validation path in migrate.
🪄 Autofix (Beta)

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

Run ID: 3cc14ff0-944c-470e-bff7-17495c22beda

📥 Commits

Reviewing files that changed from the base of the PR and between 772192d and f3d5bcf.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • contracts/swap/src/testing/test_artifacts/mainnet_code_1962.wasm is excluded by !**/*.wasm
  • contracts/swap/src/testing/test_artifacts/mainnet_code_67.wasm is excluded by !**/*.wasm
📒 Files selected for processing (22)
  • .github/workflows/rust.yaml
  • Cargo.toml
  • Changelog.md
  • build_release.sh
  • contracts/swap/Cargo.toml
  • contracts/swap/src/contract.rs
  • contracts/swap/src/error.rs
  • contracts/swap/src/exact_math.rs
  • contracts/swap/src/helpers.rs
  • contracts/swap/src/lib.rs
  • contracts/swap/src/queries.rs
  • contracts/swap/src/state.rs
  • contracts/swap/src/swap.rs
  • contracts/swap/src/testing/custody_tests.rs
  • contracts/swap/src/testing/exact_math_tests.rs
  • contracts/swap/src/testing/migration_test.rs
  • contracts/swap/src/testing/mod.rs
  • contracts/swap/src/testing/test_utils.rs
  • contracts/swap/src/testing/types_tests.rs
  • contracts/swap/src/testing/weighted_average_rounding_regression.rs
  • contracts/swap/src/types.rs
  • rust-toolchain.toml

Comment on lines +32 to +36
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.78.0
toolchain: 1.86.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

if rg -n 'actions-rs/(toolchain|cargo)`@v1`' .github/workflows; then
  echo "Archived actions remain" >&2
  exit 1
fi

Repository: InjectiveLabs/swap-contract

Length of output: 157


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*'

printf '%s\n' '--- actions-rs references ---'
rg -n -C 3 'actions-rs/(toolchain|cargo)`@v1`' .github/workflows || true

printf '%s\n' '--- workflow sections ---'
sed -n '1,145p' .github/workflows/rust.yaml

Repository: InjectiveLabs/swap-contract

Length of output: 157


🌐 Web query:

actions-rs/toolchain GitHub archived actions-rs/cargo maintenance status

💡 Result:

The entire actions-rs organization, including the actions-rs/toolchain and actions-rs/cargo repositories, has been archived by the owner [1][2][3][4]. As of October 13, 2023, these repositories are in a read-only state and are no longer maintained [5][6]. The organization was archived due to the primary maintainer stepping back, and the community has widely recognized these actions as unmaintained [3][4][7][8]. Users are advised to migrate to alternative, actively maintained solutions [3]. For example, dtolnay/rust-toolchain is a common replacement for actions-rs/toolchain, while standard cargo commands are often used in place of actions-rs/cargo [3].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- file metadata ---'
stat .github/workflows/rust.yaml 2>&1 || true

printf '%s\n' '--- requested workflow ranges ---'
for range in '1,75' '90,135'; do
  printf '%s\n' "--- lines $range ---"
  sed -n "${range}p" .github/workflows/rust.yaml 2>&1 || true
done

printf '%s\n' '--- exact action references ---'
grep -nE 'actions-rs/(toolchain|cargo)`@v1`' .github/workflows/rust.yaml 2>&1 || true

Repository: InjectiveLabs/swap-contract

Length of output: 157


🌐 Web query:

GitHub Actions archived action still runs actions-rs toolchain Node runtime actionlint too old to run

💡 Result:

The actions-rs organization, including actions-rs/toolchain, was archived by its owner in October 2023 and is unmaintained [1][2][3]. Because these actions were authored using older Node.js runtimes (such as Node 12) that have long been deprecated by GitHub Actions, they trigger deprecation warnings in CI workflows [1][4][5]. While these archived actions may still execute in some environments, they are technically unsupported and incompatible with the current GitHub Actions runtime requirements [6][4]. As of mid-2026, GitHub Actions has largely transitioned to Node.js 24 [7][8]. Using unmaintained actions that rely on obsolete Node.js versions poses significant risks of pipeline failure as GitHub continues to remove support for older runtimes [7][8]. To resolve these issues and remove deprecation warnings, you should replace the deprecated actions with maintained alternatives [1][6]: 1. Replace actions-rs/toolchain with dtolnay/rust-toolchain, which is the industry-standard, maintained alternative [1][4][9]. 2. Replace other actions like actions-rs/cargo, actions-rs/clippy-check, and actions-rs/audit-check with direct shell commands (e.g., cargo build, cargo clippy) or community-supported forks [1][6]. The error mentioning actionlint being "too old" likely refers to the fact that your local or CI-based linter (actionlint) is flagging the use of these deprecated, archived actions or the underlying obsolete Node.js runtime configuration in your workflow files [1][4]. Updating your workflow syntax to use the recommended modern replacements will resolve these linting errors and ensure your pipelines remain compatible with current and future GitHub Actions environments [1][9].

Citations:


Replace the archived actions-rs actions. The actions-rs organization has been archived since October 2023. Replace actions-rs/toolchain@v1 with dtolnay/rust-toolchain and replace actions-rs/cargo@v1 with direct cargo commands to avoid unsupported CI dependencies.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 33-33: the runner of "actions-rs/toolchain@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 zizmor (1.28.0)

[warning] 33-33: action or reusable workflow from archived repository (archived-uses): repository is archived

(archived-uses)

🤖 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 @.github/workflows/rust.yaml around lines 32 - 36, Update the Rust CI
workflow by replacing the archived actions-rs/toolchain@v1 step with
dtolnay/rust-toolchain, preserving the minimal profile and Rust 1.86.0 toolchain
settings. Replace every actions-rs/cargo@v1 usage in the workflow with
equivalent direct cargo commands while retaining the existing CI operations and
arguments.

Source: Linters/SAST tools

SWAP_ROUTES
.load(storage, key)
.map_err(|_| StdError::generic_err(format!("No swap route not found from {source_denom} to {target_denom}",)))
.map_err(|_| StdError::msg(format!("No swap route not found from {source_denom} to {target_denom}",)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the double negative in the error message.

The message reads "No swap route not found from ... to ...". This states the opposite of the condition. Use a single negation.

🐛 Proposed fix
-        .map_err(|_| StdError::msg(format!("No swap route not found from {source_denom} to {target_denom}",)))
+        .map_err(|_| StdError::msg(format!("No swap route found from {source_denom} to {target_denom}")))

Check whether any test asserts on the current text before you change it.

📝 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
.map_err(|_| StdError::msg(format!("No swap route not found from {source_denom} to {target_denom}",)))
.map_err(|_| StdError::msg(format!("No swap route found from {source_denom} to {target_denom}")))
🤖 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 `@contracts/swap/src/state.rs` at line 29, Update the error message in the
visible map_err closure to use a single negation, such as indicating that no
swap route was found between source_denom and target_denom. Before changing it,
check tests for assertions against the current message and update only those
expected strings as needed.

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.

1 participant