Skip to content

Fix #351 — burn_for_basket early return for zero/empty recipients#424

Merged
Junman140 merged 1 commit into
Pi-Defi-world:devfrom
temisan0x:fix/351-burn-basket-early-return
Jun 25, 2026
Merged

Fix #351 — burn_for_basket early return for zero/empty recipients#424
Junman140 merged 1 commit into
Pi-Defi-world:devfrom
temisan0x:fix/351-burn-basket-early-return

Conversation

@temisan0x

@temisan0x temisan0x commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Fix #351 — burn_for_basket early return for zero/empty recipients

Problem

When recipients was empty, redeem_basket still ran several storage
reads and allocated Vec::new before hitting the guard — wasting gas
on a guaranteed no-op.

Fix

  • Moved the recipients.is_empty() check to immediately after
    user.require_auth(), before any storage reads or heap allocations.
  • Empty recipients now returns an empty Vec<i128> immediately (no-op)
    instead of panicking with InvalidRecipient.

Test changes

  • test.rs — renamed test_redeem_basket_rejects_empty_recipients to
    test_redeem_basket_allows_empty_recipients_as_noop and flipped
    assertion to expect success with empty return vec.
  • fuzz_test.rs — updated proptest to assert num_recipients == 0
    succeeds as no-op; only mismatched non-zero counts error.
  • All tests pass locally across all test files.

CI note

The fetch_token_wasm.sh CI failure is unrelated to this change.
The script clones soroban-examples at v21.6.0 and expects path
contracts/tokens/stellar_asset which no longer exists at that tag.

Closes #351

@drips-wave

drips-wave Bot commented Jun 24, 2026

Copy link
Copy Markdown

@temisan0x 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

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The burning contract switches oracle calls to *_WITH_TS entrypoints in both redeem_single and redeem_basket, refactors basket redemption to a two-phase weight/payout model with strict post-allocation sum checks, adds admin maintenance methods (pause, fee params, dependency address updates), migrates admin error variants to a new BurningError enum, and aligns all tests with shared test helpers.

Changes

Burning Contract Oracle, Basket, and Admin Changes

