Skip to content

feat: add admin and session safety controls#125

Merged
Luluameh merged 1 commit into
LightForgeHub:mainfrom
Akpamgbo:codex/feat/protocol-guards-116-117-118-119
Mar 29, 2026
Merged

feat: add admin and session safety controls#125
Luluameh merged 1 commit into
LightForgeHub:mainfrom
Akpamgbo:codex/feat/protocol-guards-116-117-118-119

Conversation

@Akpamgbo
Copy link
Copy Markdown
Contributor

@Akpamgbo Akpamgbo commented Mar 29, 2026

Description

Implements the contract-side protocol safety controls requested across the admin, reputation, funding window, and emergency shutdown issues in one coordinated Soroban update.

Closes #116
Closes #117
Closes #118
Closes #119

Changes proposed

What were you told to do?

Implement multi-sig-ready admin controls for protocol parameters, enforce an expert reputation threshold during session creation, terminate sessions cleanly once their funded window is exhausted, and add an admin-controlled circuit breaker that blocks risky actions while still allowing refunds.

What did I do?

Admin and protocol controls

  • Required admin authorization during initialization so Stellar multi-sig or DAO-controlled admin addresses can back protocol actions.
  • Added set_admin, get_admin, set_fee, and get_fee plus shared admin guard helpers.
  • Added pause_protocol, unpause_protocol, and is_protocol_paused to freeze sensitive flows.

Reputation and session funding guards

  • Added on-chain expert reputation storage with set_expert_reputation and get_expert_reputation.
  • Extended start_session with a min_reputation requirement and rejects experts that do not meet the seeker threshold.
  • Added calculate_expiry_timestamp plus bounded settlement logic so sessions settle only within their funded window and auto-finish when fully drained.

Refund and regression coverage

  • Added refund_session so seekers can recover unused funds even while the protocol is paused, while still paying experts for accrued work.
  • Expanded the contract tests to cover admin controls, reputation gating, funded-window expiry, and pause/refund behavior.

Check List (Check all the applicable boxes)

  • My code follows the code style of this project.
  • This PR does not contain plagiarized content.
  • The title and description of the PR is clear and explains the approach.
  • I am making a pull request against the main branch (left side).
  • My commit messages styles matches our requested structure.
  • My code additions will fail neither code linting checks nor unit test.
  • I am only making changes to files I was requested to.

Screenshots / Testing Evidence

Validated the Soroban contract with cargo fmt and cargo test in contracts; all 15 contract tests passed, including new coverage for admin handoff and fee storage, expert reputation enforcement, funded-window expiry, protocol pause behavior, and refunds while paused.

Summary by CodeRabbit

  • New Features

    • Added admin governance controls: manage protocol administrators, set platform fees, and pause/unpause the protocol as needed.
    • Introduced expert reputation system with reputation-based gating for session creation.
    • Added automatic session expiry enforcement with refund capabilities.
  • Bug Fixes

    • Sessions are now blocked when the protocol is paused.
    • Session creation validates expert reputation thresholds.
    • Improved dispute resolution and settlement mechanisms.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

Added admin-controlled protocol governance with fee management, pause/unpause functionality, and expert reputation tracking. Enhanced session lifecycle with reputation-based gating, automatic expiry enforcement, and new refund mechanism. Refactored authorization checks and dispute resolution to use centralized admin validation, with type adjustments across dispute and upgrade structures.

Changes

Cohort / File(s) Summary
Admin & Governance
contracts/src/lib.rs
New public methods for admin management (set_admin, get_admin), platform fee control (set_fee, get_fee), and protocol pause/unpause (pause_protocol, unpause_protocol, is_protocol_paused). Admin checks centralized via require_admin helper.
Expert Reputation System
contracts/src/lib.rs
Added on-chain reputation storage and management with new set_expert_reputation and get_expert_reputation methods. Reputation enforced in session creation via new min_reputation parameter.
Session Lifecycle & Expiry
contracts/src/lib.rs
Extended start_session with min_reputation gating. Added calculate_expiry_timestamp for funded window computation. Enhanced settle_session with session expiry enforcement—marks sessions Finished and returns SessionExpired when time exceeds funded duration. Refactored calculate_claimable_amount to bound user-provided timestamps.
Refund & Closing Logic
contracts/src/lib.rs
New public refund_session method for seeker-authorized withdrawals. Shared closing logic extracted into internal close_session. Updated end_session to use refactored flow.
Dispute & Upgrade Refactoring
contracts/src/lib.rs
Changed Dispute struct fields from soroban_sdk::String to String. Refactored resolve_dispute to compute transfer amounts separately, then execute transfers. Centralized admin authorization via require_admin. Updated initiate_upgrade and UpgradeTimelock type declarations.

