Skip to content

fix(bridge): add mint and burn replay protection (#6) - #63

Merged
Junman140 merged 1 commit into
Pi-Defi-world:mainfrom
charlesedeh021-cell:codex/fix-replay-protection-issue-6
Jul 30, 2026
Merged

fix(bridge): add mint and burn replay protection (#6)#63
Junman140 merged 1 commit into
Pi-Defi-world:mainfrom
charlesedeh021-cell:codex/fix-replay-protection-issue-6

Conversation

@charlesedeh021-cell

@charlesedeh021-cell charlesedeh021-cell commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes #6

Summary

This PR adds replay protection to the bridge flow so a Pi deposit cannot be minted twice on-chain, and a burn/redemption cannot be released twice off-chain.

Changes

  • Replaced the generic mint path with mint_from_deposit(...) as the bridge mint entrypoint.
  • Kept deposit idempotency on-chain by storing processed deposit IDs in contract state and rejecting reuse with DepositAlreadyProcessed.
  • Added explicit redemption replay protection for burns by storing processed redemption IDs in contract state and rejecting reuse with RedemptionAlreadyProcessed.
  • Updated the RedemptionBurned event to carry the on-chain redemption ID alongside the nonce.
  • Updated the relayer to treat the contract-supplied redemption ID as the dedupe key instead of the RPC event ID.
  • Updated relayer types, event parsing, and redemption watcher tests to match the new contract event shape.
  • Updated the on-chain reserve oracle design doc so it reflects the deposit-scoped mint flow.

Why

The previous bridge flow did not fully bind mint and burn actions to unique on-chain identifiers. That left room for double-processing if the relayer retried or replayed the same action. This change makes both directions explicit and idempotent at the contract boundary.

Testing

  • cargo test -p wpi-token --target-dir C:\tmp\cargo-target

Notes

  • The contract tests now cover:
    • deposit replay rejection
    • redemption replay rejection
    • existing mint/burn volume-limit behavior
    • role-separation checks
  • The relayer changes were updated in code and tests to align with the new redemption ID flow.

Summary by CodeRabbit

  • New Features

    • Added redemption ID tracking to prevent duplicate redemption processing.
    • Redemption burn events now include the associated redemption ID.
    • Deposit-based minting now requires a unique deposit ID.
  • Bug Fixes

    • Improved relayer deduplication by consistently using on-chain redemption IDs.
  • Documentation

    • Updated reserve-oracle and relayer documentation to reflect deposit- and redemption-based processing.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The WPI token contract now requires redemption IDs for burns, records processed redemptions, and emits IDs in burn events. The generic mint entrypoint is removed. Relayer parsing, persistence, tests, and documentation now use redemption IDs for deduplication.

Changes

Redemption replay protection

Layer / File(s) Summary
Contract redemption identity and replay handling
Stellar-contracts-v1/wpi-token/src/lib.rs, docs/design/on-chain-reserve-oracle.md
Adds persistent redemption tracking and duplicate-redemption errors, changes burn to require an ID, includes it in events, removes generic mint, and documents mint_from_deposit.
Contract API migration and replay tests
Stellar-contracts-v1/wpi-token/src/test.rs
Updates mint and burn calls to supply IDs, adds duplicate-redemption coverage, and updates randomized operation tests.
Relayer redemption event parsing and deduplication
relayer/src/stellar/*, relayer/src/types.ts, relayer/test/redemptionWatcher.test.ts
Extracts redemption IDs from event topics and uses them as the relayer’s persistence and deduplication keys.

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

Sequence Diagram(s)

sequenceDiagram
  participant SorobanWpiContractClient
  participant WpiToken
  participant RedemptionWatcher
  participant RedemptionStore
  SorobanWpiContractClient->>WpiToken: Read redemption_burned event
  WpiToken-->>SorobanWpiContractClient: redemptionId from topic and nonce from value
  SorobanWpiContractClient-->>RedemptionWatcher: BurnEvent with redemptionId
  RedemptionWatcher->>RedemptionStore: Check redemptionId
  RedemptionWatcher->>RedemptionStore: Upsert unseen redemption
Loading

Possibly related PRs

  • Pi-Defi-world/Wpi#40: Overlaps in the shared token contract’s burn flow and redemption event wiring.
  • Pi-Defi-world/Wpi#46: Directly overlaps the relayer redemption watcher and related burn-event handling.
  • Pi-Defi-world/Wpi#52: Overlaps the token contract’s burn entrypoint and redemption-related surface.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: bridge replay protection for mint and burn paths.
Linked Issues check ✅ Passed The PR adds deposit-scoped minting, redemption replay protection, relayer dedupe by redemption ID, and a double-relay test.
Out of Scope Changes check ✅ Passed The changes stay focused on bridge replay protection, relayer updates, tests, and the related design doc.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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: 2

🤖 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 `@docs/design/on-chain-reserve-oracle.md`:
- Around line 98-103: Update the documented mint_from_deposit API to remove the
admin parameter, use the contract argument order (to, amount, pi_deposit_id),
and return Result<bool, Error>. Ensure the documentation states that callers
must handle Ok(false) for volume-limit rejections.

In `@relayer/src/stellar/redemptionWatcher.ts`:
- Around line 29-31: Update the redemption deduplication flow around
hasRedemption and upsertRedemption to remain compatible with records keyed by
the legacy RPC eventId. Before processing a replay, migrate the existing record
to redemptionId or perform a legacy eventId lookup and treat it as already
processed; ensure the migrated/compatibility path prevents duplicate redemption
release while preserving the new redemptionId key for future records.
🪄 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 Plus

Run ID: 834921a6-8d03-49cc-a8a8-b22445a31b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 8f309d4 and acbb57d.

📒 Files selected for processing (8)
  • Stellar-contracts-v1/wpi-token/src/lib.rs
  • Stellar-contracts-v1/wpi-token/src/test.rs
  • docs/design/on-chain-reserve-oracle.md
  • relayer/src/stellar/redemptionWatcher.ts
  • relayer/src/stellar/sorobanWpiContractClient.ts
  • relayer/src/stellar/wpiContractClient.ts
  • relayer/src/types.ts
  • relayer/test/redemptionWatcher.test.ts

Comment on lines +98 to +103
fn mint_from_deposit(
admin: Address,
to: Address,
amount: i128,
pi_deposit_id: BytesN<32>,
) -> Result<(), Error> {

Copy link
Copy Markdown

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

Match the documented mint API to the contract.

mint_from_deposit is minter-authorized, takes (to, amount, pi_deposit_id), and returns Result<bool, Error> so callers handle Ok(false) volume-limit rejections. The documented admin parameter and Result<(), Error> will mislead the follow-up implementation.

Proposed fix
 fn mint_from_deposit(
-    admin: Address,
     to: Address,
     amount: i128,
     pi_deposit_id: BytesN<32>,
-) -> Result<(), Error> {
-    // existing admin auth...
+) -> Result<bool, Error> {
+    // existing minter auth...
📝 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
fn mint_from_deposit(
admin: Address,
to: Address,
amount: i128,
pi_deposit_id: BytesN<32>,
) -> Result<(), Error> {
fn mint_from_deposit(
to: Address,
amount: i128,
pi_deposit_id: BytesN<32>,
) -> Result<bool, Error> {
🤖 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 `@docs/design/on-chain-reserve-oracle.md` around lines 98 - 103, Update the
documented mint_from_deposit API to remove the admin parameter, use the contract
argument order (to, amount, pi_deposit_id), and return Result<bool, Error>.
Ensure the documentation states that callers must handle Ok(false) for
volume-limit rejections.

Comment on lines +29 to +31
if (this.store.hasRedemption(event.redemptionId)) continue;
this.store.upsertRedemption({
redemptionId: event.eventId,
redemptionId: event.redemptionId,

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 | 🏗️ Heavy lift

Migrate legacy event-ID records before switching the dedupe key.

Records persisted by the previous watcher are keyed by RPC eventId. If an already-processed event is replayed after rollout, hasRedemption(event.redemptionId) misses that record, requeues it, and can release Pi twice. Migrate existing records or perform a legacy-key lookup during the transition.

#!/bin/bash
# Map the idempotency-store surface before inspecting persistence-key usage.
ast-grep outline relayer/src --items all --type class,interface,function --match 'Idempotency|Store'

# Locate the concrete storage implementation and any migration/compatibility logic.
rg -n -C 4 --glob '*.ts' \
  'hasRedemption\s*\(|upsertRedemption\s*\(|listRedemptionsByStatus\s*\(|eventId|redemptionId' \
  relayer/src
🤖 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 `@relayer/src/stellar/redemptionWatcher.ts` around lines 29 - 31, Update the
redemption deduplication flow around hasRedemption and upsertRedemption to
remain compatible with records keyed by the legacy RPC eventId. Before
processing a replay, migrate the existing record to redemptionId or perform a
legacy eventId lookup and treat it as already processed; ensure the
migrated/compatibility path prevents duplicate redemption release while
preserving the new redemptionId key for future records.

@Junman140
Junman140 merged commit 926cae1 into Pi-Defi-world:main Jul 30, 2026
2 of 3 checks passed
This was referenced Jul 30, 2026
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.

Issue 6 — No replay protection tying mint to a specific Pi-chain deposit Labels: security, bridge, priority:critical

2 participants