Skip to content

Feat/contract optimizations#151

Merged
Luluameh merged 2 commits into
LightForgeHub:mainfrom
Agbeleshe:feat/contract-optimizations
Apr 26, 2026
Merged

Feat/contract optimizations#151
Luluameh merged 2 commits into
LightForgeHub:mainfrom
Agbeleshe:feat/contract-optimizations

Conversation

@Agbeleshe
Copy link
Copy Markdown
Contributor

@Agbeleshe Agbeleshe commented Apr 26, 2026

PR Summary: SkillSphere Contract Optimizations

This PR focuses on gas optimization and security integration for the SkillSphere smart contract.

Key Changes

1. Gas-Optimized Data Packing (#148)

  • Updated Session, Dispute, and UpgradeTimelock structures to use u32 for timestamps instead of u64.
  • Significantly reduces storage costs for persistent and instance data.
  • Refactored contract logic to handle type conversions and ensure accurate per-second streaming calculations.

2. Integration with Stellar SEP-10 for Auth (#147)

  • Audited require_auth() usage to ensure all entry points are compatible with SEP-10 web auth.
  • Added comprehensive documentation to the contract's README explaining the auth flow and signing requirements for frontend integration.

Closes

Summary by CodeRabbit

  • Documentation

    • Updated README documentation with comprehensive explanation of Soroban-based authorization, including SEP-10 challenge verification process, wallet-signed identity validation, and cross-contract authorization propagation mechanics.
  • Refactor

    • Optimized timestamp field storage across core contract data structures to enhance memory efficiency.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Documentation updated to describe SEP-10-based authorization using wallet-signed challenges and require_auth() verification. Simultaneously, timestamp fields in three structs were optimized from u64 to u32 with corresponding arithmetic conversions throughout the contract implementation.

Changes

Cohort / File(s) Summary
Documentation Update
contracts/README.md
Replaced role-based authorization description with SEP-10-focused explanation covering wallet-signed challenges, require_auth() verification, cross-contract authorization propagation, and actor-specific transaction signing requirements.
Timestamp Optimization
contracts/src/lib.rs
Converted timestamp fields in Dispute, UpgradeTimelock, and Session structs from u64 to u32; updated all write sites with casts from env.ledger().timestamp() and adjusted arithmetic/comparisons to handle u32/u64 conversions where needed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

Poem

🐰 Hop, skip, and a SEP-10 leap,
Wallets sign while contracts keep,
Timestamps shrink from sixty-four,
To thirty-two—gas costs less sore,
Auth flows clear, storage packed tight!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive Title is generic and lacks specificity about the actual changes (SEP-10 auth integration and u32 timestamp optimization for gas efficiency). Consider revising to more specific title like 'Implement SEP-10 auth integration and optimize timestamps with u32 for gas efficiency' to better convey the primary changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All coding requirements from both issues are met: README documents SEP-10 auth with require_auth usage and signing flow; timestamp fields in Session, Dispute, and UpgradeTimelock are converted from u64 to u32.
Out of Scope Changes check ✅ Passed All changes directly address the two linked issues; no extraneous modifications or unrelated feature additions detected in contracts/README.md and contracts/src/lib.rs.

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

Caution

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

⚠️ Outside diff range comments (4)
contracts/README.md (1)

62-108: ⚠️ Potential issue | 🟡 Minor

Struct definitions in README contradict the new lib.rs types.

This PR changes Session.start_timestamp, Session.last_settlement_timestamp, Dispute.created_at, UpgradeTimelock.initiated_at, and UpgradeTimelock.execute_after to u32 in contracts/src/lib.rs, but the README still documents them as u64. Since the README is being touched for the SEP-10 section in this same PR, please update these struct snippets so on-chain users/integrators don't rely on the wrong field widths.

Note: there are additional pre-existing drifts in this section worth fixing while you're here (e.g. Dispute now has evidence_cid / seeker_award_bps / expert_award_bps / auto_resolved instead of ipfs_metadata_hash / resolved / resolution; resolve_dispute now takes a BPS split, not an enum; start_session's documented signature on line 117 is missing min_reputation and metadata_cid and lists rate_per_second which is no longer a parameter).

📝 Proposed timestamp-only fix (the changes directly required by this PR)
 pub struct Session {
     pub id: u64,
     pub seeker: Address,
     pub expert: Address,
     pub token: Address,
     pub rate_per_second: i128,
-    pub start_timestamp: u64,
-    pub last_settlement_timestamp: u64,
+    pub start_timestamp: u32,
+    pub last_settlement_timestamp: u32,
     pub status: SessionStatus,
     pub balance: i128,
     pub accrued_amount: i128,
 }
 pub struct Dispute {
     pub session_id: u64,
     pub reason: String,
     pub ipfs_metadata_hash: String,
-    pub created_at: u64,
+    pub created_at: u32,
     pub resolved: bool,
     pub resolution: u32, // 0=unresolved, 1=SeekerWins, 2=ExpertWins, 3=Refund
 }
 pub struct UpgradeTimelock {
     pub new_wasm_hash: BytesN<32>,
-    pub initiated_at: u64,
-    pub execute_after: u64,
+    pub initiated_at: u32,
+    pub execute_after: u32,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/README.md` around lines 62 - 108, Update the README struct snippets
to match the new on-chain types in contracts/src/lib.rs: change
Session.start_timestamp and Session.last_settlement_timestamp to u32,
Dispute.created_at to u32, and UpgradeTimelock.initiated_at and execute_after to
u32; also reconcile the Dispute fields (replace
ipfs_metadata_hash/resolved/resolution with
evidence_cid/seeker_award_bps/expert_award_bps/auto_resolved) and update the
documented resolve_dispute and start_session signatures to reflect the current
function parameters (resolve_dispute now accepts a BPS split, and start_session
includes min_reputation and metadata_cid and no longer accepts rate_per_second).
contracts/src/lib.rs (3)

963-991: ⚠️ Potential issue | 🔴 Critical

Silent truncation when persisting expiry/effective_time back into u32.

expiry_timestamp_for_session returns a u64 produced via (last_settlement_timestamp as u64).saturating_add(funded_seconds), where funded_seconds = balance / rate_per_second and can become very large for big balances or low rates. Casting that result back with expiry as u32 (line 971) and effective_time as u32 (line 990) silently truncates whenever the value exceeds u32::MAX (~year 2106 in absolute terms, but reachable much sooner for expiry because it adds funded_seconds to "now"). A truncated last_settlement_timestamp will appear as a past time, making subsequent streamed_amount_since calculations stream the entire balance at once and corrupt accounting.

The same pattern exists at line 1038 (close_session) and at every env.ledger().timestamp() as u32 site (lines 566, 652, 775, 849).

🛠 Suggested helper to make the conversion explicit and safe
// Place near other helpers
fn to_u32_timestamp(env: &Env, t: u64) -> u32 {
    if t > u32::MAX as u64 {
        panic_with_error!(env, Error::SessionExpired);
    }
    t as u32
}

Then replace expiry as u32 / effective_time as u32 / env.ledger().timestamp() as u32 with Self::to_u32_timestamp(env, x). At a minimum, clamp with .min(u32::MAX as u64) as u32 so a malicious or malformed session cannot wrap the stored timestamp into the past.

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

In `@contracts/src/lib.rs` around lines 963 - 991, The code silently truncates u64
timestamps to u32 (e.g. expiry as u32 and effective_time as u32), which can wrap
stored timestamps into the past and corrupt accounting; add a helper like
Self::to_u32_timestamp(env: &Env, t: u64) -> u32 that explicitly checks if t >
u32::MAX and either panics via panic_with_error!(env, Error::SessionExpired) or
clamps to u32::MAX, then replace all raw casts (expiry as u32, effective_time as
u32, and direct env.ledger().timestamp() as u32 uses) with
Self::to_u32_timestamp(env, t) in methods such as expiry_timestamp_for_session
usage sites, the settlement block (where session.last_settlement_timestamp is
set), close_session, and other timestamp reads to ensure no silent truncation.

865-877: ⚠️ Potential issue | 🔴 Critical

Fix compile-time type mismatch in execute_upgrade comparison.

At line 875, env.ledger().timestamp() (returns u64) cannot be directly compared with timelock.execute_after (u32 from line 119). Rust does not implicitly coerce between integer types. This will fail to compile.

Fix
-        let now = env.ledger().timestamp();
-        if now < timelock.execute_after {
+        let now = env.ledger().timestamp();
+        if now < timelock.execute_after as u64 {
             return Err(Error::TimelockNotExpired);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 865 - 877, In execute_upgrade, the
comparison between now (env.ledger().timestamp() -> u64) and
timelock.execute_after (u32) causes a type-mismatch; fix by converting one side
to the other's type (e.g., cast timelock.execute_after to u64) before comparing
so the if now < ... check compiles; update the comparison in the execute_upgrade
function referencing the UpgradeTimelock.execute_after field accordingly.

88-93: ⚠️ Potential issue | 🟠 Major

Storage layout change (u64 → u32 timestamps) is a breaking, non-backward-compatible migration.

Switching created_at from u64 to u32 in the Dispute struct (line 88) changes the SCVal layout of persisted entries. The same applies to Session (last_settlement_timestamp and start_timestamp at lines 130–131) and UpgradeTimelock (initiated_at and execute_after at lines 118–119). Any pre-existing entries written with a previous version will fail to decode after upgrade.

No migration or state backfill code exists in the codebase. Before deploying:

  1. Confirm this is a greenfield deployment with no existing state, or
  2. Implement a migration step that rewrites all existing Dispute, Session, and UpgradeTimelock entries to the new layout as part of the upgrade.

Note: The README.md (lines 103–105) documents initiated_at: u64 but the code has u32—documentation is out of sync.

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

In `@contracts/src/lib.rs` around lines 88 - 93, The change from u64 to u32 for
timestamp fields is a storage-layout breaking change; either revert the types
back to u64 on the Dispute struct (created_at), Session
(last_settlement_timestamp, start_timestamp) and UpgradeTimelock (initiated_at,
execute_after) to preserve SCVal compatibility, or add an on-upgrade migration
that iterates existing keys and rewrites stored Dispute, Session, and
UpgradeTimelock entries from the old layout to the new layout before accepting
the new contract code; also update README.md to reflect whichever type you
choose (ensure initiated_at: u64/u32 is consistent).
🧹 Nitpick comments (4)
contracts/src/lib.rs (3)

1078-1095: Repeated as u64 casts on the hot streaming path — extract a helper.

streamed_amount_since and expiry_timestamp_for_session both reconstruct last_settlement_timestamp as u64 inline. Extracting a tiny accessor (e.g. fn last_settlement_u64(s: &Session) -> u64 { s.last_settlement_timestamp as u64 }) makes the intent explicit and centralizes the widening, so a future change to the timestamp type only needs to touch one place.

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

In `@contracts/src/lib.rs` around lines 1078 - 1095, Both streamed_amount_since
and expiry_timestamp_for_session repeatedly cast
session.last_settlement_timestamp to u64; add a tiny helper like fn
last_settlement_u64(s: &Session) -> u64 { s.last_settlement_timestamp as u64 }
and replace each inline cast with calls to last_settlement_u64(&session) (or &s
where applicable) so the widening is centralized; update references in
streamed_amount_since and expiry_timestamp_for_session to use this helper and
keep behavior unchanged.

9-10: Constant types still u64 while the timestamps they're added to are now u32.

TIMELOCK_DURATION and DISPUTE_EXPIRY_WINDOW remain u64 constants. At line 853 you cast TIMELOCK_DURATION as u32 per use; at line 1242 you cast dispute.created_at as u64 to add DISPUTE_EXPIRY_WINDOW. Pinning these constants to the type they're actually used with reduces casting noise and prevents accidental truncation if either constant grows in the future.

♻️ Optional cleanup
-const TIMELOCK_DURATION: u64 = 48 * 60 * 60;
-const DISPUTE_EXPIRY_WINDOW: u64 = 30 * 24 * 60 * 60;
+const TIMELOCK_DURATION: u32 = 48 * 60 * 60;
+const DISPUTE_EXPIRY_WINDOW: u64 = 30 * 24 * 60 * 60; // kept u64: added to a widened u64 timestamp
-            execute_after: now.saturating_add(TIMELOCK_DURATION as u32),
+            execute_after: now.saturating_add(TIMELOCK_DURATION),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/lib.rs` around lines 9 - 10, The constants TIMELOCK_DURATION
and DISPUTE_EXPIRY_WINDOW are declared as u64 but are added to u32 timestamps in
code (you currently cast them at use sites), so change their types to u32 to
match usage, update any dependent arithmetic to u32, and remove the explicit
casts (e.g., the TIMELOCK_DURATION as u32 and the cast of dispute.created_at as
u64) so the additions use matching types and avoid truncation or unnecessary
casting.

1272-1303: Add tests covering the u32 timestamp boundaries.

The optimization touches every timestamp write site, but the test suite only exercises timestamps in the low thousands (around 1_0001_060). Two cases worth covering explicitly:

  1. A session whose expiry_timestamp_for_session would exceed u32::MAX — verify the contract either errors cleanly or saturates instead of silently truncating into a past timestamp.
  2. execute_upgrade invoked with env.ledger().timestamp() set above the timelock — this currently doesn't compile (see comment on lines 865-877), and a test would have caught it.

Want me to draft these tests?

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

In `@contracts/src/lib.rs` around lines 1272 - 1303, Add unit tests that exercise
u32 timestamp boundary behavior: (1) create a session via client.start_session
with a rate and deposit such that expiry_timestamp_for_session would exceed
u32::MAX and assert the contract either returns an explicit error or saturates
(e.g., compare client.get_current_earnings or start_session result against
expected saturated value/error) to ensure no silent wraparound; (2) add a test
that advances env.ledger().set_timestamp above the timelock and then calls
execute_upgrade (or the public wrapper used in tests) to ensure the call
compiles and behaves correctly when ledger timestamp > timelock, asserting the
expected success/failure path. Use existing helpers like setup(),
register_and_avail(), test_cid(), and token::StellarAssetClient::mint() to
construct the scenarios so the new tests mirror test_1_second_session and
test_1_year_session_overflow_check structure.
contracts/README.md (1)

252-266: Clarify SEP-10 vs. Soroban require_auth() distinction in the Authorization & SEP-10 section.

The signer mappings are correct—start_session (seeker), settle_session (expert), and flag_dispute (seeker) all use require_auth() as documented. However, the section conflates two different authorization mechanisms. SEP-10 is an off-chain web authentication protocol that issues JWTs for wallet-server sessions, while Soroban require_auth() is an on-chain mechanism that validates pre-signed authorization entries in transactions. The wording "By leveraging require_auth(), the contract ensures that only the actual owner of a Stellar address can perform sensitive operations" is accurate, but preceding context about SEP-10 could mislead readers into thinking the SEP-10 challenge itself is what require_auth() validates. Consider clarifying that SEP-10 handles frontend/backend authentication, while Soroban auth entries (also wallet-signed) are what require_auth() enforces on-chain.

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

In `@contracts/README.md` around lines 252 - 266, Revise the "Authorization &
SEP-10" section to clearly separate SEP-10 (off-chain web auth that issues JWTs
and validates wallet-server sessions) from Soroban's on-chain authorization
(require_auth() and require_auth_for_args()); explicitly state that SEP-10 is
used by the frontend/backend for login and challenge signing, while
require_auth() enforces wallet-signed auth entries in on-chain transactions, and
update the wording around start_session, settle_session, and flag_dispute to
note those functions call address.require_auth() on-chain (not that SEP-10
itself is validated by require_auth()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@contracts/README.md`:
- Around line 62-108: Update the README struct snippets to match the new
on-chain types in contracts/src/lib.rs: change Session.start_timestamp and
Session.last_settlement_timestamp to u32, Dispute.created_at to u32, and
UpgradeTimelock.initiated_at and execute_after to u32; also reconcile the
Dispute fields (replace ipfs_metadata_hash/resolved/resolution with
evidence_cid/seeker_award_bps/expert_award_bps/auto_resolved) and update the
documented resolve_dispute and start_session signatures to reflect the current
function parameters (resolve_dispute now accepts a BPS split, and start_session
includes min_reputation and metadata_cid and no longer accepts rate_per_second).

In `@contracts/src/lib.rs`:
- Around line 963-991: The code silently truncates u64 timestamps to u32 (e.g.
expiry as u32 and effective_time as u32), which can wrap stored timestamps into
the past and corrupt accounting; add a helper like Self::to_u32_timestamp(env:
&Env, t: u64) -> u32 that explicitly checks if t > u32::MAX and either panics
via panic_with_error!(env, Error::SessionExpired) or clamps to u32::MAX, then
replace all raw casts (expiry as u32, effective_time as u32, and direct
env.ledger().timestamp() as u32 uses) with Self::to_u32_timestamp(env, t) in
methods such as expiry_timestamp_for_session usage sites, the settlement block
(where session.last_settlement_timestamp is set), close_session, and other
timestamp reads to ensure no silent truncation.
- Around line 865-877: In execute_upgrade, the comparison between now
(env.ledger().timestamp() -> u64) and timelock.execute_after (u32) causes a
type-mismatch; fix by converting one side to the other's type (e.g., cast
timelock.execute_after to u64) before comparing so the if now < ... check
compiles; update the comparison in the execute_upgrade function referencing the
UpgradeTimelock.execute_after field accordingly.
- Around line 88-93: The change from u64 to u32 for timestamp fields is a
storage-layout breaking change; either revert the types back to u64 on the
Dispute struct (created_at), Session (last_settlement_timestamp,
start_timestamp) and UpgradeTimelock (initiated_at, execute_after) to preserve
SCVal compatibility, or add an on-upgrade migration that iterates existing keys
and rewrites stored Dispute, Session, and UpgradeTimelock entries from the old
layout to the new layout before accepting the new contract code; also update
README.md to reflect whichever type you choose (ensure initiated_at: u64/u32 is
consistent).

---

Nitpick comments:
In `@contracts/README.md`:
- Around line 252-266: Revise the "Authorization & SEP-10" section to clearly
separate SEP-10 (off-chain web auth that issues JWTs and validates wallet-server
sessions) from Soroban's on-chain authorization (require_auth() and
require_auth_for_args()); explicitly state that SEP-10 is used by the
frontend/backend for login and challenge signing, while require_auth() enforces
wallet-signed auth entries in on-chain transactions, and update the wording
around start_session, settle_session, and flag_dispute to note those functions
call address.require_auth() on-chain (not that SEP-10 itself is validated by
require_auth()).

In `@contracts/src/lib.rs`:
- Around line 1078-1095: Both streamed_amount_since and
expiry_timestamp_for_session repeatedly cast session.last_settlement_timestamp
to u64; add a tiny helper like fn last_settlement_u64(s: &Session) -> u64 {
s.last_settlement_timestamp as u64 } and replace each inline cast with calls to
last_settlement_u64(&session) (or &s where applicable) so the widening is
centralized; update references in streamed_amount_since and
expiry_timestamp_for_session to use this helper and keep behavior unchanged.
- Around line 9-10: The constants TIMELOCK_DURATION and DISPUTE_EXPIRY_WINDOW
are declared as u64 but are added to u32 timestamps in code (you currently cast
them at use sites), so change their types to u32 to match usage, update any
dependent arithmetic to u32, and remove the explicit casts (e.g., the
TIMELOCK_DURATION as u32 and the cast of dispute.created_at as u64) so the
additions use matching types and avoid truncation or unnecessary casting.
- Around line 1272-1303: Add unit tests that exercise u32 timestamp boundary
behavior: (1) create a session via client.start_session with a rate and deposit
such that expiry_timestamp_for_session would exceed u32::MAX and assert the
contract either returns an explicit error or saturates (e.g., compare
client.get_current_earnings or start_session result against expected saturated
value/error) to ensure no silent wraparound; (2) add a test that advances
env.ledger().set_timestamp above the timelock and then calls execute_upgrade (or
the public wrapper used in tests) to ensure the call compiles and behaves
correctly when ledger timestamp > timelock, asserting the expected
success/failure path. Use existing helpers like setup(), register_and_avail(),
test_cid(), and token::StellarAssetClient::mint() to construct the scenarios so
the new tests mirror test_1_second_session and
test_1_year_session_overflow_check structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 791f3663-78bc-4f23-bae5-ba8a769f195c

📥 Commits

Reviewing files that changed from the base of the PR and between 2832b0a and 6792dcf.

📒 Files selected for processing (2)
  • contracts/README.md
  • contracts/src/lib.rs

@Luluameh Luluameh merged commit c6419f3 into LightForgeHub:main Apr 26, 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

Development

Successfully merging this pull request may close these issues.

Gas-Optimized Data Packing Integration with Stellar SEP-10 for Auth

2 participants