Sequence Diagram(s)

sequenceDiagram
    actor Seeker
    participant Contract as SkillSphere Contract
    participant Storage as Storage (Reputation)
    participant Balance as Token Balance
    participant Expert

    Seeker->>Contract: start_session(expert, min_reputation, rate, amount)
    activate Contract
    
    Contract->>Contract: Check: protocol not paused
    alt Protocol Paused
        Contract-->>Seeker: ProtocolPaused Error
    else Proceed
        Contract->>Storage: Get expert reputation
        activate Storage
        Storage-->>Contract: expert_reputation
        deactivate Storage
        
        alt Reputation < min_reputation
            Contract-->>Seeker: ReputationTooLow Error
        else Reputation OK
            Contract->>Balance: Lock amount tokens
            activate Balance
            Balance-->>Contract: Locked ✓
            deactivate Balance
            
            Contract->>Storage: Store session with expiry window
            Contract-->>Seeker: session_id
        end
    end
    
    deactivate Contract
    
    rect rgb(100, 150, 200, 0.5)
    note over Contract: Later: settle_session
    Seeker->>Contract: settle_session(session_id)
    activate Contract
    
    Contract->>Contract: Check: protocol not paused
    Contract->>Storage: Calculate claimable (with time bounds)
    Contract->>Contract: Check: current_time > expiry?
    
    alt Expired & Nothing Claimable
        Contract->>Storage: Mark session Finished
        Contract-->>Seeker: SessionExpired
    else Claimable Funds
        Contract->>Balance: Transfer claimable to Expert
        Contract-->>Seeker: claimable_amount
    end
    
    deactivate Contract
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 Whispers of governance through tunnels deep,
Admin keys now guard what seekers keep,
Reputation blooms where experts dare,
Sessions pause, then breathe fresh air,
A circuit-breaker's heartbeat fair! ✨🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: admin controls and session safety mechanisms (expiry, reputation gating, pause/resume, refunds) added throughout the contract.
Linked Issues check ✅ Passed All four linked issue requirements are met: #116 expiry/drain logic implemented; #117 admin/fee controls added; #118 pause/unpause with selective refund access implemented; #119 reputation gating added to start_session.
Out of Scope Changes check ✅ Passed All changes align with the four linked issues; refactoring of dispute types and upgrade checks are supporting changes necessary to implement the core governance and safety features.

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

Caution

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

⚠️ Outside diff range comments (3)
contracts/src/lib.rs (3)

235-244: ⚠️ Potential issue | 🟠 Major

Session is stored before token transfer, risking inconsistent state on transfer failure.

If token_client.transfer (line 241) fails, the session is already persisted in storage (lines 235-237) with balance: amount. This creates an orphan session with no actual locked funds.

Consider reordering to transfer first:

🐛 Proposed fix: Transfer tokens before storing session
+        token_client.transfer(&seeker, &env.current_contract_address(), &amount);
+
         env.storage()
             .persistent()
             .set(&DataKey::Session(session_id), &session);
         env.events()
             .publish((symbol_short!("started"),), session_id);
-
-        token_client.transfer(&seeker, &env.current_contract_address(), &amount);

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

In `@contracts/src/lib.rs` around lines 235 - 244, The session is persisted
(env.storage().persistent().set with DataKey::Session(session_id)) and published
before attempting token_client.transfer, risking an orphan session if transfer
fails; change the order so you call token_client.transfer(&seeker,
&env.current_contract_address(), &amount) first and propagate any error, then on
successful transfer write the session to storage and publish the
(symbol_short!("started"),) event for session_id; ensure the function returns
the transfer error if it fails so the storage/publish steps are not executed.

54-60: ⚠️ Potential issue | 🟡 Minor

Inconsistent mapping between Resolution enum discriminants and stored resolution values.