Layer / File(s) Summary
Imports, BurningError enum, and ADMIN_TIMELOCK_SECONDS
acbu_burning/src/lib.rs
Oracle import symbols switch to ORACLE_GET_*_WITH_TS variants; ADMIN_TIMELOCK_SECONDS constant and BurningError enum with timelock/admin-transfer failure variants are introduced.
redeem_single: timestamped oracle rates and event update
acbu_burning/src/lib.rs
ACBU and per-currency rates are fetched via *_WITH_TS oracle symbols and validated against current ledger timestamp; BurnEvent fields are updated to include rate and timestamp.
redeem_basket: early empty-recipients guard and weight aggregation
acbu_burning/src/lib.rs
Empty recipients is rejected immediately; a two-phase preparation computes and validates basket weights (rejects negatives and non-positive totals), then derives per-leg fee and net amounts.
redeem_basket: per-leg payout, S-token transfer, and post-allocation validation
acbu_burning/src/lib.rs
Each leg's native_i is computed from net_acbu_i and oracle acbu_rate; S-token transfer_from is standardized; end-of-function asserts that sum of gross and fee allocations exactly matches inputs.
Admin maintenance methods and BurningError migration
acbu_burning/src/lib.rs
Public methods added for pause/unpause, fee get/set, and dependency address updates; accept_admin and cancel_admin_transfer panic with BurningError variants; broken check_paused section and stale comments removed.
Test suite realignment with shared helpers and new assertions
acbu_burning/tests/test.rs, acbu_burning/tests/fuzz_test.rs, acbu_burning/tests/redeem_basket.rs
All test files refactored to use setup_test and shared mock components; upgrade rejection test expects Error(Contract, #12); fuzz test replaces panic::catch_unwind with direct try_redeem_basket result checks; insufficient-reserves test receives currs argument.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant BurningContract
  participant Oracle
  participant SToken
  participant ReserveTracker

  rect rgba(70, 130, 180, 0.5)
    Note over Caller,ReserveTracker: redeem_basket flow
    Caller->>BurningContract: redeem_basket(acbu_amount, recipients)
    BurningContract->>BurningContract: reject if recipients empty
    BurningContract->>BurningContract: compute & validate basket weights
    loop per currency leg i
      BurningContract->>Oracle: ORACLE_GET_ACBU_RATE_WITH_TS
      Oracle-->>BurningContract: (acbu_rate, oracle_ts)
      BurningContract->>Oracle: ORACLE_GET_RATE_WITH_TS(currency_i)
      Oracle-->>BurningContract: (currency_rate, oracle_ts)
      BurningContract->>BurningContract: compute net_acbu_i → native_i
      BurningContract->>ReserveTracker: check reserve sufficiency
      ReserveTracker-->>BurningContract: ok
      BurningContract->>SToken: transfer_from(spender, recipient_i, native_i)
      BurningContract->>BurningContract: emit BurnEvent(leg i)
    end
    BurningContract->>BurningContract: assert Σgross == acbu_amount && Σfee == total_fee
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 Hop, hop through the basket code I go,
Timestamps now guard each oracle's glow,
Empty recipients turned away at the gate,
Weights summed precisely — no rounding debate,
New admin levers, errors named right,
Tests aligned with helpers, clean and bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR adds unrelated admin, fee, pause, and dependency-update APIs beyond the empty-recipient fix. Remove or split out the unrelated admin/maintenance API changes into a separate PR focused on the early-return fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: early return for empty basket recipients.
Linked Issues check ✅ Passed The PR adds an earlier empty-recipient check in redeem_basket, matching the issue’s no-op early-return requirement.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
acbu_burning/src/lib.rs (1)

160-170: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject future-dated oracle timestamps too.

The freshness check only rejects old timestamps. A future oracle_timestamp/rate_timestamp passes and extends the accepted freshness window.

Proposed fix
-        if current_time > oracle_timestamp.saturating_add(UPDATE_INTERVAL_SECONDS) {
+        if oracle_timestamp > current_time
+            || current_time > oracle_timestamp.saturating_add(UPDATE_INTERVAL_SECONDS)
+        {
             env.panic_with_error(ContractError::OracleError);
         }

Apply the same pattern to each rate_timestamp check.

Also applies to: 312-413

🤖 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 `@acbu_burning/src/lib.rs` around lines 160 - 170, The freshness checks in the
oracle lookup flow only reject stale timestamps, so future-dated values can
incorrectly pass; update the timestamp validation around the oracle fetch logic
in the rate retrieval path to reject both old and future timestamps. Apply the
same fix wherever the timestamp freshness pattern is used, including the checks
tied to oracle_timestamp and rate_timestamp in the contract’s rate-reading
functions, by comparing against current_time before accepting the value.
🤖 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 `@acbu_burning/src/lib.rs`:
- Around line 235-245: The BurnEvent timestamp is using the ledger time instead
of the oracle sample time, so update the BurnEvent construction in redeem_single
and redeem_basket to emit the rate’s source timestamp. Replace the current
timestamp assignment in the BurnEvent literal with rate_timestamp (the timestamp
returned alongside ORACLE_GET_RATE_WITH_TS) so indexers can match each
BurnEvent.rate to the exact oracle sample used.
- Around line 266-274: The empty-recipient path in this transfer flow still does
storage work before short-circuiting, and it currently panics instead of
returning the intended no-op. Update the handler around
reentrancy_guard::acquire_guard, Self::check_paused, and user.require_auth so
recipients.is_empty() is checked first and exits immediately with the no-op
result; only perform guard/pause/auth/storage work after that. Also replace the
ContractError::InvalidRecipient panic in this branch with the expected
successful no-op return.

In `@acbu_burning/tests/fuzz_test.rs`:
- Around line 47-51: Update the fuzz oracle in fuzz_test.rs so the
single-currency basket check treats num_recipients == 0 as a valid no-op, and
only asserts an error when num_recipients is non-zero and not exactly 1. Adjust
the expectation logic around the res result in the fuzz test to reflect this
rule while keeping the single-recipient success case unchanged.

In `@acbu_burning/tests/test.rs`:
- Around line 249-257: The empty-recipient redeem basket test is asserting the
wrong contract for try_redeem_basket on Burning. Update
test_redeem_basket_rejects_empty_recipients in test.rs so it expects the
zero-address Vec case to succeed as a no-op after the auth check, rather than
returning an error. Adjust the assertion to verify success and, if needed,
confirm no state changes for the empty basket path using the existing
setup_test, ctx.burning, and ctx.user helpers.

---

Outside diff comments:
In `@acbu_burning/src/lib.rs`:
- Around line 160-170: The freshness checks in the oracle lookup flow only
reject stale timestamps, so future-dated values can incorrectly pass; update the
timestamp validation around the oracle fetch logic in the rate retrieval path to
reject both old and future timestamps. Apply the same fix wherever the timestamp
freshness pattern is used, including the checks tied to oracle_timestamp and
rate_timestamp in the contract’s rate-reading functions, by comparing against
current_time before accepting the value.
🪄 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: e8694038-f695-4074-8560-7e059c051289

📥 Commits

Reviewing files that changed from the base of the PR and between dce688f and 319dd69.

📒 Files selected for processing (4)
  • acbu_burning/src/lib.rs
  • acbu_burning/tests/fuzz_test.rs
  • acbu_burning/tests/redeem_basket.rs
  • acbu_burning/tests/test.rs

Comment thread acbu_burning/src/lib.rs
Comment on lines 235 to 245
let burn_event = BurnEvent {
transaction_id: tx_id,
user: user.clone(),
acbu_amount, // gross amount burned (unchanged — was already correct)
net_acbu, // explicit post-fee net so indexer needs no arithmetic
acbu_amount,
net_acbu,
local_amount: stoken_out,
currency: currency.clone(),
fee, // total fee for this redemption
fee,
rate,
timestamp: env.ledger().timestamp(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Emit the oracle timestamp that belongs to the emitted rate.

BurnEvent.rate comes from ORACLE_GET_RATE_WITH_TS, but timestamp is currently the ledger timestamp. Use rate_timestamp so indexers can reconcile the exact rate sample.

Proposed fix
-            timestamp: env.ledger().timestamp(),
+            timestamp: rate_timestamp,

Apply this in both redeem_single and redeem_basket.

Also applies to: 448-458

🤖 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 `@acbu_burning/src/lib.rs` around lines 235 - 245, The BurnEvent timestamp is
using the ledger time instead of the oracle sample time, so update the BurnEvent
construction in redeem_single and redeem_basket to emit the rate’s source
timestamp. Replace the current timestamp assignment in the BurnEvent literal
with rate_timestamp (the timestamp returned alongside ORACLE_GET_RATE_WITH_TS)
so indexers can match each BurnEvent.rate to the exact oracle sample used.

Comment thread acbu_burning/src/lib.rs
Comment thread acbu_burning/tests/fuzz_test.rs Outdated
Comment thread acbu_burning/tests/test.rs Outdated
@temisan0x temisan0x closed this Jun 24, 2026
@temisan0x temisan0x force-pushed the fix/351-burn-basket-early-return branch from 319dd69 to 170d8e2 Compare June 24, 2026 11:49
@temisan0x temisan0x reopened this Jun 24, 2026
@Junman140 Junman140 merged commit ef7bf0c into Pi-Defi-world:dev Jun 25, 2026
1 of 3 checks passed
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.

burn_for_basket Vec::new allocates empty Vec for zero recipients

2 participants