Skip to content

feat: jury selection, appeal mechanism, fee auto-conversion, and surge pricing (#284–287)#294

Merged
Luluameh merged 1 commit into
LightForgeHub:mainfrom
ijeoma270:feat/issues-284-285-286-287
Jun 25, 2026
Merged

feat: jury selection, appeal mechanism, fee auto-conversion, and surge pricing (#284–287)#294
Luluameh merged 1 commit into
LightForgeHub:mainfrom
ijeoma270:feat/issues-284-285-286-287

Conversation

@ijeoma270

@ijeoma270 ijeoma270 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Closes #284
Closes #285
Closes #286
Closes #287

What changed

Issue #284 — Jury Selection

  • New JuryVoteRecord #[contracttype] struct storing the jury panel, per-side vote counts, and a voted list to prevent double-voting
  • select_jury(dispute_id, candidates, jury_size) — admin-only; filters candidates by minimum reputation (≥ 300), shuffles with on-chain PRNG (env.prng().shuffle), and selects the first jury_size from the shuffled list
  • cast_jury_vote(juror, dispute_id, vote_for_seeker) — juror-signed; rejects non-panel addresses and duplicate votes
  • finalize_jury_verdict(dispute_id) — callable once all jurors vote or a majority is reached; winning side gets 9000 bps, loser gets 1000 bps (50/50 on tie)
  • New DataKey variants: JurySession(u64), JurySize
  • New error codes: JuryNotSelected(70), JuryAlreadyVoted(71), JuryVotingClosed(72), InsufficientCandidates(73)

Issue #285 — Appeal Mechanism

  • New AppealRecord #[contracttype] struct with appellant address, bond amount/token, timestamps, and ruling fields
  • appeal_dispute(appellant, dispute_id, bond_token) — filed while dispute is still open; collects a configurable bond held in contract escrow
  • resolve_appeal(dispute_id, seeker_award_bps, expert_award_bps) — admin-only; delegates to resolve_dispute_internal with the new split and returns the bond to the appellant
  • New DataKey variants: Appeal(u64), AppealBondAmount
  • New error codes: AppealBondRequired(74), AppealAlreadyFiled(75), AppealNotFound(76)

Issue #286 — Fee Auto-Conversion (SKILL buyback)

  • convert_fees_to_skill(fee_amount, source_token, expected_skill_out) in treasury.rs — reuses the existing dex::cross_contract_swap and dex::check_slippage helpers; slippage defaults to 100 bps
  • Admin setters: set_skill_token, set_fee_buyback_enabled, set_fee_buyback_slippage
  • New DataKey variants: SkillTokenAddress, FeeBuybackEnabled, FeeBuybackSlippageBps
  • New error codes: SkillTokenNotSet(78), BuybackDisabled(79)

Issue #287 — Surge Pricing

  • New SurgeConfig #[contracttype] struct (threshold_sessions, max_multiplier_bps, step_sessions, step_bps)
  • get_surge_multiplier_bps(expert) — reads CategoryActiveSessions for the expert's category and computes a stepped multiplier, capped at max_multiplier_bps
  • start_session_with_surge(…, surge_accepted) — wraps start_session; panics with SurgeNotAccepted if surge is active and surge_accepted = false; increments CategoryActiveSessions on success
  • end_surge_session(caller, session_id) — participant-callable decrement after a session ends
  • Admin setters: set_surge_config, set_surge_pricing_enabled, set_expert_category
  • New DataKey variants: CategoryActiveSessions(String), SurgePricingEnabled, ExpertCategory(Address), SurgeConfig
  • New error code: SurgeNotAccepted(82)

Why

Addresses the four open feature issues to extend SkillSphere dispute resolution and dynamic pricing with on-chain jury governance, appeal pathways, SKILL token buyback from platform fees, and demand-responsive surge pricing.

How to test

  • Jury: call flag_disputeselect_jury with ≥3 reputation-eligible candidates → each juror calls cast_jury_votefinalize_jury_verdict returns the award split
  • Appeal: call flag_disputeappeal_dispute (with bond) → admin calls resolve_appeal (bond returned, dispute resolved with new split)
  • Fee buyback: configure DEX + SKILL token + enable buyback → call convert_fees_to_skill with a fee amount and expected output
  • Surge: enable surge, assign expert to category, seed CategoryActiveSessions, call start_session_with_surge with surge_accepted = true; test rejection when surge_accepted = false and surge is active

Summary by CodeRabbit

  • New Features

    • Added jury-based dispute resolution with panel selection, voting, and final verdicts.
    • Added an appeals flow with filing, resolution, and bond handling.
    • Added fee buyback support to convert fees into the governance token when enabled.
    • Added surge pricing controls and session-aware pricing adjustments for experts.
  • Bug Fixes

    • Improved dispute handling with clearer validation and safer finalization rules.
    • Added additional safeguards for voting, appeals, swaps, and pricing configuration.

…icing (LightForgeHub#284–287)

Implements four new SkillSphere features as Soroban contract extensions:

Issue LightForgeHub#284 — Jury Selection
- JuryVoteRecord contracttype with jurors, vote counts, and voted tracker
- select_jury(): shuffles reputation-filtered candidates via on-chain PRNG
- cast_jury_vote(): per-juror vote with duplicate-vote guard
- finalize_jury_verdict(): majority or unanimous verdict → 9000/1000 bps split (50/50 on tie)
- Public functions: select_jury, cast_jury_vote, finalize_jury_verdict, get_jury_session, set_jury_size
- New DataKey variants: JurySession(u64), JurySize
- New Error codes: JuryNotSelected(70), JuryAlreadyVoted(71), JuryVotingClosed(72), InsufficientCandidates(73)

Issue LightForgeHub#285 — Appeal Mechanism
- AppealRecord contracttype with appellant, bond, resolution fields
- appeal_dispute(): collects configurable bond from appellant before admin resolves
- resolve_appeal(): admin sets new split, delegates to resolve_dispute_internal, refunds bond
- Public functions: appeal_dispute, resolve_appeal, get_appeal, set_appeal_bond_amount
- New DataKey variants: Appeal(u64), AppealBondAmount
- New Error codes: AppealBondRequired(74), AppealAlreadyFiled(75), AppealNotFound(76)

Issue LightForgeHub#286 — Fee Auto-Conversion (SKILL buyback)
- convert_fees_to_skill(): reuses existing cross_contract_swap + check_slippage from dex.rs
- Configurable via SkillTokenAddress, FeeBuybackEnabled, FeeBuybackSlippageBps DataKeys
- Public functions: set_skill_token, set_fee_buyback_enabled, set_fee_buyback_slippage, convert_fees_to_skill
- New Error codes: SkillTokenNotSet(78), BuybackDisabled(79)

Issue LightForgeHub#287 — Surge Pricing
- SurgeConfig contracttype (threshold, max multiplier, step sessions, step bps)
- get_surge_multiplier_bps(): reads CategoryActiveSessions and computes stepped multiplier
- start_session_with_surge(): rejects unconsented surge, increments category counter
- end_surge_session(): participant-called decrement after session ends
- set_expert_category(), increment/decrement_category_sessions() helpers in reputation.rs
- New DataKey variants: CategoryActiveSessions(String), SurgePricingEnabled, ExpertCategory(Address), SurgeConfig
- New Error code: SurgeNotAccepted(82)

Closes LightForgeHub#284
Closes LightForgeHub#285
Closes LightForgeHub#286
Closes LightForgeHub#287
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The contract adds jury selection and voting for disputes, an appeal flow with bond handling, fee-to-SKILL conversion controls, and surge-pricing state plus session hooks. Public APIs, storage keys, error codes, and event symbols were expanded to support these flows.

Changes

Core contract extensions

Layer / File(s) Summary
Shared storage and event surface
contracts/src/lib.rs, contracts/src/errors.rs, contracts/src/events.rs
DataKey gains storage keys for jury, appeal, fee buyback, and surge pricing; Error adds new jury/appeal/fee/surge codes; event_type adds symbols for the new emitted events.
Jury selection and voting
contracts/src/disputes.rs, contracts/src/lib.rs
Jury candidate filtering, random panel selection, juror voting, verdict finalization, and jury-related public entry points are added.
Appeal filing and resolution
contracts/src/disputes.rs, contracts/src/lib.rs
Appeal records, bond handling, appeal filing, appeal resolution, and appeal-related public entry points are added.
Fee buyback conversion
contracts/src/treasury.rs, contracts/src/lib.rs
Conditional fee-to-SKILL swap logic and the associated configuration and conversion entry points are added.
Surge pricing state and session hooks
contracts/src/reputation.rs, contracts/src/lib.rs
Surge configuration, expert category tracking, multiplier computation, and surge-aware session start/end entry points are added.

Sequence Diagram(s)

Jury flow

sequenceDiagram
  participant SkillSphereContract
  participant select_jury as "disputes::select_jury"
  participant Juror
  participant cast_jury_vote as "disputes::cast_jury_vote"
  participant finalize_jury_verdict as "disputes::finalize_jury_verdict"

  SkillSphereContract->>select_jury: select jury panel
  Juror->>cast_jury_vote: cast vote
  SkillSphereContract->>finalize_jury_verdict: close vote
  finalize_jury_verdict-->>SkillSphereContract: seeker_bps, expert_bps
Loading

Appeal flow

sequenceDiagram
  participant Appellant
  participant SkillSphereContract
  participant appeal_dispute as "disputes::appeal_dispute"
  participant resolve_appeal as "disputes::resolve_appeal"

  Appellant->>SkillSphereContract: appeal_dispute(...)
  SkillSphereContract->>appeal_dispute: store AppealRecord + bond
  SkillSphereContract->>resolve_appeal: resolve_appeal(...)
  resolve_appeal->>SkillSphereContract: resolve_dispute_internal(...)
Loading

Fee buyback flow

sequenceDiagram
  participant SkillSphereContract
  participant convert_fees_to_skill as "treasury::convert_fees_to_skill"
  participant cross_contract_swap as "dex::cross_contract_swap"

  SkillSphereContract->>convert_fees_to_skill: convert fees
  convert_fees_to_skill->>cross_contract_swap: swap source_token for SKILL
  cross_contract_swap-->>convert_fees_to_skill: received amount
  convert_fees_to_skill-->>SkillSphereContract: fee_buyback event
Loading

Surge pricing flow

sequenceDiagram
  participant SkillSphereContract
  participant get_surge_multiplier_bps as "reputation::get_surge_multiplier_bps"
  participant start_session_with_surge
  participant increment_category_sessions
  participant end_surge_session
  participant decrement_category_sessions

  SkillSphereContract->>get_surge_multiplier_bps: read multiplier
  SkillSphereContract->>start_session_with_surge: start session
  start_session_with_surge->>increment_category_sessions: track active session
  SkillSphereContract->>end_surge_session: end session
  end_surge_session->>decrement_category_sessions: reduce active count
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Poem

Hop-hop, the jury drums are set,
and appeal bonds glint like dewdrop wet.
SKILL goes zipping through the swap,
while surgey sessions start and stop. 🐇
I twirl my ears—what a tidy net!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issues #284#286 are only partially met: jury compensation and appeal escalation/48h logic are missing, and fee buyback doesn't show treasury transfer. Add automatic 3/5-juror selection at dispute open, compensate jurors from escrow, enforce the 48-hour appeal path with escalation and bond redistribution, and transfer converted SKILL to treasury.
✅ 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 clearly summarizes the four feature areas added in the PR.
Out of Scope Changes check ✅ Passed The added code stays within the four requested feature areas and their supporting storage, events, and error codes.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/src/disputes.rs`:
- Around line 201-209: The juror candidate filtering in the dispute selection
flow only checks reputation, so inactive or unregistered experts can still be
selected. Update the eligibility logic in the candidate loop in the dispute
handling code to also require an active registration state and an available
availability_status from SkillSphereContract::expert_profile before pushing
candidates into eligible, so only active eligible experts are considered for
juror selection.
- Around line 192-220: The jury selection logic in the dispute resolver must
enforce a valid panel size and remove duplicate addresses before drawing jurors.
Update the size resolution in the jury selection flow so the selected value is
constrained to the supported 3-or-5 juror counts, and deduplicate the
`candidates` input before filtering by reputation and shuffling. Make the fix in
the selection routine that builds `eligible` and `jurors` so repeated addresses
cannot occupy multiple seats and an invalid `size` cannot produce a
zero/too-small panel.
- Around line 343-355: The jury verdict finalization currently only persists the
jury session and emits the verdict event, but it does not actually settle the
dispute outcome or distribute rewards. Update the finalization flow that writes
DataKey::JurySession and calls events::publish_event so it also applies the
verdict to the underlying dispute escrow, releases the correct funds based on
seeker_bps and expert_bps, and pays out the jurors’ required share before
returning the result.
- Around line 480-490: The appeal resolution flow currently calls
resolve_dispute_internal, which rejects already resolved disputes, so the
post-ruling appeal path needs to be reworked in the dispute resolution logic.
Update the appeal-handling code in the dispute resolution function to apply the
ruling without relying on the already-finalized dispute state, and add explicit
handling for failed appeals so the bond is not always returned. Use the existing
dispute/bond fields and the token::Client transfer path to route the bond
appropriately depending on the appeal outcome.
- Around line 399-422: The appeal flow in appeal_dispute is missing validation
for dispute ownership and timing, so add checks before creating the appeal
record: verify the dispute exists, confirm appellant is one of the dispute
parties, and ensure the appeal is still within the 48-hour window. Use the
existing appeal_dispute path and DataKey::Appeal handling to locate the spot,
then reject unauthorized or late appeals before the first has been filed so a
random authenticated address cannot lock out the real party.

In `@contracts/src/lib.rs`:
- Around line 6377-6384: The manual admin-only buyback path in
convert_fees_to_skill is not enough to ensure automatic fee-to-SKILL conversion,
so wire the conversion into the existing fee-collection flow instead of relying
on this standalone entrypoint. Update the fee routing/collection logic that
records or forwards fees so it can invoke treasury::convert_fees_to_skill with
the collected token and expected output when fees arrive, while keeping
convert_fees_to_skill as an admin-only fallback if needed.
- Around line 6461-6466: The end_surge_session flow in Self::end_surge_session
currently decrements the surge/session counter without verifying that the
session actually completed and was previously started via
start_session_with_surge. Add state checks so only a real finished surge-tracked
session can trigger reputation::decrement_category_sessions, and make the
operation idempotent by preventing repeated decrements for the same session. Use
the existing session lookup and participant/auth checks in end_surge_session,
plus whatever session status/flag is tracked in the session model, to gate the
decrement.
- Around line 6423-6455: The surge flow in start_session_with_surge currently
only checks acceptance and emits an event, but pricing still comes from
start_session’s original profile.rate_per_second, so the multiplier is never
applied. Update the session creation path used by start_session_with_surge so
the surge-adjusted rate is actually stored or passed through, and make sure
start_session cannot be used to bypass the surge enforcement by calling it
directly. Use the start_session_with_surge and start_session symbols to locate
the shared session-creation logic and apply the multiplier there.
- Around line 6367-6371: `set_fee_buyback_slippage` currently stores any `u32`,
so update this setter to validate and reject values above 100 bps before
persisting them. Use `Self::require_admin` and the
`DataKey::FeeBuybackSlippageBps` write in `set_fee_buyback_slippage` as the
place to enforce the cap, and return the appropriate `Error` when the limit is
exceeded so `convert_fees_to_skill` can only read a bounded value.

In `@contracts/src/reputation.rs`:
- Around line 240-243: The set_surge_config function currently persists
SurgeConfig without validation, allowing invalid surge parameters to be stored.
Add checks in set_surge_config before env.storage().instance().set to enforce
the required bounds for max_multiplier_bps, threshold_sessions, step_sessions,
and step_bps, and reject any config that would make surge a discount or exceed
the allowed cap. Use the existing SurgeConfig and DataKey::SurgeConfig symbols
to locate the persistence path and ensure only validated configs are saved.

In `@contracts/src/treasury.rs`:
- Around line 123-147: The swap flow in the treasury buyback path leaves
accounting and custody out of sync: `cross_contract_swap` returns the received
SKILL, but the value is only checked and logged by `events::publish_event`
without updating `TreasuryBalance(source_token)` or moving the SKILL into
treasury control. In the buyback function that performs
`dex::cross_contract_swap`, add the missing post-swap treasury accounting and
transfer/credit step so the received SKILL is delivered to the community
treasury and the balance state stays consistent before returning `received`.
🪄 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: 59b9e259-06eb-4545-bdfc-3e88c83f51d3

📥 Commits

Reviewing files that changed from the base of the PR and between a4b46d0 and 66c27b7.

📒 Files selected for processing (6)
  • contracts/src/disputes.rs
  • contracts/src/errors.rs
  • contracts/src/events.rs
  • contracts/src/lib.rs
  • contracts/src/reputation.rs
  • contracts/src/treasury.rs

Comment thread contracts/src/disputes.rs
Comment on lines +192 to +220
let size = if jury_size == 0 {
env.storage()
.instance()
.get(&DataKey::JurySize)
.unwrap_or(DEFAULT_JURY_SIZE)
} else {
jury_size
};

// Filter candidates by minimum reputation threshold.
let mut eligible: Vec<Address> = Vec::new(env);
let cand_len = candidates.len();
for i in 0..cand_len {
let candidate = candidates.get(i).unwrap();
let profile = SkillSphereContract::expert_profile(env, candidate.clone());
if profile.reputation >= MIN_JURY_CANDIDATE_REPUTATION {
eligible.push_back(candidate);
}
}

if eligible.len() < size {
return Err(Error::InsufficientCandidates);
}

// Shuffle eligible candidates in-place using on-chain PRNG, then take the first `size`.
env.prng().shuffle(&mut eligible);
let mut jurors: Vec<Address> = Vec::new(env);
for i in 0..size {
jurors.push_back(eligible.get(i).unwrap());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate jury size and de-duplicate candidates before selection.

The resolved size can be 0, 1, 2, or any large value, and duplicate addresses in candidates can be selected multiple times. That breaks the required 3-or-5 juror panel and can make voting impossible when one address occupies multiple seats.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/disputes.rs` around lines 192 - 220, The jury selection logic
in the dispute resolver must enforce a valid panel size and remove duplicate
addresses before drawing jurors. Update the size resolution in the jury
selection flow so the selected value is constrained to the supported 3-or-5
juror counts, and deduplicate the `candidates` input before filtering by
reputation and shuffling. Make the fix in the selection routine that builds
`eligible` and `jurors` so repeated addresses cannot occupy multiple seats and
an invalid `size` cannot produce a zero/too-small panel.

Comment thread contracts/src/disputes.rs
Comment on lines +201 to +209
// Filter candidates by minimum reputation threshold.
let mut eligible: Vec<Address> = Vec::new(env);
let cand_len = candidates.len();
for i in 0..cand_len {
let candidate = candidates.get(i).unwrap();
let profile = SkillSphereContract::expert_profile(env, candidate.clone());
if profile.reputation >= MIN_JURY_CANDIDATE_REPUTATION {
eligible.push_back(candidate);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter for active eligible experts, not just raw reputation.

The selection path ignores availability_status and registration state, so inactive/unregistered experts can be selected as jurors and stall dispute voting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/disputes.rs` around lines 201 - 209, The juror candidate
filtering in the dispute selection flow only checks reputation, so inactive or
unregistered experts can still be selected. Update the eligibility logic in the
candidate loop in the dispute handling code to also require an active
registration state and an available availability_status from
SkillSphereContract::expert_profile before pushing candidates into eligible, so
only active eligible experts are considered for juror selection.

Comment thread contracts/src/disputes.rs
Comment on lines +343 to +355
record.finalized = true;
env.storage()
.persistent()
.set(&DataKey::JurySession(dispute_id), &record);

events::publish_event(
env,
events::event_type::jury_verdict(),
dispute_id,
(dispute_id, seeker_bps, expert_bps),
);

Ok((seeker_bps, expert_bps))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply the jury verdict to the dispute escrow and juror rewards.

Finalization only stores the jury record and returns (seeker_bps, expert_bps); it does not resolve the underlying dispute, release escrow, or pay jurors their required share.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/disputes.rs` around lines 343 - 355, The jury verdict
finalization currently only persists the jury session and emits the verdict
event, but it does not actually settle the dispute outcome or distribute
rewards. Update the finalization flow that writes DataKey::JurySession and calls
events::publish_event so it also applies the verdict to the underlying dispute
escrow, releases the correct funds based on seeker_bps and expert_bps, and pays
out the jurors’ required share before returning the result.

Comment thread contracts/src/disputes.rs
Comment on lines +399 to +422
pub fn appeal_dispute(
env: &Env,
appellant: Address,
dispute_id: u64,
bond_token: Address,
) -> Result<(), Error> {
appellant.require_auth();

if env
.storage()
.persistent()
.has(&DataKey::Appeal(dispute_id))
{
return Err(Error::AppealAlreadyFiled);
}

let bond = appeal_bond_amount(env);
if bond > 0 {
let token_client = token::Client::new(env, &bond_token);
if token_client.balance(&appellant) < bond {
return Err(Error::AppealBondRequired);
}
token_client.transfer(&appellant, &env.current_contract_address(), &bond);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Authorize appeals against real disputes within the appeal window.

appeal_dispute lets any authenticated address file the first appeal for any dispute_id, without verifying the dispute exists, the appellant is a party, or the 48-hour appeal window is open. This can block the legitimate party with AppealAlreadyFiled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/disputes.rs` around lines 399 - 422, The appeal flow in
appeal_dispute is missing validation for dispute ownership and timing, so add
checks before creating the appeal record: verify the dispute exists, confirm
appellant is one of the dispute parties, and ensure the appeal is still within
the 48-hour window. Use the existing appeal_dispute path and DataKey::Appeal
handling to locate the spot, then reject unauthorized or late appeals before the
first has been filed so a random authenticated address cannot lock out the real
party.

Comment thread contracts/src/disputes.rs
Comment on lines +480 to +490
// Apply the new ruling to the underlying dispute.
SkillSphereContract::resolve_dispute_internal(env, dispute_id, seeker_award_bps)?;

// Return bond to appellant now that the appeal has been resolved.
if record.bond_amount > 0 {
let token_client = token::Client::new(env, &record.bond_token);
token_client.transfer(
&env.current_contract_address(),
&record.appellant,
&record.bond_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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Rework appeal resolution for resolved disputes and failed bonds.

resolve_dispute_internal rejects already resolved disputes, so it cannot apply a post-ruling appeal. The bond is also always returned, with no way to redistribute it when the appeal fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/disputes.rs` around lines 480 - 490, The appeal resolution flow
currently calls resolve_dispute_internal, which rejects already resolved
disputes, so the post-ruling appeal path needs to be reworked in the dispute
resolution logic. Update the appeal-handling code in the dispute resolution
function to apply the ruling without relying on the already-finalized dispute
state, and add explicit handling for failed appeals so the bond is not always
returned. Use the existing dispute/bond fields and the token::Client transfer
path to route the bond appropriately depending on the appeal outcome.

Comment thread contracts/src/lib.rs
Comment on lines +6377 to +6384
pub fn convert_fees_to_skill(
env: Env,
fee_amount: i128,
source_token: Address,
expected_skill_out: i128,
) -> Result<i128, Error> {
Self::require_admin(&env)?;
treasury::convert_fees_to_skill(&env, fee_amount, source_token, expected_skill_out)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wire buybacks into fee collection instead of only exposing a manual admin call.

The objective is automatic fee-to-SKILL conversion, but this entrypoint only lets an admin manually trigger conversion; existing fee routing can still send/record fees without invoking the buyback path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/lib.rs` around lines 6377 - 6384, The manual admin-only buyback
path in convert_fees_to_skill is not enough to ensure automatic fee-to-SKILL
conversion, so wire the conversion into the existing fee-collection flow instead
of relying on this standalone entrypoint. Update the fee routing/collection
logic that records or forwards fees so it can invoke
treasury::convert_fees_to_skill with the collected token and expected output
when fees arrive, while keeping convert_fees_to_skill as an admin-only fallback
if needed.

Comment thread contracts/src/lib.rs
Comment on lines +6423 to +6455
pub fn start_session_with_surge(
env: Env,
seeker: Address,
expert: Address,
token: Address,
amount: i128,
min_reputation: u32,
metadata_cid: String,
surge_accepted: bool,
) -> u64 {
seeker.require_auth();
if Self::protocol_paused(&env) || Self::is_emergency_paused(&env) {
panic_with_error!(&env, Error::ProtocolPaused);
}

let multiplier = reputation::get_surge_multiplier_bps(&env, &expert);
if multiplier > 10_000 && !surge_accepted {
panic_with_error!(&env, Error::SurgeNotAccepted);
}

if multiplier > 10_000 {
events::publish_event(
&env,
events::event_type::surge_accepted(),
0,
(expert.clone(), multiplier, seeker.clone()),
);
}

reputation::increment_category_sessions(&env, &expert);

Self::start_session(env, seeker, expert, token, amount, min_reputation, metadata_cid)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply the surge multiplier and prevent bypass through start_session.

This wrapper checks acceptance and emits an event, but it delegates to start_session, which stores the original profile.rate_per_second; the multiplier never changes pricing. Because start_session remains public, callers can also skip this wrapper entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/lib.rs` around lines 6423 - 6455, The surge flow in
start_session_with_surge currently only checks acceptance and emits an event,
but pricing still comes from start_session’s original profile.rate_per_second,
so the multiplier is never applied. Update the session creation path used by
start_session_with_surge so the surge-adjusted rate is actually stored or passed
through, and make sure start_session cannot be used to bypass the surge
enforcement by calling it directly. Use the start_session_with_surge and
start_session symbols to locate the shared session-creation logic and apply the
multiplier there.

Comment thread contracts/src/lib.rs
Comment on lines +6461 to +6466
pub fn end_surge_session(env: Env, caller: Address, session_id: u64) -> Result<(), Error> {
caller.require_auth();
let session = Self::get_session_or_error(&env, session_id)?;
Self::require_participant(&session, &caller)?;
reputation::decrement_category_sessions(&env, &session.expert);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Tie surge counter decrements to real session completion.

Any participant can call end_surge_session immediately, repeatedly, or for sessions not counted by start_session_with_surge, which lets active-session counters drift below reality and suppress surge pricing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/lib.rs` around lines 6461 - 6466, The end_surge_session flow in
Self::end_surge_session currently decrements the surge/session counter without
verifying that the session actually completed and was previously started via
start_session_with_surge. Add state checks so only a real finished surge-tracked
session can trigger reputation::decrement_category_sessions, and make the
operation idempotent by preventing repeated decrements for the same session. Use
the existing session lookup and participant/auth checks in end_surge_session,
plus whatever session status/flag is tracked in the session model, to gate the
decrement.

Comment on lines +240 to +243
pub fn set_surge_config(env: &Env, config: SurgeConfig) {
env.storage()
.instance()
.set(&DataKey::SurgeConfig, &config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate surge config before persisting it.

max_multiplier_bps can be set below 10_000 or above the required 20_000 cap, so surge can become a discount or exceed 2×. Validate max_multiplier_bps, threshold_sessions, step_sessions, and step_bps here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/reputation.rs` around lines 240 - 243, The set_surge_config
function currently persists SurgeConfig without validation, allowing invalid
surge parameters to be stored. Add checks in set_surge_config before
env.storage().instance().set to enforce the required bounds for
max_multiplier_bps, threshold_sessions, step_sessions, and step_bps, and reject
any config that would make surge a discount or exceed the allowed cap. Use the
existing SurgeConfig and DataKey::SurgeConfig symbols to locate the persistence
path and ensure only validated configs are saved.

Comment thread contracts/src/treasury.rs
Comment on lines +123 to +147
let path: Vec<Address> = Vec::new(env);
let received = dex::cross_contract_swap(
env,
&dex_contract,
&source_token,
&skill_token,
&path,
fee_amount,
);

if received <= 0 {
return Err(Error::SwapFailed);
}

dex::check_slippage(expected_skill_out, received, slippage_bps)?;

events::publish_event(
env,
events::event_type::fee_buyback(),
0,
(source_token, skill_token, fee_amount, received),
);

Ok(received)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Update treasury accounting and deliver received SKILL to the treasury.

After swapping, the function leaves TreasuryBalance(source_token) unchanged and keeps the received SKILL in the contract instead of transferring or crediting it to the community treasury. That can make later treasury withdrawals/accounting inconsistent with actual token balances.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/treasury.rs` around lines 123 - 147, The swap flow in the
treasury buyback path leaves accounting and custody out of sync:
`cross_contract_swap` returns the received SKILL, but the value is only checked
and logged by `events::publish_event` without updating
`TreasuryBalance(source_token)` or moving the SKILL into treasury control. In
the buyback function that performs `dex::cross_contract_swap`, add the missing
post-swap treasury accounting and transfer/credit step so the received SKILL is
delivered to the community treasury and the balance state stays consistent
before returning `received`.

@Luluameh Luluameh merged commit fddbd0e into LightForgeHub:main Jun 25, 2026
1 check 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

2 participants