The Resolution enum defines SeekerWins = 0, ExpertWins = 1, Refund = 2, but resolve_dispute (lines 442-446) stores 1, 2, 3 respectively. This creates confusion and makes the stored value unusable for reconstructing the original enum.

Consider storing the enum discriminant directly:

🔧 Proposed fix to align stored values with enum
         dispute.resolution = match resolution {
-            Resolution::SeekerWins => 1,
-            Resolution::ExpertWins => 2,
-            Resolution::Refund => 3,
+            Resolution::SeekerWins => 0,
+            Resolution::ExpertWins => 1,
+            Resolution::Refund => 2,
         };

Or better, store the enum directly if Resolution derives Copy (which it does):

pub struct Dispute {
    // ...
    pub resolution: Option<Resolution>,  // None when unresolved
}

Also applies to: 70-70

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

In `@contracts/src/lib.rs` around lines 54 - 60, The stored numeric resolution
values do not match the Resolution enum discriminants (Resolution: SeekerWins=0,
ExpertWins=1, Refund=2 vs resolve_dispute storing 1,2,3), so update the contract
to use a consistent representation: change the Dispute struct's resolution field
to Option<Resolution> (so unresolved is None and resolved stores the enum
directly) and update resolve_dispute to assign the appropriate Resolution
variant (SeekerWins/ExpertWins/Refund) instead of hard-coded integers;
alternatively, if you must store integers, change the values written in
resolve_dispute to use the enum discriminants (0,1,2) to match Resolution.
Ensure all code paths that read/write Dispute::resolution and any
serialization/de/serialization use the new Option<Resolution> type or the
corrected integer mapping to keep reconstruction correct.

282-300: ⚠️ Potential issue | 🟡 Minor

Resuming session after expiry sets incorrect last_settlement_timestamp.

If a session is paused, then time passes beyond the expiry window, and then it's resumed, line 292 sets last_settlement_timestamp to the current unbounded time. This could create incorrect accounting since the session's funded window has already elapsed.

Consider bounding the resume timestamp or preventing resume after expiry:

