Skip to content

feat: implemented Set TTL on refund and dispute persistent storage entries#159

Merged
anonfedora merged 3 commits intoanonfedora:masterfrom
Ugasutun:feature/refund-dispute-ttl
Apr 2, 2026
Merged

feat: implemented Set TTL on refund and dispute persistent storage entries#159
anonfedora merged 3 commits intoanonfedora:masterfrom
Ugasutun:feature/refund-dispute-ttl

Conversation

@Ugasutun
Copy link
Copy Markdown
Contributor

@Ugasutun Ugasutun commented Mar 27, 2026

Closes #150

PR Description: Set TTL on refund and dispute persistent storage entries

Overview

This PR adds Time-To-Live (TTL) management for refund and dispute storage entries in the EscrowManager contract to prevent ledger bloat and reduce storage costs.

Changes Made

1. EscrowStatus Enum Enhancement

  • Added Disputed = 3 status to track disputed escrows

2. TTL Configuration

  • Added DEFAULT_TTL_LEDGER_COUNT constant (518400 ledgers ≈ 30 days)

3. TTL Extension for Refund

  • Modified refund_escrow function to extend TTL after refund is processed
  • Storage entries for resolved refunds will be automatically pruned after expiry

4. New Function: dispute_escrow

  • Added new function to allow buyer, seller, or lender to raise a dispute
  • Sets escrow status to Disputed
  • Extends TTL for dispute entry (30 days)

5. New Function: renew_dispute_ttl

  • Added function to renew TTL for active disputes
  • Can be called by any party (buyer, seller, lender)
  • Allows extending TTL if dispute is still active after initial 30 days

6. Storage TTL Extension

  • All refund and dispute entries now have 30-day TTL
  • Entries are automatically pruned by the network after expiry

Definition of Done (DoD) Checklist

  • Used env.storage().persistent().extend_ttl() for all refund and dispute records
  • Set default TTL of 30 days (518400 ledgers)
  • Implemented mechanism to renew TTL if a dispute is still active
  • Storage entries for resolved refunds are automatically pruned by the network after expiry

Files Modified

  • contracts/escrow-manager/src/lib.rs

Breaking Changes

  • Added new dispute_escrow function to the contract interface
  • Added new renew_dispute_ttl function to the contract interface
  • Added Disputed status to EscrowStatus enum

Summary by CodeRabbit

  • Tests
    • Added test coverage for collateral verification error scenarios.

@Ugasutun Ugasutun requested a review from anonfedora as a code owner March 27, 2026 01:59
@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 27, 2026

@Ugasutun 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
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

A new unit test test_verify_collateral_not_found was added to the collateral registry library to verify the admin verify_collateral function's behavior when a collateral ID doesn't exist. The test uses a panic expectation to assert the correct error is raised.

Changes

Cohort / File(s) Summary
Unit Test Addition
contracts/collateral-registry/src/lib.rs
Added test case test_verify_collateral_not_found to verify the verify_collateral function returns an error when the collateral ID is not found.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

Suggested reviewers

  • anonfedora

Poem

🐰 A collateral not found, we test with care,
Admin paths verified, edge cases laid bare,
One little test, but it matters so much,
Error handling caught by a cottontail's touch! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title claims to implement TTL on refund and dispute storage, but the actual changes only add a test case for collateral verification, which is unrelated. Ensure the PR title accurately reflects the actual code changes. If TTL implementation exists elsewhere, verify it's included. Otherwise, update the title to match the test changes (e.g., 'test: add unit test for collateral not found verification').
Linked Issues check ⚠️ Warning Issue #150 requires TTL implementation for refund/dispute entries, but the changeset only adds a collateral verification test case with no TTL-related code changes present. Verify that TTL implementation code is actually present in the changeset. If absent, implement the required env.storage().persistent().extend_ttl() calls for refund and dispute records as specified in issue #150.
Out of Scope Changes check ⚠️ Warning The test addition for collateral verification is out of scope; the PR claims to implement TTL on refund/dispute storage but this change is for collateral registry testing. Remove the collateral verification test or provide separate PR. Focus this PR solely on implementing TTL for refund and dispute persistent storage entries as required by issue #150.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description is detailed and well-structured, covering overview, changes, files modified, and breaking changes per the template.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
contracts/escrow-manager/src/lib.rs (3)

500-504: Consider extending TTL for released escrows.

The refund_escrow function extends TTL for refunded entries (line 591-593), but release_funds_on_confirmation does not extend TTL for released entries. For consistency and to enable automatic pruning of all resolved escrow records, consider adding TTL extension here as well.

♻️ Proposed addition after line 501
         escrow.status = EscrowStatus::Released;
         env.storage().persistent().set(&escrow_id, &escrow);
+
+        // Extend TTL for released entry (30 days / 518400 ledgers)
+        env.storage()
+            .persistent()
+            .extend_ttl(&escrow_id, DEFAULT_TTL_LEDGER_COUNT, DEFAULT_TTL_LEDGER_COUNT);

         env.events()
             .publish((symbol_short!("esc_rel"),), (escrow_id,));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/escrow-manager/src/lib.rs` around lines 500 - 504, The
release_funds_on_confirmation path sets escrow.status = EscrowStatus::Released
and persists it but doesn't extend the TTL like refund_escrow does; update the
release_funds_on_confirmation logic to call the same TTL-extension helper or
storage API after env.storage().persistent().set(&escrow_id, &escrow) (or use
the same function used in refund_escrow) so released escrows get the persistent
TTL extension and can be pruned consistently.

664-667: Error code reuse may cause confusion.

EscrowNotActive is returned when the escrow status is not Disputed, but the escrow could actually be Active, Released, or Refunded. Reusing this error makes it harder for callers to distinguish between "escrow is active but not disputed" vs "escrow doesn't exist" vs "escrow was already resolved".

Consider adding a specific error variant like EscrowNotDisputed to the ContractError enum for clearer error semantics.

♻️ Proposed fix

Add to ContractError enum:

EscrowNotDisputed = 13,

Then update the check:

         // Only allow TTL renewal for active disputes
         if escrow.status != EscrowStatus::Disputed {
-            return Err(ContractError::EscrowNotActive);
+            return Err(ContractError::EscrowNotDisputed);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/escrow-manager/src/lib.rs` around lines 664 - 667, Add a new error
variant EscrowNotDisputed to the ContractError enum and update the TTL-renewal
check to return that new variant instead of EscrowNotActive; specifically, add
EscrowNotDisputed to the ContractError definition (matching the enum's encoding
style) and change the guard in the function that currently checks escrow.status
!= EscrowStatus::Disputed to return Err(ContractError::EscrowNotDisputed). Also
search for other uses of EscrowNotActive that currently represent the "not
disputed" case (and update tests/docs accordingly) to keep semantics clear.

614-683: Missing test coverage for dispute functions.

The new dispute_escrow and renew_dispute_ttl functions lack corresponding test cases. Critical scenarios to cover:

  • dispute_escrow success with each authorized party (buyer, seller, lender)
  • dispute_escrow rejection for unauthorized caller
  • dispute_escrow rejection when escrow is not Active
  • renew_dispute_ttl success for active dispute
  • renew_dispute_ttl rejection when escrow is not Disputed
  • renew_dispute_ttl rejection for unauthorized caller

Would you like me to generate the test cases for these scenarios?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/escrow-manager/src/lib.rs` around lines 614 - 683, Add unit tests
covering dispute_escrow and renew_dispute_ttl: create test cases that (1) call
dispute_escrow successfully as buyer, seller, and lender by setting up an escrow
with EscrowStatus::Active and appropriate buyer/seller/lender Addresses,
asserting Ok result, status becomes EscrowStatus::Disputed, event
(symbol_short!("esc_dsp")) is emitted and TTL was extended; (2) call
dispute_escrow with an unauthorized Address and assert it returns
ContractError::Unauthorized; (3) call dispute_escrow when escrow.status !=
EscrowStatus::Active and assert ContractError::EscrowNotActive; (4) call
renew_dispute_ttl successfully when escrow.status == EscrowStatus::Disputed from
buyer/seller/lender and assert Ok, event (symbol_short!("dsp_rnew")) emitted and
TTL extended; (5) call renew_dispute_ttl when escrow.status !=
EscrowStatus::Disputed and assert ContractError::EscrowNotActive; and (6) call
renew_dispute_ttl from an unauthorized Address and assert
ContractError::Unauthorized. Use the existing test harness to manipulate Env
storage, set Escrow entries by escrow_id, and verify events and TTL via
env.storage().persistent() and env.events(); reference functions dispute_escrow
and renew_dispute_ttl and enum variants EscrowStatus::Active and
EscrowStatus::Disputed when locating code to test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/collateral-registry/src/lib.rs`:
- Around line 711-741: The test currently calls env.mock_all_auths() so
admin.require_auth() always succeeds; replace that with selective mocking and
explicitly invoke verify_collateral as the unauthorized caller: remove
env.mock_all_auths(), call env.mock_auths(...) to only authorize the admin
(e.g., mock_auths with admin allowed and leave unauthorized unmocked), then set
the test caller to the unauthorized address (use env.set_caller or the test
harness method you use to change the caller) before calling
CollateralRegistry::verify_collateral in test_verify_collateral_unauthorized so
the admin.require_auth() check fails and the call returns
Err(ContractError::Unauthorized).
- Line 390: The event symbol passed to symbol_short! is too long
("coll_verified"); update the call in the env.events().publish invocation to use
a symbol of at most 9 characters (e.g., "coll_vrfy") so it satisfies
symbol_short! constraints; locate the publish call that uses symbol_short!
(env.events().publish((symbol_short!("coll_verified"),), (id,));) and replace
the symbol string with a shortened, consistent token such as "coll_vrfy".

In `@contracts/escrow-manager/src/lib.rs`:
- Around line 628-633: Add a dispute-resolution entrypoint that allows an
authorized actor to transition escrows from EscrowStatus::Disputed to a final
state (e.g., EscrowStatus::Cancelled, Released, or Refunded) and perform the
necessary token/account transfers and storage updates; implement a new function
(e.g., resolve_dispute) which validates caller authorization, loads the escrow
by escrow_id, checks escrow.status == EscrowStatus::Disputed, sets escrow.status
to the chosen final status, executes the release/refund logic tied to the Escrow
struct (refund_to, release_to, or similar fields), and persists the updated
escrow with env.storage().persistent().set(&escrow_id, &escrow); ensure you
reuse existing helpers for transfers and permission checks and add tests
asserting transitions from Disputed to the allowed final states and that funds
are moved accordingly.
- Around line 19-24: Update the EscrowStatus enum to align discriminant values
with the rest of the codebase: add missing Pending and Cancelled variants and
set numeric values to Pending=0, Active=1, Released=2, Cancelled=3, Disputed=4
in the EscrowStatus enum; if the previous Refunded variant must be preserved for
backward compatibility, make Refunded an alias or implement a conversion mapping
so Refunded maps to Cancelled (same discriminant) and update any
serialize/deserialize logic that references EscrowStatus accordingly.

---

Nitpick comments:
In `@contracts/escrow-manager/src/lib.rs`:
- Around line 500-504: The release_funds_on_confirmation path sets escrow.status
= EscrowStatus::Released and persists it but doesn't extend the TTL like
refund_escrow does; update the release_funds_on_confirmation logic to call the
same TTL-extension helper or storage API after
env.storage().persistent().set(&escrow_id, &escrow) (or use the same function
used in refund_escrow) so released escrows get the persistent TTL extension and
can be pruned consistently.
- Around line 664-667: Add a new error variant EscrowNotDisputed to the
ContractError enum and update the TTL-renewal check to return that new variant
instead of EscrowNotActive; specifically, add EscrowNotDisputed to the
ContractError definition (matching the enum's encoding style) and change the
guard in the function that currently checks escrow.status !=
EscrowStatus::Disputed to return Err(ContractError::EscrowNotDisputed). Also
search for other uses of EscrowNotActive that currently represent the "not
disputed" case (and update tests/docs accordingly) to keep semantics clear.
- Around line 614-683: Add unit tests covering dispute_escrow and
renew_dispute_ttl: create test cases that (1) call dispute_escrow successfully
as buyer, seller, and lender by setting up an escrow with EscrowStatus::Active
and appropriate buyer/seller/lender Addresses, asserting Ok result, status
becomes EscrowStatus::Disputed, event (symbol_short!("esc_dsp")) is emitted and
TTL was extended; (2) call dispute_escrow with an unauthorized Address and
assert it returns ContractError::Unauthorized; (3) call dispute_escrow when
escrow.status != EscrowStatus::Active and assert ContractError::EscrowNotActive;
(4) call renew_dispute_ttl successfully when escrow.status ==
EscrowStatus::Disputed from buyer/seller/lender and assert Ok, event
(symbol_short!("dsp_rnew")) emitted and TTL extended; (5) call renew_dispute_ttl
when escrow.status != EscrowStatus::Disputed and assert
ContractError::EscrowNotActive; and (6) call renew_dispute_ttl from an
unauthorized Address and assert ContractError::Unauthorized. Use the existing
test harness to manipulate Env storage, set Escrow entries by escrow_id, and
verify events and TTL via env.storage().persistent() and env.events(); reference
functions dispute_escrow and renew_dispute_ttl and enum variants
EscrowStatus::Active and EscrowStatus::Disputed when locating code to test.
🪄 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: 05829196-8387-4294-9b98-1774f2521d38

📥 Commits

Reviewing files that changed from the base of the PR and between 23d41e7 and d4aab2a.

📒 Files selected for processing (2)
  • contracts/collateral-registry/src/lib.rs
  • contracts/escrow-manager/src/lib.rs

@anonfedora
Copy link
Copy Markdown
Owner

Fix failing CI and fix merge conflicts, please @Ugasutun

@Ugasutun
Copy link
Copy Markdown
Contributor Author

Ugasutun commented Apr 2, 2026

@anonfedora merge

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
contracts/collateral-registry/src/lib.rs (2)

764-835: ⚠️ Potential issue | 🔴 Critical

Critical: Malformed test function with unclosed delimiter.

The test_verify_collateral_unauthorized function starting at line 765 is never properly closed. At line 771, a new function test_propose_admin is defined inside it, creating an invalid nested function. This causes the Rust parse error reported by CI.

The test function body from lines 812-831 appears to be remnants of multiple tests merged incorrectly:

  • Code referencing owner variable (lines 821, 823) that's declared at line 770
  • Code referencing new_admin (line 833) that's declared at line 775
  • Mixed logic from test_verify_collateral_unauthorized and test_propose_admin_unauthorized

This entire section needs to be restructured with proper function boundaries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/collateral-registry/src/lib.rs` around lines 764 - 835, The test
block is malformed: close the fn test_verify_collateral_unauthorized() body
before defining fn test_propose_admin() so you don't nest functions; split the
merged logic into three separate tests (test_verify_collateral_unauthorized,
test_propose_admin, and test_propose_admin_unauthorized), ensuring each has its
own env/mock setup and local variables (declare owner in the verify test,
new_admin in the propose tests) and move calls like
CollateralRegistry::register_collateral, ::verify_collateral, and
::propose_admin into the appropriate test functions so variables (owner,
new_admin, collateral_id) are defined in the same function where they are used
and no function bodies overlap.

837-857: ⚠️ Potential issue | 🔴 Critical

Critical: Two test functions merged together causing parse error.

The changed line 838 (fn test_verify_collateral_not_found()) was inserted incorrectly, creating invalid syntax where two test functions are interleaved:

    #[test]
    fn test_verify_collateral_not_found() {  // <-- starts here
    #[should_panic(expected = "HostError: Error(Contract, `#8`)")]  // <-- attribute for next fn
    fn test_accept_admin_no_pending() {  // <-- another fn starts

The function test_verify_collateral_not_found has no closing brace before the next test's attribute, and the body content (lines 841-857) is shared/mixed between both functions.

🐛 Proposed fix to separate the test functions
     #[test]
     fn test_verify_collateral_not_found() {
+        let env = Env::default();
+        env.mock_all_auths();
+        let admin = Address::generate(&env);
+        let contract_id = env.register_contract(None, CollateralRegistry);
+
+        env.as_contract(&contract_id, || {
+            CollateralRegistry::initialize(env.clone(), admin).unwrap();
+
+            // Try to verify non-existent collateral
+            let result = CollateralRegistry::verify_collateral(env.clone(), 999);
+            assert_eq!(result, Err(ContractError::CollateralNotFound));
+        });
+    }
+
+    #[test]
     #[should_panic(expected = "HostError: Error(Contract, `#8`)")]
     fn test_accept_admin_no_pending() {
         let env = Env::default();
         env.mock_all_auths();
         let admin = Address::generate(&env);
         let contract_id = env.register_contract(None, CollateralRegistry);
-
-        env.as_contract(&contract_id, || {
-            CollateralRegistry::initialize(env.clone(), admin).unwrap();
-
-            // Try to verify non-existent collateral
-            let result = CollateralRegistry::verify_collateral(env.clone(), 999);
-            assert_eq!(result, Err(ContractError::CollateralNotFound));
-        });
         let client = CollateralRegistryClient::new(&env, &contract_id);
 
         client.initialize(&admin);
         client.accept_admin();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/collateral-registry/src/lib.rs` around lines 837 - 857, The file
accidentally merged two test functions; split the interleaved tests by closing
fn test_verify_collateral_not_found() before the next test attribute and put the
#[should_panic(...)] attribute immediately above fn
test_accept_admin_no_pending(). Ensure test_verify_collateral_not_found contains
only the verify_collateral-related setup/call (calling
CollateralRegistry::initialize and CollateralRegistry::verify_collateral and
asserting Err(ContractError::CollateralNotFound)), and
test_accept_admin_no_pending contains the client initialization and
client.accept_admin() with the #[should_panic(expected = "HostError:
Error(Contract, `#8`)")] attribute; verify function and client names
(CollateralRegistry::verify_collateral, CollateralRegistry::initialize,
CollateralRegistryClient::new, client.accept_admin) are used to locate and
separate the blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@contracts/collateral-registry/src/lib.rs`:
- Around line 764-835: The test block is malformed: close the fn
test_verify_collateral_unauthorized() body before defining fn
test_propose_admin() so you don't nest functions; split the merged logic into
three separate tests (test_verify_collateral_unauthorized, test_propose_admin,
and test_propose_admin_unauthorized), ensuring each has its own env/mock setup
and local variables (declare owner in the verify test, new_admin in the propose
tests) and move calls like CollateralRegistry::register_collateral,
::verify_collateral, and ::propose_admin into the appropriate test functions so
variables (owner, new_admin, collateral_id) are defined in the same function
where they are used and no function bodies overlap.
- Around line 837-857: The file accidentally merged two test functions; split
the interleaved tests by closing fn test_verify_collateral_not_found() before
the next test attribute and put the #[should_panic(...)] attribute immediately
above fn test_accept_admin_no_pending(). Ensure test_verify_collateral_not_found
contains only the verify_collateral-related setup/call (calling
CollateralRegistry::initialize and CollateralRegistry::verify_collateral and
asserting Err(ContractError::CollateralNotFound)), and
test_accept_admin_no_pending contains the client initialization and
client.accept_admin() with the #[should_panic(expected = "HostError:
Error(Contract, `#8`)")] attribute; verify function and client names
(CollateralRegistry::verify_collateral, CollateralRegistry::initialize,
CollateralRegistryClient::new, client.accept_admin) are used to locate and
separate the blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a8a162f-ece9-45a9-a878-42e8a20f1149

📥 Commits

Reviewing files that changed from the base of the PR and between d4aab2a and 5bec2d4.

📒 Files selected for processing (1)
  • contracts/collateral-registry/src/lib.rs

Copy link
Copy Markdown
Owner

@anonfedora anonfedora left a comment

Choose a reason for hiding this comment

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

Please, go to drips and submit a ticket so you'll be rewarded with points

@anonfedora anonfedora merged commit e774fd3 into anonfedora:master Apr 2, 2026
4 of 7 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.

[RefundManager] Set TTL on refund and dispute persistent storage entries**

2 participants