feat: jury selection, appeal mechanism, fee auto-conversion, and surge pricing (#284–287)#294
Conversation
…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
📝 WalkthroughWalkthroughThe 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. ChangesCore contract extensions
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
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(...)
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
contracts/src/disputes.rscontracts/src/errors.rscontracts/src/events.rscontracts/src/lib.rscontracts/src/reputation.rscontracts/src/treasury.rs
| 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()); |
There was a problem hiding this comment.
🎯 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.
| // 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); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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)) |
There was a problem hiding this comment.
🎯 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🔒 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.
| // 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, | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| 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) |
There was a problem hiding this comment.
🎯 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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(()) |
There was a problem hiding this comment.
🎯 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.
| pub fn set_surge_config(env: &Env, config: SurgeConfig) { | ||
| env.storage() | ||
| .instance() | ||
| .set(&DataKey::SurgeConfig, &config); |
There was a problem hiding this comment.
🎯 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🗄️ 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`.
Closes #284
Closes #285
Closes #286
Closes #287
What changed
Issue #284 — Jury Selection
JuryVoteRecord#[contracttype]struct storing the jury panel, per-side vote counts, and avotedlist to prevent double-votingselect_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 firstjury_sizefrom the shuffled listcast_jury_vote(juror, dispute_id, vote_for_seeker)— juror-signed; rejects non-panel addresses and duplicate votesfinalize_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)JurySession(u64),JurySizeJuryNotSelected(70),JuryAlreadyVoted(71),JuryVotingClosed(72),InsufficientCandidates(73)Issue #285 — Appeal Mechanism
AppealRecord#[contracttype]struct with appellant address, bond amount/token, timestamps, and ruling fieldsappeal_dispute(appellant, dispute_id, bond_token)— filed while dispute is still open; collects a configurable bond held in contract escrowresolve_appeal(dispute_id, seeker_award_bps, expert_award_bps)— admin-only; delegates toresolve_dispute_internalwith the new split and returns the bond to the appellantAppeal(u64),AppealBondAmountAppealBondRequired(74),AppealAlreadyFiled(75),AppealNotFound(76)Issue #286 — Fee Auto-Conversion (SKILL buyback)
convert_fees_to_skill(fee_amount, source_token, expected_skill_out)intreasury.rs— reuses the existingdex::cross_contract_swapanddex::check_slippagehelpers; slippage defaults to 100 bpsset_skill_token,set_fee_buyback_enabled,set_fee_buyback_slippageSkillTokenAddress,FeeBuybackEnabled,FeeBuybackSlippageBpsSkillTokenNotSet(78),BuybackDisabled(79)Issue #287 — Surge Pricing
SurgeConfig#[contracttype]struct (threshold_sessions,max_multiplier_bps,step_sessions,step_bps)get_surge_multiplier_bps(expert)— readsCategoryActiveSessionsfor the expert's category and computes a stepped multiplier, capped atmax_multiplier_bpsstart_session_with_surge(…, surge_accepted)— wrapsstart_session; panics withSurgeNotAcceptedif surge is active andsurge_accepted = false; incrementsCategoryActiveSessionson successend_surge_session(caller, session_id)— participant-callable decrement after a session endsset_surge_config,set_surge_pricing_enabled,set_expert_categoryCategoryActiveSessions(String),SurgePricingEnabled,ExpertCategory(Address),SurgeConfigSurgeNotAccepted(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
flag_dispute→select_jurywith ≥3 reputation-eligible candidates → each juror callscast_jury_vote→finalize_jury_verdictreturns the award splitflag_dispute→appeal_dispute(with bond) → admin callsresolve_appeal(bond returned, dispute resolved with new split)convert_fees_to_skillwith a fee amount and expected outputCategoryActiveSessions, callstart_session_with_surgewithsurge_accepted = true; test rejection whensurge_accepted = falseand surge is activeSummary by CodeRabbit
New Features
Bug Fixes