🔧 Proposed fix: Bound resume timestamp or prevent post-expiry resume
 pub fn resume_session(env: Env, caller: Address, session_id: u64) -> Result<(), Error> {
     Self::ensure_protocol_active(&env)?;
     caller.require_auth();
     let mut session = Self::get_session_or_error(&env, session_id)?;
     Self::require_participant(&session, &caller)?;

     if session.status != SessionStatus::Paused {
         return Err(Error::InvalidSessionState);
     }

-    session.last_settlement_timestamp = env.ledger().timestamp();
+    let now = env.ledger().timestamp();
+    let expiry = Self::expiry_timestamp_for_session(&session);
+    if now >= expiry {
+        return Err(Error::SessionExpired);
+    }
+    session.last_settlement_timestamp = now;
     session.status = SessionStatus::Active;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 282 - 300, In resume_session, avoid
setting last_settlement_timestamp to an unbounded future value by bounding it to
the session expiry (or failing resume if expired): fetch now =
env.ledger().timestamp(), then if now > session.expiry either return
Err(Error::SessionExpired) or set session.last_settlement_timestamp = min(now,
session.expiry); keep the rest of resume_session (status change, save_session,
publish) unchanged while using the existing session, resume_session,
last_settlement_timestamp, and session.expiry identifiers to locate the change.
🧹 Nitpick comments (4)
contracts/src/lib.rs (4)

177-192: Consider adding bounds validation for reputation value.

The set_expert_reputation function accepts any u32 value without upper bounds. If reputation is intended to represent a percentage or score (0-100), unbounded values could cause confusion.

💡 Optional: Add max reputation validation
+const MAX_REPUTATION: u32 = 100;
+
 pub fn set_expert_reputation(env: Env, expert: Address, reputation: u32) -> Result<(), Error> {
     Self::require_admin(&env)?;
+    if reputation > MAX_REPUTATION {
+        return Err(Error::InvalidAmount); // or add InvalidReputation error
+    }
     env.storage()
         .persistent()
         .set(&DataKey::ExpertReputation(expert.clone()), &reputation);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 177 - 192, Add an upper-bound check in
set_expert_reputation to validate the incoming reputation before writing to
storage: ensure the u32 reputation is within the expected range (e.g. 0..=100)
and return an Err variant (add or reuse an Error like Error::InvalidReputation)
when out of bounds; keep the existing storage write to
DataKey::ExpertReputation(expert.clone()) and the publish call
(symbol_short!("setReput")) only after the value passes validation.

319-327: Returning SessionExpired error after successfully updating session state may confuse callers.

Lines 320-324 update the session status to Finished and persist it, then return Err(Error::SessionExpired). While technically correct (no tokens transferred), this pattern is unusual—callers might expect no state change on error.

Consider either:

  1. Returning Ok(0) with the state change (session naturally expired, no claim available)
  2. Or documenting this behavior clearly
💡 Alternative: Return Ok(0) for natural expiry
         if claimable <= 0 {
             if now > expiry {
                 session.status = SessionStatus::Finished;
                 session.last_settlement_timestamp = expiry;
                 Self::save_session(&env, &session);
-                return Err(Error::SessionExpired);
+                env.events().publish((symbol_short!("expired"),), session_id);
+                return Ok(0);
             }
 
             return Ok(0);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 319 - 327, The code updates session.status
to SessionStatus::Finished, sets session.last_settlement_timestamp, and calls
Self::save_session(&env, &session) but then returns Err(Error::SessionExpired),
which is surprising; change the return to Ok(0) after persisting the session (in
the same branch where claimable <= 0 and now > expiry) so callers observe the
state change but receive a successful zero-claim result (adjust any
callers/tests if they expected Error::SessionExpired); update the branch
handling claimable, now, expiry around variables/methods claimable, now, expiry,
session.status, SessionStatus::Finished, session.last_settlement_timestamp,
Self::save_session, and remove the Err(Error::SessionExpired) return.

675-686: Theoretical truncation risk in expiry calculation, but practically safe.

The cast funded_seconds as u64 (line 681) could theoretically truncate if balance/rate_per_second exceeds u64::MAX (~584 billion years). While practically impossible with realistic token amounts, consider using try_into() or saturating_add for defensive coding:

💡 Optional: Defensive saturating conversion
-        let funded_seconds =
-            ((session.balance + session.rate_per_second - 1) / session.rate_per_second) as u64;
+        let funded_seconds_i128 =
+            (session.balance + session.rate_per_second - 1) / session.rate_per_second;
+        let funded_seconds = if funded_seconds_i128 > u64::MAX as i128 {
+            u64::MAX
+        } else {
+            funded_seconds_i128 as u64
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 675 - 686, The
expiry_timestamp_for_session function casts funded_seconds to u64 which could
theoretically overflow when converting from the intermediate integer type;
update the conversion to be defensive by using try_into() and handling Err by
returning u64::MAX (or use saturating conversion) before calling
last_settlement_timestamp.saturating_add, e.g. perform let funded_seconds =
((session.balance + session.rate_per_second - 1) /
session.rate_per_second).try_into().unwrap_or(u64::MAX) and then use
last_settlement_timestamp.saturating_add(funded_seconds) so the Session overflow
is handled safely; refer to expiry_timestamp_for_session, Session and
funded_seconds in your change.

917-933: Test name promises to verify settlement blocking but doesn't test it.

test_protocol_pause_blocks_settlement_but_allows_refund_session only tests that refund_session works during pause. It doesn't verify that settle_session is blocked. Consider adding the settlement assertion:

🧪 Add assertion for blocked settlement
 #[test]
+#[should_panic(expected = "Error(Contract, `#12`)")]
+fn test_protocol_pause_blocks_settlement() {
+    let (env, client, _, _, seeker, expert, token, _) = setup();
+    let session_id = client.start_session(&seeker, &expert, &token, &10, &500, &0);
+
+    env.ledger().set_timestamp(1_010);
+    client.pause_protocol();
+
+    // This should panic with ProtocolPaused error
+    client.settle_session(&session_id);
+}
+
+#[test]
 fn test_protocol_pause_blocks_settlement_but_allows_refund_session() {

Or rename the existing test to test_protocol_pause_allows_refund_session.

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

In `@contracts/src/lib.rs` around lines 917 - 933, The test named
test_protocol_pause_blocks_settlement_but_allows_refund_session currently only
calls client.refund_session and asserts balances; add an explicit check that
calling client.settle_session(&seeker, &session_id) while the protocol is paused
is rejected: call settle_session after pause (and after refund if you keep the
current flow) and assert it returns an error/non-zero code (or Err) and that
token balances (token::Client::balance for expert and seeker) remain unchanged;
reference client.pause_protocol(), client.settle_session(),
client.refund_session(), token::Client::balance(), and client.get_session() to
locate where to add the assertion.
🤖 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/src/lib.rs`:
- Around line 133-153: The platform fee is stored by set_fee/get_fee but never
applied: update settle_session, close_session, and resolve_dispute to read the
fee via get_fee (PlatformFeeBps), compute fee_amount = claimable * fee_bps /
MAX_BPS (integer math) and subtract it from amounts sent to the expert
(similarly apply to remaining where applicable), transfer the fee_amount to the
platform/admin account (use the same admin/platform address used elsewhere in
the contract) and transfer the net amount to the expert, and update any emitted
events to reflect both fee and net transfers; ensure you reference and reuse the
existing symbols PlatformFeeBps, get_fee, settle_session, close_session,
resolve_dispute, claimable, remaining, and MAX_BPS.

---

Outside diff comments:
In `@contracts/src/lib.rs`:
- Around line 235-244: The session is persisted (env.storage().persistent().set
with DataKey::Session(session_id)) and published before attempting
token_client.transfer, risking an orphan session if transfer fails; change the
order so you call token_client.transfer(&seeker,
&env.current_contract_address(), &amount) first and propagate any error, then on
successful transfer write the session to storage and publish the
(symbol_short!("started"),) event for session_id; ensure the function returns
the transfer error if it fails so the storage/publish steps are not executed.
- Around line 54-60: The stored numeric resolution values do not match the
Resolution enum discriminants (Resolution: SeekerWins=0, ExpertWins=1, Refund=2
vs resolve_dispute storing 1,2,3), so update the contract to use a consistent
representation: change the Dispute struct's resolution field to
Option<Resolution> (so unresolved is None and resolved stores the enum directly)
and update resolve_dispute to assign the appropriate Resolution variant
(SeekerWins/ExpertWins/Refund) instead of hard-coded integers; alternatively, if
you must store integers, change the values written in resolve_dispute to use the
enum discriminants (0,1,2) to match Resolution. Ensure all code paths that
read/write Dispute::resolution and any serialization/de/serialization use the
new Option<Resolution> type or the corrected integer mapping to keep
reconstruction correct.
- Around line 282-300: In resume_session, avoid setting
last_settlement_timestamp to an unbounded future value by bounding it to the
session expiry (or failing resume if expired): fetch now =
env.ledger().timestamp(), then if now > session.expiry either return
Err(Error::SessionExpired) or set session.last_settlement_timestamp = min(now,
session.expiry); keep the rest of resume_session (status change, save_session,
publish) unchanged while using the existing session, resume_session,
last_settlement_timestamp, and session.expiry identifiers to locate the change.

---

Nitpick comments:
In `@contracts/src/lib.rs`:
- Around line 177-192: Add an upper-bound check in set_expert_reputation to
validate the incoming reputation before writing to storage: ensure the u32
reputation is within the expected range (e.g. 0..=100) and return an Err variant
(add or reuse an Error like Error::InvalidReputation) when out of bounds; keep
the existing storage write to DataKey::ExpertReputation(expert.clone()) and the
publish call (symbol_short!("setReput")) only after the value passes validation.
- Around line 319-327: The code updates session.status to
SessionStatus::Finished, sets session.last_settlement_timestamp, and calls
Self::save_session(&env, &session) but then returns Err(Error::SessionExpired),
which is surprising; change the return to Ok(0) after persisting the session (in
the same branch where claimable <= 0 and now > expiry) so callers observe the
state change but receive a successful zero-claim result (adjust any
callers/tests if they expected Error::SessionExpired); update the branch
handling claimable, now, expiry around variables/methods claimable, now, expiry,
session.status, SessionStatus::Finished, session.last_settlement_timestamp,
Self::save_session, and remove the Err(Error::SessionExpired) return.
- Around line 675-686: The expiry_timestamp_for_session function casts
funded_seconds to u64 which could theoretically overflow when converting from
the intermediate integer type; update the conversion to be defensive by using
try_into() and handling Err by returning u64::MAX (or use saturating conversion)
before calling last_settlement_timestamp.saturating_add, e.g. perform let
funded_seconds = ((session.balance + session.rate_per_second - 1) /
session.rate_per_second).try_into().unwrap_or(u64::MAX) and then use
last_settlement_timestamp.saturating_add(funded_seconds) so the Session overflow
is handled safely; refer to expiry_timestamp_for_session, Session and
funded_seconds in your change.
- Around line 917-933: The test named
test_protocol_pause_blocks_settlement_but_allows_refund_session currently only
calls client.refund_session and asserts balances; add an explicit check that
calling client.settle_session(&seeker, &session_id) while the protocol is paused
is rejected: call settle_session after pause (and after refund if you keep the
current flow) and assert it returns an error/non-zero code (or Err) and that
token balances (token::Client::balance for expert and seeker) remain unchanged;
reference client.pause_protocol(), client.settle_session(),
client.refund_session(), token::Client::balance(), and client.get_session() to
locate where to add the assertion.
🪄 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: ca612c24-20fd-4d3a-98b6-05e77feaec9b

📥 Commits

Reviewing files that changed from the base of the PR and between a6fa1e8 and bea495f.

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

Comment thread contracts/src/lib.rs
Comment on lines +133 to +153
pub fn set_fee(env: Env, fee_bps: u32) -> Result<(), Error> {
Self::require_admin(&env)?;

if fee_bps > MAX_BPS {
return Err(Error::InvalidFeeBps);
}

env.storage()
.instance()
.set(&DataKey::PlatformFeeBps, &fee_bps);
env.events().publish((symbol_short!("setFee"),), fee_bps);

Ok(())
}

pub fn get_fee(env: Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::PlatformFeeBps)
.unwrap_or(0u32)
}
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
# Search for any usage of PlatformFeeBps or get_fee in settlement/transfer logic
rg -n 'PlatformFeeBps|get_fee' contracts/src/lib.rs | grep -v 'set_fee\|DataKey::PlatformFeeBps'

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 185


🏁 Script executed:

# Search for settle_session, close_session, and resolve_dispute function implementations
rg -n 'fn settle_session|fn close_session|fn resolve_dispute' contracts/src/lib.rs -A 20

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 2724


🏁 Script executed:

# Check if there are any arithmetic operations or divisions that might apply fees
rg -n 'divide|multiply|fee|percent' contracts/src/lib.rs -i | head -30

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 681


🏁 Script executed:

# Search for token transfers and where balances are actually transferred
rg -n 'token_client\|transfer\|balance.*=' contracts/src/lib.rs | head -40

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 56


🏁 Script executed:

# Get all references to PlatformFeeBps across the entire file
rg -n 'PlatformFeeBps' contracts/src/lib.rs

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 256


🏁 Script executed:

# Get the full settle_session and close_session implementations to see token transfers
sed -n '302,422p' contracts/src/lib.rs

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 3732


🏁 Script executed:

# Check close_session fully
sed -n '613,660p' contracts/src/lib.rs

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 1717


Platform fee is stored but never deducted from settlements.

The set_fee and get_fee functions exist, but PlatformFeeBps is never read in settle_session, close_session, or resolve_dispute. In settle_session (line 350), the full claimable amount is transferred to the expert without any fee deduction. Similarly, in close_session (lines 644, 649), both claimable and remaining are transferred in full. The fee has no effect on any token transfers.

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

In `@contracts/src/lib.rs` around lines 133 - 153, The platform fee is stored by
set_fee/get_fee but never applied: update settle_session, close_session, and
resolve_dispute to read the fee via get_fee (PlatformFeeBps), compute fee_amount
= claimable * fee_bps / MAX_BPS (integer math) and subtract it from amounts sent
to the expert (similarly apply to remaining where applicable), transfer the
fee_amount to the platform/admin account (use the same admin/platform address
used elsewhere in the contract) and transfer the net amount to the expert, and
update any emitted events to reflect both fee and net transfers; ensure you
reference and reuse the existing symbols PlatformFeeBps, get_fee,
settle_session, close_session, resolve_dispute, claimable, remaining, and
MAX_BPS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants