Skip to content

Feature/revenue service fee#50

Open
clintjeff2 wants to merge 11 commits intoLightForgeHub:mainfrom
clintjeff2:feature/revenue-service-fee
Open

Feature/revenue service fee#50
clintjeff2 wants to merge 11 commits intoLightForgeHub:mainfrom
clintjeff2:feature/revenue-service-fee

Conversation

@clintjeff2
Copy link
Copy Markdown

@clintjeff2 clintjeff2 commented Mar 25, 2026

feat(vault): implement platform revenue service fee (#31)

Summary

Extracts a configurable percentage-based fee (in Basis Points, 10000 = 100%) from the expert's gross pay on every successfully finalized session, routing it to a designated treasury address. The user's refund is unaffected.


Proof of Successful Build & Tests

Screenshot from 2026-03-25 04-03-32 Screenshot from 2026-03-25 04-09-09

Changes

src/error.rs

  • Added FeeTooHigh = 11 — returned when set_fee is called with a value above 2000 BPS (20%).

src/storage.rs

  • Added DataKey::FeeBps and DataKey::Treasury variants to the DataKey enum.
  • Added set_fee_bps / get_fee_bps (instance storage, defaults to 0).
  • Added set_treasury / get_treasury (instance storage).

src/events.rs

  • Updated session_finalized signature to include fee_amount: i128 as a third payload field, giving off-chain indexers full visibility into fee extraction.

src/contract.rs

  • set_fee(env, new_fee_bps) — Admin-only. Rejects values above 2000 BPS.
  • set_treasury(env, treasury) — Admin-only. Stores the treasury address.
  • finalize_session — Updated payment calculation:
    fee_amount    = (gross_expert_pay × fee_bps) / 10_000
    expert_net    = gross_expert_pay − fee_amount
    refund        = total_deposit − gross_expert_pay   (unchanged)
    
    Transfers fee_amount → treasury, expert_net → expert, refund → user.

src/lib.rs

  • Exposed pub fn set_fee(env: Env, new_fee_bps: u32) -> Result<(), VaultError>.
  • Exposed pub fn set_treasury(env: Env, treasury: Address) -> Result<(), VaultError>.

src/test.rs

  • test_set_fee_and_treasury — verifies admin can set both values.
  • test_fee_cap_at_2000_bps — 2000 BPS accepted, 2001 rejected.
  • test_finalize_with_10_percent_fee — full session: treasury=100, expert=900, user unchanged.
  • test_finalize_with_fee_and_partial_refund — partial session: treasury=50, expert=450, user refunded 500.
  • test_finalize_zero_fee_behaves_as_before — no regression when fee is not set.

Acceptance Criteria

  • Contract correctly calculates fractional fees using BPS (fee = gross × bps / 10000).
  • Admin cannot raise fee above 20% (2000 BPS) — returns FeeTooHigh.
  • Fee is deducted from expert's gross pay, not the user's refund.
  • All 33 tests pass with zero failures.

Closes

Closes #31

Summary by CodeRabbit

  • New Features

    • Configurable platform fee (max 20%) with admin controls to set fee rate and treasury address
    • Fees are deducted from expert payouts and routed to the configured treasury when set; refunds adjusted accordingly
    • Session finalization now reports expert net pay and fee amount
  • Tests

    • Added tests for fee configuration, cap enforcement, and settlement scenarios (fee split, partial refund, zero-fee)

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds admin-configurable platform fee (bps, capped at 2000) and optional treasury. finalize_session now computes gross expert pay, derives fee_amount, pays treasury (if set and fee>0), pays expert net, refunds user, and emits session_finalized with expert_net_pay and fee_amount.

Changes

Cohort / File(s) Summary
Core contract logic
contracts/payment-vault-contract/src/contract.rs
Added admin entrypoints set_fee and set_treasury; changed finalize_session to compute gross_expert_pay, fee_amount, expert_net_pay; transfer fee to optional treasury (if >0), transfer net to expert, refund user; updated emitted event args.
Public interface
contracts/payment-vault-contract/src/lib.rs
Exposed pub fn set_fee and pub fn set_treasury contractimpls delegating to internal implementations.
Storage
contracts/payment-vault-contract/src/storage.rs
Added DataKey::FeeBps and DataKey::Treasury plus set_fee_bps/get_fee_bps (defaults 0) and set_treasury/get_treasury (Option).
Events
contracts/payment-vault-contract/src/events.rs
Changed session_finalized signature to include expert_pay and fee_amount and updated published payload to (actual_duration, expert_pay, fee_amount).
Errors
contracts/payment-vault-contract/src/error.rs
Added FeeTooHigh = 13 to VaultError. Note: this creates a duplicate discriminant with existing BookingNotDisputed = 13.
Tests
contracts/payment-vault-contract/src/test.rs
Added “Platform Fee Tests” validating admin fee/treasury setup, fee cap enforcement (accept 2000 bps, reject >2000), and multiple finalize_session scenarios (10% fee payouts, partial duration refund, zero-fee fallback). Reworked some key-rotation/oracle tests accordingly.

Sequence Diagram

sequenceDiagram
    actor Admin as Admin
    participant Contract
    participant Storage
    participant Treasury
    participant Expert
    participant User

    rect rgba(100,149,237,0.5)
    note right of Admin: Fee Configuration Phase
    Admin->>Contract: set_fee(env, new_fee_bps)
    activate Contract
    Contract->>Contract: require_auth(admin)
    Contract->>Storage: set_fee_bps(fee_bps)
    deactivate Contract

    Admin->>Contract: set_treasury(env, treasury)
    activate Contract
    Contract->>Contract: require_auth(admin)
    Contract->>Storage: set_treasury(treasury_address)
    deactivate Contract
    end

    rect rgba(144,238,144,0.5)
    note right of Admin: Session Finalization Phase
    Admin->>Contract: finalize_session(booking_id, ...)
    activate Contract
    Contract->>Storage: get_fee_bps()
    Contract->>Contract: compute gross_expert_pay = rate_per_second * actual_duration
    Contract->>Contract: fee_amount = gross_expert_pay * fee_bps / 10000
    Contract->>Contract: expert_net_pay = gross_expert_pay - fee_amount
    Contract->>Storage: get_treasury()
    alt treasury exists & fee_amount > 0
    Contract->>Treasury: transfer(fee_amount)
    end
    Contract->>Expert: transfer(expert_net_pay)
    Contract->>User: transfer(refund)
    Contract->>Contract: emit session_finalized(booking_id, actual_duration, expert_net_pay, fee_amount)
    deactivate Contract
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through code and found a fee,

Admin sets the bps—keeps it tame at twenty,
Treasury nibbles crumbs, expert keeps the rest,
User gets their change, events sing the test,
I wiggle my nose — payments balanced, no mess.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive Title is vague and generic; it uses 'Feature/revenue service fee' which lacks specificity and doesn't clearly communicate the primary change (percentage-based platform fee implementation). Revise title to be more specific and descriptive, e.g., 'Implement configurable platform service fee extraction from expert pay' or 'Add basis-point platform revenue fee mechanism'.
Out of Scope Changes check ❓ Inconclusive Changes align entirely with issue #31 scope; however, one concern exists: error.rs assigns FeeTooHigh=13 but the linked issue specifies FeeTooHigh=11, creating a discrepancy that may indicate unintended deviation or documentation error. Verify whether FeeTooHigh should be 11 or 13; if 13 was intentional to avoid conflicts with existing error codes, update the linked issue documentation or add a code comment explaining the deviation.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed Description is comprehensive and well-structured, covering changes across all modified files, test evidence, and acceptance criteria verification with build/test screenshots.
Linked Issues check ✅ Passed All coding requirements from issue #31 are met: error variants, storage keys/accessors, admin-authenticated fee/treasury setters with 2000 BPS cap, finalize_session fee calculation and routing, contract entrypoints exposed, event signature updated, and comprehensive test coverage with all 33 tests passing.

✏️ 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: 1

🧹 Nitpick comments (4)
contracts/payment-vault-contract/src/lib.rs (1)

42-48: Add doc comments for consistency with other entrypoints.

Other admin functions like pause and unpause have documentation comments. Consider adding similar comments for these new entrypoints to maintain consistency.

📝 Suggested documentation
+    /// Set the platform fee in basis points (Admin-only)
+    /// Maximum allowed fee is 2000 BPS (20%)
     pub fn set_fee(env: Env, new_fee_bps: u32) -> Result<(), VaultError> {
         contract::set_fee(&env, new_fee_bps)
     }

+    /// Set the treasury address for receiving platform fees (Admin-only)
     pub fn set_treasury(env: Env, treasury: Address) -> Result<(), VaultError> {
         contract::set_treasury(&env, &treasury)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/lib.rs` around lines 42 - 48, Add
consistent Rust doc comments above the public entrypoints set_fee and
set_treasury to match the style used for pause/unpause: describe purpose,
parameters (env, new_fee_bps or treasury), expected behavior, and returned
Result<(), VaultError>. Update the docstrings for the functions named set_fee
and set_treasury so tooling and generated docs show the same level of detail as
other admin entrypoints.
contracts/payment-vault-contract/src/error.rs (1)

16-16: Minor: Gap in error code numbering.

The error codes skip from ExpertRateNotSet = 9 to FeeTooHigh = 11, leaving value 10 unused. This doesn't affect functionality but may cause confusion if error codes are used for debugging or monitoring.

💡 Suggested fix
-    FeeTooHigh = 11,
+    FeeTooHigh = 10,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/error.rs` at line 16, The enum error
codes have a gap between ExpertRateNotSet = 9 and FeeTooHigh = 11; update the
variant values so they are contiguous—either change FeeTooHigh to = 10 or insert
a placeholder/reserved variant for = 10 to document the unused code; locate the
enum with ExpertRateNotSet and FeeTooHigh in error.rs and make the numeric
adjustment so error codes are sequential and self-explanatory.
contracts/payment-vault-contract/src/test.rs (2)

741-780: Consider adding test for fee without treasury configured.

The current tests always set both fee and treasury together. However, in finalize_session, when fee_amount > 0 but no treasury is set, the fee is silently not transferred (lines 174-178 in contract.rs). This could lead to funds being "locked" in the contract.

Consider adding a test that verifies behavior when:

  1. Fee is configured (e.g., 10%)
  2. Treasury is NOT set
  3. Session is finalized

This would help document the expected behavior and catch any unintended changes.

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

In `@contracts/payment-vault-contract/src/test.rs` around lines 741 - 780, Add a
unit test that configures a non-zero fee via set_fee but does NOT call
set_treasury, then creates a booking (use set_my_rate, book_session, mint tokens
as in existing tests) and calls finalize_session; assert expected balances for
user, expert, contract (and treasury remains zero) to document current behavior
when fee_amount > 0 but treasury is None; reference helper functions used in
other tests (create_token_contract, create_client, set_my_rate, book_session,
finalize_session) and mirror the structure of test_finalize_with_10_percent_fee
while omitting the call to set_treasury so the test fails if fee handling or
funds locking changes.

700-720: Tests could be strengthened with value verification.

The test only verifies that try_set_fee and try_set_treasury return Ok, but doesn't verify that the values were actually stored correctly. Consider adding getter functions or verifying the effect through finalize_session behavior.

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

In `@contracts/payment-vault-contract/src/test.rs` around lines 700 - 720, The
test test_set_fee_and_treasury only asserts that client.try_set_fee and
client.try_set_treasury return Ok; add assertions that the values were actually
stored by reading them back (e.g., call a view/getter such as client.get_fee()
and client.get_treasury() or the contract's public query methods) and assert
equality with 1000_u32 and the treasury Address; if no getters exist, add
minimal view methods to the contract or verify via finalize_session/other
behavior that depends on those values to assert they took effect. Ensure you
reference the same client instance used in the test and keep the added
assertions immediately after each try_set_* call.
🤖 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/payment-vault-contract/src/contract.rs`:
- Around line 174-178: The code currently deducts fee_amount but only transfers
it when storage::get_treasury(env) returns Some, causing tokens to remain in the
contract if treasury is None; update the payment flow in the function that
computes and applies fee (where fee_amount, contract_address and
token_client.transfer are used) to handle the None case: if
storage::get_treasury(env) is None then treat fee_amount as zero (i.e., do not
deduct from the expert payout and do not call token_client.transfer), otherwise
proceed with the transfer to the treasury; alternatively (if you prefer stricter
behavior) enforce in set_fee that a treasury must be configured before allowing
non-zero fees by validating in set_fee and returning an error when treasury is
missing.

---

Nitpick comments:
In `@contracts/payment-vault-contract/src/error.rs`:
- Line 16: The enum error codes have a gap between ExpertRateNotSet = 9 and
FeeTooHigh = 11; update the variant values so they are contiguous—either change
FeeTooHigh to = 10 or insert a placeholder/reserved variant for = 10 to document
the unused code; locate the enum with ExpertRateNotSet and FeeTooHigh in
error.rs and make the numeric adjustment so error codes are sequential and
self-explanatory.

In `@contracts/payment-vault-contract/src/lib.rs`:
- Around line 42-48: Add consistent Rust doc comments above the public
entrypoints set_fee and set_treasury to match the style used for pause/unpause:
describe purpose, parameters (env, new_fee_bps or treasury), expected behavior,
and returned Result<(), VaultError>. Update the docstrings for the functions
named set_fee and set_treasury so tooling and generated docs show the same level
of detail as other admin entrypoints.

In `@contracts/payment-vault-contract/src/test.rs`:
- Around line 741-780: Add a unit test that configures a non-zero fee via
set_fee but does NOT call set_treasury, then creates a booking (use set_my_rate,
book_session, mint tokens as in existing tests) and calls finalize_session;
assert expected balances for user, expert, contract (and treasury remains zero)
to document current behavior when fee_amount > 0 but treasury is None; reference
helper functions used in other tests (create_token_contract, create_client,
set_my_rate, book_session, finalize_session) and mirror the structure of
test_finalize_with_10_percent_fee while omitting the call to set_treasury so the
test fails if fee handling or funds locking changes.
- Around line 700-720: The test test_set_fee_and_treasury only asserts that
client.try_set_fee and client.try_set_treasury return Ok; add assertions that
the values were actually stored by reading them back (e.g., call a view/getter
such as client.get_fee() and client.get_treasury() or the contract's public
query methods) and assert equality with 1000_u32 and the treasury Address; if no
getters exist, add minimal view methods to the contract or verify via
finalize_session/other behavior that depends on those values to assert they took
effect. Ensure you reference the same client instance used in the test and keep
the added assertions immediately after each try_set_* call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1c96142-aa9e-4fa9-960d-0ebe3b318a05

📥 Commits

Reviewing files that changed from the base of the PR and between 01a3a5c and 0df5476.

📒 Files selected for processing (6)
  • contracts/payment-vault-contract/src/contract.rs
  • contracts/payment-vault-contract/src/error.rs
  • contracts/payment-vault-contract/src/events.rs
  • contracts/payment-vault-contract/src/lib.rs
  • contracts/payment-vault-contract/src/storage.rs
  • contracts/payment-vault-contract/src/test.rs

Comment on lines +174 to +178
if fee_amount > 0 {
if let Some(treasury) = storage::get_treasury(env) {
token_client.transfer(&contract_address, &treasury, &fee_amount);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential issue: Fee amount may be silently retained in contract when treasury is not set.

When fee_amount > 0 but get_treasury() returns None, the fee is deducted from the expert's pay but never transferred out. These tokens will remain locked in the contract with no mechanism to retrieve them.

Consider one of these approaches:

  1. Skip fee deduction if treasury is not set (treat as 0% fee)
  2. Require treasury to be set before allowing non-zero fee
  3. Fail the transaction if fee would be collected but treasury is missing
🔧 Option 1: Skip fee if no treasury (recommended)
     let fee_bps = storage::get_fee_bps(env);
-    let fee_amount = (gross_expert_pay * fee_bps as i128) / 10_000;
-    let expert_net_pay = gross_expert_pay - fee_amount;
+    let treasury_opt = storage::get_treasury(env);
+    let fee_amount = if treasury_opt.is_some() {
+        (gross_expert_pay * fee_bps as i128) / 10_000
+    } else {
+        0
+    };
+    let expert_net_pay = gross_expert_pay - fee_amount;
 
     // ...
 
     if fee_amount > 0 {
-        if let Some(treasury) = storage::get_treasury(env) {
+        if let Some(treasury) = treasury_opt {
             token_client.transfer(&contract_address, &treasury, &fee_amount);
         }
     }
🔧 Option 2: Require treasury when fee is set

In set_fee, enforce that treasury must already be configured:

 pub fn set_fee(env: &Env, new_fee_bps: u32) -> Result<(), VaultError> {
     let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?;
     admin.require_auth();
     if new_fee_bps > 2000 {
         return Err(VaultError::FeeTooHigh);
     }
+    if new_fee_bps > 0 && storage::get_treasury(env).is_none() {
+        return Err(VaultError::TreasuryNotSet); // New error variant needed
+    }
     storage::set_fee_bps(env, new_fee_bps);
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/contract.rs` around lines 174 - 178, The
code currently deducts fee_amount but only transfers it when
storage::get_treasury(env) returns Some, causing tokens to remain in the
contract if treasury is None; update the payment flow in the function that
computes and applies fee (where fee_amount, contract_address and
token_client.transfer are used) to handle the None case: if
storage::get_treasury(env) is None then treat fee_amount as zero (i.e., do not
deduct from the expert payout and do not call token_client.transfer), otherwise
proceed with the transfer to the treasury; alternatively (if you prefer stricter
behavior) enforce in set_fee that a treasury must be configured before allowing
non-zero fees by validating in set_fee and returning an error when treasury is
missing.

@Bosun-Josh121
Copy link
Copy Markdown
Collaborator

Nice work @clintjeff2 . Please resolve the merge conflicts

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

🧹 Nitpick comments (2)
contracts/payment-vault-contract/src/lib.rs (1)

42-48: Add doc comments for new public entrypoints.

set_fee and set_treasury are admin-facing configuration methods that affect payment distribution. Adding doc comments (similar to other methods like pause/unpause) would clarify their purpose, parameters, and constraints (e.g., fee cap at 2000 BPS).

📝 Proposed documentation
+    /// Set the platform fee in basis points (Admin-only).
+    /// Fee is capped at 2000 BPS (20%). Fee is deducted from expert payout on finalization.
     pub fn set_fee(env: Env, new_fee_bps: u32) -> Result<(), VaultError> {
         contract::set_fee(&env, new_fee_bps)
     }

+    /// Set the treasury address to receive platform fees (Admin-only).
+    /// Must be configured before setting a non-zero fee to avoid locked funds.
     pub fn set_treasury(env: Env, treasury: Address) -> Result<(), VaultError> {
         contract::set_treasury(&env, &treasury)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/lib.rs` around lines 42 - 48, Add Rust
doc comments to the new public entrypoints set_fee and set_treasury similar to
existing docs for pause/unpause: describe that these are admin-only
configuration methods, document parameters (new_fee_bps as basis points with max
cap 2000 BPS and treasury as the destination Address), explain effects on
payment distribution and any constraints or errors (e.g., returns VaultError if
fee exceeds cap or caller unauthorized), and mention who can call them.
Reference the public functions set_fee and set_treasury when adding the ///
comments directly above each function definition.
contracts/payment-vault-contract/src/contract.rs (1)

176-176: Unchecked multiplication inconsistent with book_session.

book_session (lines 111-113) uses checked_mul with ok_or(VaultError::Overflow)? for rate_per_second * max_duration, but here in finalize_session the same pattern uses direct multiplication. While overflow is unlikely for realistic values (i128 can hold ~1.7e38, and typical products are much smaller), the inconsistency could mask edge-case bugs.

♻️ Proposed fix for consistency
     // 4. Calculate payments
-    let gross_expert_pay = booking.rate_per_second * (actual_duration as i128);
+    let gross_expert_pay = booking.rate_per_second
+        .checked_mul(actual_duration as i128)
+        .ok_or(VaultError::Overflow)?;
     let refund = booking.total_deposit - gross_expert_pay;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/contract.rs` at line 176, In
finalize_session, the multiplication that computes gross_expert_pay
(booking.rate_per_second * (actual_duration as i128)) should use checked_mul and
propagate an Overflow error to match book_session; replace the direct
multiplication in finalize_session with
booking.rate_per_second.checked_mul(actual_duration as
i128).ok_or(VaultError::Overflow)? (referencing booking.rate_per_second,
actual_duration, the finalize_session function and the book_session pattern that
uses checked_mul).
🤖 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/payment-vault-contract/src/lib.rs`:
- Around line 50-52: The doc comment "/// Set an expert's own rate per second"
is placed above transfer_admin but belongs to set_my_rate; move or remove the
misplaced comment: remove that line above the transfer_admin declaration
(function transfer_admin) and ensure the same comment is present directly above
the set_my_rate function (function set_my_rate) so documentation matches the
implementation.

In `@contracts/payment-vault-contract/src/test.rs`:
- Line 701: The tests call client.init(&admin, &token, &oracle) but the contract
init signature expects (env, admin, token, oracle, registry) so the client.init
calls must include the missing registry argument; update each affected test
(test_set_fee_and_treasury, test_fee_cap_at_2000_bps,
test_finalize_with_10_percent_fee, test_finalize_with_fee_and_partial_refund,
test_finalize_zero_fee_behaves_as_before) to call client.init(&admin, &token,
&oracle, &registry) (i.e., pass the fourth address for registry) so the test
harness matches the contract init signature and compiles.

---

Nitpick comments:
In `@contracts/payment-vault-contract/src/contract.rs`:
- Line 176: In finalize_session, the multiplication that computes
gross_expert_pay (booking.rate_per_second * (actual_duration as i128)) should
use checked_mul and propagate an Overflow error to match book_session; replace
the direct multiplication in finalize_session with
booking.rate_per_second.checked_mul(actual_duration as
i128).ok_or(VaultError::Overflow)? (referencing booking.rate_per_second,
actual_duration, the finalize_session function and the book_session pattern that
uses checked_mul).

In `@contracts/payment-vault-contract/src/lib.rs`:
- Around line 42-48: Add Rust doc comments to the new public entrypoints set_fee
and set_treasury similar to existing docs for pause/unpause: describe that these
are admin-only configuration methods, document parameters (new_fee_bps as basis
points with max cap 2000 BPS and treasury as the destination Address), explain
effects on payment distribution and any constraints or errors (e.g., returns
VaultError if fee exceeds cap or caller unauthorized), and mention who can call
them. Reference the public functions set_fee and set_treasury when adding the
/// comments directly above each function definition.
🪄 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: 65b7f404-cf5e-4df0-ac58-94c6bc9a2999

📥 Commits

Reviewing files that changed from the base of the PR and between 0df5476 and 83b2344.

📒 Files selected for processing (6)
  • contracts/payment-vault-contract/src/contract.rs
  • contracts/payment-vault-contract/src/error.rs
  • contracts/payment-vault-contract/src/events.rs
  • contracts/payment-vault-contract/src/lib.rs
  • contracts/payment-vault-contract/src/storage.rs
  • contracts/payment-vault-contract/src/test.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • contracts/payment-vault-contract/src/events.rs
  • contracts/payment-vault-contract/src/storage.rs
  • contracts/payment-vault-contract/src/error.rs

Comment on lines 50 to 52
/// Set an expert's own rate per second
/// Transfer admin rights to a new address (Admin-only)
/// Old admin instantly loses all privileges
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misplaced doc comment.

The comment /// Set an expert's own rate per second is positioned before transfer_admin but describes set_my_rate (line 63-68). This appears to be a leftover from editing.

🧹 Proposed fix
-    /// Set an expert's own rate per second
     /// Transfer admin rights to a new address (Admin-only)
     /// Old admin instantly loses all privileges
     pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), VaultError> {
📝 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
/// Set an expert's own rate per second
/// Transfer admin rights to a new address (Admin-only)
/// Old admin instantly loses all privileges
/// Transfer admin rights to a new address (Admin-only)
/// Old admin instantly loses all privileges
pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), VaultError> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/lib.rs` around lines 50 - 52, The doc
comment "/// Set an expert's own rate per second" is placed above transfer_admin
but belongs to set_my_rate; move or remove the misplaced comment: remove that
line above the transfer_admin declaration (function transfer_admin) and ensure
the same comment is present directly above the set_my_rate function (function
set_my_rate) so documentation matches the implementation.

@Bosun-Josh121
Copy link
Copy Markdown
Collaborator

CI fails and you deleted key rotation tests. Please resolve this and avoid affecting existing code unrelated to changes required

@clintjeff2
Copy link
Copy Markdown
Author

@Bosun-Josh121 review and merge please

clintjeff2 and others added 2 commits March 30, 2026 09:44
- Merge origin/feature/revenue-service-fee (main branch changes)
- Fix duplicate VaultError discriminant: FeeTooHigh reassigned to 13 (was 11, conflicting with SessionAlreadyStarted)
- Fix misplaced doc comment in lib.rs: remove '/// Set an expert\''s own rate per second' from before transfer_admin (it belongs before set_my_rate)
- Fix Platform Fee tests (test_set_fee_and_treasury, test_fee_cap_at_2000_bps, test_finalize_with_10_percent_fee, test_finalize_with_fee_and_partial_refund, test_finalize_zero_fee_behaves_as_before): add missing registry argument to client.init calls
- Add key rotation tests: test_transfer_admin_success, test_transfer_admin_requires_current_admin_auth, test_transfer_admin_old_admin_loses_privileges, test_set_oracle_success, test_set_oracle_requires_admin_auth, test_set_oracle_old_oracle_loses_finalize_auth

All 71 tests pass (25 identity-registry + 46 payment-vault)
@clintjeff2
Copy link
Copy Markdown
Author

CI fails and you deleted key rotation tests. Please resolve this and avoid affecting existing code unrelated to changes required

@Bosun-Josh121 that was a reckless mistake, please review the new changes.

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

♻️ Duplicate comments (1)
contracts/payment-vault-contract/src/contract.rs (1)

249-251: ⚠️ Potential issue | 🟠 Major

Don't deduct a fee unless Treasury is configured.

fee_amount is removed from expert_net_pay before the code knows it can transfer to a treasury. With a non-zero fee and no treasury, the expert is underpaid and the delta stays trapped in the vault.

Also applies to: 259-263

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

In `@contracts/payment-vault-contract/src/contract.rs` around lines 249 - 251, The
code deducts fee_amount from expert_net_pay unconditionally; change it to only
compute and subtract the fee when a Treasury is configured. Specifically,
replace the unconditional use of storage::get_fee_bps/env fee math around
fee_amount and expert_net_pay with a conditional: call the storage accessor that
returns the configured Treasury (e.g., storage::get_treasury(env) or equivalent)
and, if it returns Some(... ) and fee_bps > 0, compute fee_amount =
(gross_expert_pay * fee_bps as i128) / 10_000 and set expert_net_pay =
gross_expert_pay - fee_amount; otherwise set expert_net_pay = gross_expert_pay
and fee_amount = 0. Apply the same fix for the duplicated block referenced at
lines 259-263.
🤖 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/payment-vault-contract/src/contract.rs`:
- Around line 241-251: In finalize_session replace raw i128 math for
gross_expert_pay, refund, fee_amount and expert_net_pay with checked arithmetic
(checked_mul, checked_sub, checked_add, checked_div where appropriate) and
propagate any None to Err(VaultError::Overflow); specifically compute
gross_expert_pay using checked_mul, compute refund via checked_sub on
booking.total_deposit and gross_expert_pay, compute fee_amount by converting
fee_bps to i128 and using checked_mul followed by checked_div (or checked_mul
then checked_div with checks), then compute expert_net_pay via checked_sub, and
return Err(VaultError::Overflow) whenever a checked_* yields None instead of
allowing a panic.

In `@contracts/payment-vault-contract/src/test.rs`:
- Around line 980-983: The test currently only verifies failure with no auth by
calling env.set_auths(&[]) before client.try_finalize_session(&booking_id,
&50_u64); instead it must also assert that providing the old oracle's auth
(oracle_a) is rejected after rotation and that the new oracle's auth (oracle_b)
succeeds: set auths to oracle_a and assert try_finalize_session(&booking_id,
&50_u64) returns Err, then set auths to oracle_b and assert
try_finalize_session(...) returns Ok; use the existing env.set_auths,
try_finalize_session, booking_id, oracle_a and oracle_b identifiers to locate
and implement these checks.
- Around line 903-909: The test currently clears all auths (env.set_auths(&[]))
which causes client.try_pause() to fail regardless of who is stored as admin;
instead, explicitly authenticate as admin_a and assert try_pause() returns an
error to prove admin_a lost admin rights, then authenticate as admin_b and
assert try_pause() succeeds (is_ok) to prove rotation worked; update calls
around env.set_auths, using env.set_auths(&[admin_a]) to assert failure and
env.set_auths(&[admin_b]) to assert success, referencing client.try_pause(),
admin_a, and admin_b in the updated assertions.

---

Duplicate comments:
In `@contracts/payment-vault-contract/src/contract.rs`:
- Around line 249-251: The code deducts fee_amount from expert_net_pay
unconditionally; change it to only compute and subtract the fee when a Treasury
is configured. Specifically, replace the unconditional use of
storage::get_fee_bps/env fee math around fee_amount and expert_net_pay with a
conditional: call the storage accessor that returns the configured Treasury
(e.g., storage::get_treasury(env) or equivalent) and, if it returns Some(... )
and fee_bps > 0, compute fee_amount = (gross_expert_pay * fee_bps as i128) /
10_000 and set expert_net_pay = gross_expert_pay - fee_amount; otherwise set
expert_net_pay = gross_expert_pay and fee_amount = 0. Apply the same fix for the
duplicated block referenced at lines 259-263.
🪄 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: 4b190363-021a-4118-8d54-10bce25571c9

📥 Commits

Reviewing files that changed from the base of the PR and between 83b2344 and ac5e76f.

📒 Files selected for processing (6)
  • contracts/payment-vault-contract/src/contract.rs
  • contracts/payment-vault-contract/src/error.rs
  • contracts/payment-vault-contract/src/events.rs
  • contracts/payment-vault-contract/src/lib.rs
  • contracts/payment-vault-contract/src/storage.rs
  • contracts/payment-vault-contract/src/test.rs
✅ Files skipped from review due to trivial changes (1)
  • contracts/payment-vault-contract/src/events.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/payment-vault-contract/src/error.rs

Comment on lines +241 to +251
// 4. Calculate payments
let gross_expert_pay = booking.rate_per_second * (actual_duration as i128);
let refund = booking.total_deposit - gross_expert_pay;

// Ensure calculations are valid
if expert_pay < 0 || refund < 0 {
if gross_expert_pay < 0 || refund < 0 {
return Err(VaultError::InvalidAmount);
}

let fee_bps = storage::get_fee_bps(env);
let fee_amount = (gross_expert_pay * fee_bps as i128) / 10_000;
let expert_net_pay = gross_expert_pay - fee_amount;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== finalize_session settlement math ==="
sed -n '241,252p' contracts/payment-vault-contract/src/contract.rs

echo
echo "=== overflow-check settings in Cargo profiles ==="
fd -HI 'Cargo.toml|config.toml' . -t f | while read -r file; do
  matches=$(rg -n '^\[profile\.release\]|overflow-checks' "$file" || true)
  if [ -n "$matches" ]; then
    echo "--- $file ---"
    printf '%s\n' "$matches"
  fi
done

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 1187


Use checked arithmetic for graceful error handling in finalize_session.

Lines 242–250 use raw i128 arithmetic. While the workspace enables overflow-checks = true in all Cargo profiles (which causes panics on overflow rather than silent wrapping), it's better to handle potential overflows explicitly and return VaultError::Overflow instead of crashing. This improves user experience and gives the contract control over error handling.

Proposed fix
     // 4. Calculate payments
-    let gross_expert_pay = booking.rate_per_second * (actual_duration as i128);
-    let refund = booking.total_deposit - gross_expert_pay;
+    let gross_expert_pay = booking
+        .rate_per_second
+        .checked_mul(actual_duration as i128)
+        .ok_or(VaultError::Overflow)?;
+    let refund = booking
+        .total_deposit
+        .checked_sub(gross_expert_pay)
+        .ok_or(VaultError::Overflow)?;
 
-    if gross_expert_pay < 0 || refund < 0 {
+    if refund < 0 {
         return Err(VaultError::InvalidAmount);
     }
 
     let fee_bps = storage::get_fee_bps(env);
-    let fee_amount = (gross_expert_pay * fee_bps as i128) / 10_000;
-    let expert_net_pay = gross_expert_pay - fee_amount;
+    let fee_amount = gross_expert_pay
+        .checked_mul(fee_bps as i128)
+        .ok_or(VaultError::Overflow)?
+        / 10_000;
+    let expert_net_pay = gross_expert_pay
+        .checked_sub(fee_amount)
+        .ok_or(VaultError::Overflow)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/payment-vault-contract/src/contract.rs` around lines 241 - 251, In
finalize_session replace raw i128 math for gross_expert_pay, refund, fee_amount
and expert_net_pay with checked arithmetic (checked_mul, checked_sub,
checked_add, checked_div where appropriate) and propagate any None to
Err(VaultError::Overflow); specifically compute gross_expert_pay using
checked_mul, compute refund via checked_sub on booking.total_deposit and
gross_expert_pay, compute fee_amount by converting fee_bps to i128 and using
checked_mul followed by checked_div (or checked_mul then checked_div with
checks), then compute expert_net_pay via checked_sub, and return
Err(VaultError::Overflow) whenever a checked_* yields None instead of allowing a
panic.

Comment on lines +903 to +909
// admin_a should no longer be able to call admin-only operations
// (only admin_b is now the admin; mock_all_auths allows both,
// so we disable and re-mock only admin_a to verify it fails)
env.set_auths(&[]);

// Without any valid auth for admin_b, pause should fail
// (admin_b is now the required auth, but no auth is mocked)
let result = client.try_pause();
assert!(result.is_err());
// Attempting a pause (admin-only) with no auth should fail
let res = client.try_pause();
assert!(res.is_err());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This test does not prove admin_a lost admin-only access.

Clearing all auths makes try_pause() fail regardless of which address is stored as admin. To validate the rotation, this should authenticate as admin_a and show failure, then authenticate as admin_b and show success.

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

In `@contracts/payment-vault-contract/src/test.rs` around lines 903 - 909, The
test currently clears all auths (env.set_auths(&[])) which causes
client.try_pause() to fail regardless of who is stored as admin; instead,
explicitly authenticate as admin_a and assert try_pause() returns an error to
prove admin_a lost admin rights, then authenticate as admin_b and assert
try_pause() succeeds (is_ok) to prove rotation worked; update calls around
env.set_auths, using env.set_auths(&[admin_a]) to assert failure and
env.set_auths(&[admin_b]) to assert success, referencing client.try_pause(),
admin_a, and admin_b in the updated assertions.

Comment on lines +980 to +983
// Now finalize without any auth — should fail (oracle_a no longer valid)
env.set_auths(&[]);
let res = client.try_finalize_session(&booking_id, &50_u64);
assert!(res.is_err());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This only checks “no auth”, not “old oracle rejected”.

env.set_auths(&[]) makes try_finalize_session() fail even if oracle_a is still accepted. The regression test should explicitly try finalizing with oracle_a auth after rotation, and with oracle_b auth as the success case.

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

In `@contracts/payment-vault-contract/src/test.rs` around lines 980 - 983, The
test currently only verifies failure with no auth by calling env.set_auths(&[])
before client.try_finalize_session(&booking_id, &50_u64); instead it must also
assert that providing the old oracle's auth (oracle_a) is rejected after
rotation and that the new oracle's auth (oracle_b) succeeds: set auths to
oracle_a and assert try_finalize_session(&booking_id, &50_u64) returns Err, then
set auths to oracle_b and assert try_finalize_session(...) returns Ok; use the
existing env.set_auths, try_finalize_session, booking_id, oracle_a and oracle_b
identifiers to locate and implement these checks.

@Bosun-Josh121
Copy link
Copy Markdown
Collaborator

Still errors in CI @clintjeff2

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.

Implement Platform Revenue (Service Fee)

2 participants