Skip to content

feat: add borrower loyalty rewards system - #208

Merged
thisisouvik merged 3 commits into
thisisouvik:mainfrom
JemimahEkong:feat/borrower-loyalty-rewards
Jul 30, 2026
Merged

feat: add borrower loyalty rewards system#208
thisisouvik merged 3 commits into
thisisouvik:mainfrom
JemimahEkong:feat/borrower-loyalty-rewards

Conversation

@JemimahEkong

@JemimahEkong JemimahEkong commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

PR: feat(lending): add borrower loyalty rewards system

This PR introduces a borrower loyalty rewards system that incentivizes timely loan repayments through configurable reward calculations and seamless integration with the lending contract.

What changed

New contract: contracts/borrower_loyalty

Added a dedicated BorrowerLoyaltyContract with the following functionality:

  • initialize
  • set_config
  • set_reward_token
  • distribute_reward
  • calculate_reward_view
  • get_borrower_rewards
  • get_total_rewards_distributed

Reward calculation

Rewards are calculated using the formula:

base_amount × size_multiplier × duration_multiplier × tier_multiplier

All calculations are performed in basis points (bps) using i128 arithmetic for overflow safety.

Supported borrower reputation tiers:

Tier Multiplier
None 0.5×
Beginner
Silver 1.5×
Gold
Platinum

Lending contract integration

Updated contracts/lending/src/lib.rs to integrate the loyalty system:

  • Added LoyaltyClient for cross-contract calls using env.invoke_contract

  • Added new storage keys:

    • DataKey::LoyaltyContract
    • DataKey::LoanReputationTier(u32)
  • Added admin functions:

    • set_loyalty_contract
    • get_loyalty_contract
  • Extended LoanRequestInput with a reputation_tier field

  • Added reward distribution hook during record_payment for eligible full, on-time repayments

Test coverage

Borrower loyalty contract

Added 11 unit tests covering:

  • Reward calculation correctness
  • Reputation tier differentiation
  • Reward scaling across loan sizes and durations
  • Unauthorized access protection
  • Reward pool exhaustion handling

Lending integration

Added integration tests covering:

  • Successful repayments without a configured loyalty contract (graceful fallback)
  • End-to-end reward distribution for eligible borrowers
  • Late repayments receiving no rewards
  • Admin-only loyalty contract configuration

Validation

All verification steps completed successfully:

  • ✅ 11 borrower loyalty contract tests passed
  • ✅ 90 lending contract tests passed
  • ✅ 19 property-based tests passed
  • cargo fmt
  • cargo clippy -D warnings

No regressions or warnings were introduced.

Closes #77

Summary by CodeRabbit

  • New Features

    • Added borrower loyalty rewards based on loan amount, repayment duration, and reputation tier.
    • Lending now supports reputation tiers and can issue rewards for eligible, on-time repayments.
    • Added administration controls for configuring rewards and connecting the lending and loyalty systems.
    • Added reward balances, distribution tracking, and token funding safeguards.
  • Bug Fixes

    • Corrected focus handling in authentication and fiat-withdrawal dialogs.
  • Tests

    • Expanded coverage for reward calculations, permissions, repayment scenarios, limits, and edge cases.

- New borrower_loyalty contract with configurable tier-based rewards
- On-time repayments trigger reward distribution via cross-contract call
- Reward formula: base_amount x size_multiplier x duration_multiplier x tier_multiplier
- Configurable tiers: None(0.5x), Beginner(1x), Silver(1.5x), Gold(2x), Platinum(3x)
- Late repayments receive zero rewards
- i128 arithmetic for overflow safety
- 11 unit tests for loyalty contract, 4 integration tests in lending contract
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@JemimahEkong is attempting to deploy a commit to the thisisouvik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@JemimahEkong Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a Soroban borrower-loyalty rewards contract, integrates reward distribution with timely loan repayment, extends lending requests with reputation tiers, updates related tests and snapshots, and fixes two incomplete FocusTrap JSX elements. Treasury changes are formatting-only.

Changes

Borrower loyalty rewards

Layer / File(s) Summary
Reward contract and configuration
contracts/borrower_loyalty/*
Adds reward configuration, tiered reward calculation, admin controls, token transfers, balance capping, accounting, getters, and validation.
Lending repayment integration
contracts/lending/src/lib.rs, contracts/lending/src/test.rs
Stores reputation tiers, configures the loyalty contract, and invokes reward distribution for timely full repayment.
Contract tests and snapshots
contracts/*/test_snapshots/*
Adds and regenerates fixtures covering reward behavior, loan serialization, repayment flows, collateral scenarios, and flash-loan cases.
UI and treasury maintenance
components/auth/AuthAccessButton.tsx, components/dashboard/WithdrawToFiatButton.tsx, contracts/treasury/*
Closes two FocusTrap elements and reformats treasury storage expressions and related test setup.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Borrower
  participant LendingContract
  participant BorrowerLoyaltyContract
  participant RewardToken
  Borrower->>LendingContract: record_payment
  LendingContract->>BorrowerLoyaltyContract: distribute_reward
  BorrowerLoyaltyContract->>RewardToken: transfer reward tokens
  BorrowerLoyaltyContract-->>LendingContract: return transferred reward
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning FAIL: The PR also includes unrelated treasury and TSX formatting fixes that are outside the borrower loyalty rewards scope. Move the treasury and frontend formatting changes into a separate PR, or remove them from this one to keep scope focused on issue #77.
✅ 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 is concise and accurately summarizes the main change: adding a borrower loyalty rewards system.
Linked Issues check ✅ Passed PASS: The PR implements dynamic borrower rewards, on-time repayment payouts, and tests matching issue #77's requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 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: 6

🧹 Nitpick comments (1)
contracts/lending/src/test.rs (1)

2166-2346: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated setup between the two new loyalty integration tests.

test_repayment_calls_loyalty_contract and test_late_repayment_no_reward repeat the same ~35 lines (register/initialize lending + loyalty, whitelist asset, fund loyalty contract, link set_loyalty_contract). Consider extracting a shared setup_with_loyalty() helper (mirroring the existing setup()/setup_flash_loan() pattern in this file) to reduce duplication and ease future edits to the wiring.

🤖 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/lending/src/test.rs` around lines 2166 - 2346, The two loyalty
integration tests duplicate their lending and loyalty contract wiring setup.
Extract the shared initialization, token funding, and set_loyalty_contract flow
from test_repayment_calls_loyalty_contract and test_late_repayment_no_reward
into a setup_with_loyalty helper matching the existing setup/setup_flash_loan
pattern, then update both tests to use its returned clients and identifiers
while preserving their distinct repayment scenarios.
🤖 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/borrower_loyalty/Cargo.toml`:
- Around line 7-8: Update the [lib] crate-type configuration in
borrower-loyalty’s Cargo.toml to include both cdylib and rlib, preserving Rust
dependency usage while enabling generation of the deployable WASM artifact.

In `@contracts/borrower_loyalty/src/lib.rs`:
- Around line 51-75: Update initialize to call admin.require_auth() before
checking or writing initialization state, using the existing admin parameter and
preserving the current validation and storage behavior.
- Around line 278-285: Update the reputation_tier match in the loyalty
calculation to reject values outside the defined 0–4 tiers instead of returning
ONE_HUNDRED_PERCENT_BPS. Propagate the existing contract error through the
surrounding function so invalid tiers fail the call, while preserving the
configured multipliers for valid tiers.
- Around line 185-226: In the reward distribution flow, compute the transferable
amount once from contract_balance and reward, return immediately when it is
zero, update BorrowerRewards and TotalRewardsDistributed plus the reward event
before calling the external token transfer, then perform a single transfer using
that computed amount. Remove the duplicated balance comparison and preserve the
returned amount as the value actually transferred.

In `@contracts/lending/src/lib.rs`:
- Around line 106-107: The create_loan_request flow must not trust the
borrower-supplied reputation_tier for loyalty rewards. Validate it is within
0..=4 before storing the loan tier, or derive it from
borrower_reputation::get_profile and use that authoritative value for
distribution through borrower_loyalty::distribute_reward; reject invalid or
mismatched tiers.
- Around line 5-34: Update LoyaltyClient::distribute_reward to call
env.try_invoke_contract::<i128, soroban_sdk::Error> instead of invoke_contract,
and handle the returned error by skipping the reward rather than propagating or
panicking. Preserve successful reward values while ensuring record_payment can
persist on-time full repayments even when the loyalty contract fails.

---

Nitpick comments:
In `@contracts/lending/src/test.rs`:
- Around line 2166-2346: The two loyalty integration tests duplicate their
lending and loyalty contract wiring setup. Extract the shared initialization,
token funding, and set_loyalty_contract flow from
test_repayment_calls_loyalty_contract and test_late_repayment_no_reward into a
setup_with_loyalty helper matching the existing setup/setup_flash_loan pattern,
then update both tests to use its returned clients and identifiers while
preserving their distinct repayment scenarios.
🪄 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 Plus

Run ID: 8eb7b9b3-5a30-44fc-bd5b-c70b221623e7

📥 Commits

Reviewing files that changed from the base of the PR and between 70dcd6b and 72ef7e3.

⛔ Files ignored due to path filters (1)
  • contracts/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (84)
  • contracts/Cargo.toml
  • contracts/borrower_loyalty/Cargo.toml
  • contracts/borrower_loyalty/src/lib.rs
  • contracts/borrower_loyalty/src/test.rs
  • contracts/borrower_loyalty/test_snapshots/test/test_admin_can_update_config.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_different_tiers_receive_different_rewards.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_double_initialize_panics.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_eligible_borrower_receives_rewards.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_fund_and_drain_scenario.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_initialize.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_calculation_correctness.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_duration.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_loan_size.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_cannot_distribute.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_gets_zero.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_unauthorized_returns_zero_reward.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_zero_loan_amount_returns_zero.1.json
  • contracts/lending/Cargo.toml
  • contracts/lending/src/lib.rs
  • contracts/lending/src/test.rs
  • contracts/lending/test_snapshots/test/test_approve_loan_blocked_when_paused.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_max_duration_365_days.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_min_duration_1_day.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_request_basic.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_request_checks_borrowing_power.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_request_with_multi_asset_collateral.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_with_fixed_rate_model.1.json
  • contracts/lending/test_snapshots/test/test_create_loan_with_floating_rate_model.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_blocked_when_paused.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_multiple_assets.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_non_whitelisted_asset_rejected.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_same_asset_accumulates.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_single_asset.1.json
  • contracts/lending/test_snapshots/test/test_deposit_collateral_zero_amount_rejected.1.json
  • contracts/lending/test_snapshots/test/test_due_at_computed_correctly.1.json
  • contracts/lending/test_snapshots/test/test_empty_collateral_entries_rejected.1.json
  • contracts/lending/test_snapshots/test/test_failed_flash_loan_rolls_back_pool_balance.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_accepts_overpayment.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_fee_capped.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_rejects_amount_over_pool_liquidity.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_rejects_negative_amount.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_rejects_zero_amount.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_reverts_on_partial_repayment.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_reverts_when_receiver_repays_nothing.1.json
  • contracts/lending/test_snapshots/test/test_flash_loan_success_full_repayment.1.json
  • contracts/lending/test_snapshots/test/test_get_asset_collateral_config_default.1.json
  • contracts/lending/test_snapshots/test/test_get_borrowing_power_multiple_assets.1.json
  • contracts/lending/test_snapshots/test/test_get_borrowing_power_no_collateral.1.json
  • contracts/lending/test_snapshots/test/test_get_borrowing_power_single_asset.1.json
  • contracts/lending/test_snapshots/test/test_get_total_collateral_value.1.json
  • contracts/lending/test_snapshots/test/test_get_total_collateral_value_empty.1.json
  • contracts/lending/test_snapshots/test/test_get_user_collateral_entries_empty_for_new_borrower.1.json
  • contracts/lending/test_snapshots/test/test_interest_calculation_is_correct.1.json
  • contracts/lending/test_snapshots/test/test_late_repayment_no_reward.1.json
  • contracts/lending/test_snapshots/test/test_mark_defaulted_blocked_when_paused.1.json
  • contracts/lending/test_snapshots/test/test_no_overflow_at_maximum_valid_inputs.1.json
  • contracts/lending/test_snapshots/test/test_non_whitelisted_collateral_asset_rejected.1.json
  • contracts/lending/test_snapshots/test/test_only_admin_can_change_flash_loan_fee.1.json
  • contracts/lending/test_snapshots/test/test_platform_fee_is_one_percent_of_interest.1.json
  • contracts/lending/test_snapshots/test/test_pool_usable_again_immediately_after_a_failed_flash_loan.1.json
  • contracts/lending/test_snapshots/test/test_record_payment_allowed_when_paused.1.json
  • contracts/lending/test_snapshots/test/test_repayment_calls_loyalty_contract.1.json
  • contracts/lending/test_snapshots/test/test_repayment_without_loyalty_contract_works.1.json
  • contracts/lending/test_snapshots/test/test_resume_operations_after_unpause.1.json
  • contracts/lending/test_snapshots/test/test_set_asset_collateral_config_clamps_to_bounds.1.json
  • contracts/lending/test_snapshots/test/test_set_asset_collateral_config_non_admin_rejected.1.json
  • contracts/lending/test_snapshots/test/test_set_asset_collateral_config_non_whitelisted_rejected.1.json
  • contracts/lending/test_snapshots/test/test_set_loyalty_contract.1.json
  • contracts/lending/test_snapshots/test/test_set_loyalty_contract_non_admin_rejected.1.json
  • contracts/lending/test_snapshots/test/test_set_price_oracle.1.json
  • contracts/lending/test_snapshots/test/test_set_price_oracle_non_admin_rejected.1.json
  • contracts/lending/test_snapshots/test/test_switch_rate_model_allowed_after_cooldown.1.json
  • contracts/lending/test_snapshots/test/test_switch_rate_model_cooldown_enforced.1.json
  • contracts/lending/test_snapshots/test/test_switch_rate_model_fixed_to_floating.1.json
  • contracts/lending/test_snapshots/test/test_switch_rate_model_only_active.1.json
  • contracts/lending/test_snapshots/test/test_switch_rate_model_only_borrower.1.json
  • contracts/lending/test_snapshots/test/test_update_floating_rate.1.json
  • contracts/lending/test_snapshots/test/test_update_floating_rate_rejects_fixed.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_allowed_with_no_active_loans.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_basic.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_exceeds_balance_rejected.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_full_amount_removes_entry.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_nonexistent_asset_rejected.1.json
  • contracts/lending/test_snapshots/test/test_withdraw_collateral_zero_amount_rejected.1.json

Comment on lines +7 to +8
[lib]
crate-type = ["rlib"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files around borrower_loyalty and lending manifests =="
git ls-files 'contracts/borrower_loyalty/Cargo.toml' 'contracts/lending/Cargo.toml' 'Cargo.toml' 'contracts/*/Cargo.toml' | sort

echo
echo "== borrower_loyalty Cargo.toml =="
cat -n contracts/borrower_loyalty/Cargo.toml

echo
echo "== lending Cargo.toml dependencies/use references =="
sed -n '1,220p' contracts/lending/Cargo.toml
echo
rg -n "borrower_loyalty|BorrowerLoyaltyContractClient|RewardConfig" contracts/lending -S || true

echo
echo "== workspace package list =="
if [ -f Cargo.toml ]; then
  sed -n '1,120p' Cargo.toml
fi

echo
echo "== contract lib structure =="
fd -a 'lib.rs|src' contracts/borrower_loyancy contracts/borrower_loyalty 2>/dev/null || true
fd -a 'lib.rs' contracts 2>/dev/null || true

Repository: thisisouvik/trustlend-stellar

Length of output: 3616


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== borrower_loyalty src/lib.rs top-level symbols =="
sed -n '1,180p' contracts/borrower_loyalty/src/lib.rs
echo
rg -n "struct BorrowerLoyaltyContract|register!(BorrowerLoyaltyContract|impl BorrowerLoyaltyContract|pub fn|#[contract|Error|Type" contracts/borrower_loyalty/src/lib.rs || true

echo
echo "== root Cargo.toml package/workspace =="
cat -n Cargo.toml 2>/dev/null || true

echo
echo "== other contract manifests crate-type and workspace dependency usage =="
python3 - <<'PY'
from pathlib import Path
for p in sorted(Path("contracts").glob("*/Cargo.toml")):
    text=p.read_text()
    lines=[l.strip() for l in text.splitlines()]
    print(f"\n--- {p} ---")
    for i,l in enumerate(lines,1):
        if l.startswith(("name", "crate-type", "soroban", "stellar", "contract")) or "borrower" in l:
            print(f"{i}: {l}")
PY

echo
echo "== Rust/Cargo availability =="
command -v rustc || true
command -v cargo || true
rustc --version 2>&1 || true
cargo --version 2>&1 || true

Repository: thisisouvik/trustlend-stellar

Length of output: 159


🌐 Web query:

Soroban contracts Cargo.toml crate-type cdylib rlib required build wasm

💡 Result:

In Soroban smart contract development, the Cargo.toml file configuration is critical for ensuring the contract builds correctly as a WebAssembly (Wasm) module [1][2]. Crate-Type Configuration: To build a Soroban contract, the Cargo.toml file for the contract must specify the crate-type as cdylib within the [lib] section [2][3]. This configuration is mandatory for the compiler to generate a dynamic library (a.wasm file) that can be deployed to the Soroban network [1][4]. [lib] crate-type = ["cdylib"] The rlib crate-type is generally avoided in contract-specific Cargo.toml files [5]. While rlib is the default Rust library format for creating static libraries that can be imported by other Rust projects, including both cdylib and rlib in the same crate can disable Link Time Optimizations (LTO) [5]. Because LTO is vital for producing smaller, more efficient Wasm files, the recommended practice is to define only cdylib for the contract itself [5]. If you need to share code between contracts or enable testing features like fuzzing that require importing the contract as a library, developers often use advanced build commands (such as cargo rustc) to explicitly set the crate-type at build time, or maintain separate crates for shared logic to avoid multiple crate-types in the contract-producing crate [6][5]. Key Considerations: 1. WASM Compilation: The cdylib setting tells the Rust compiler to produce a dynamic library, which is the necessary format for Soroban deployment [1][3]. 2. LTO Optimization: Using only cdylib ensures that Link Time Optimizations remain active, which helps keep the final.wasm binary size minimized [5]. 3. Build Tools: While standard Cargo can be used, the stellar-cli (stellar contract build) is the recommended tool for compiling contracts as it automates the platform-specific requirements (e.g., targeting wasm32-unknown-unknown) [2][3]. Top Results: [1][2][5][3]

Citations:


🌐 Web query:

soroban sdk contract artifact name contract.wasm c dylib rlib workspace crates "Cargo.toml crate-type" "rlib" "cdylib"

💡 Result:

When developing Soroban smart contracts, the primary artifact is a WebAssembly (.wasm) file [1]. This file contains the contract's logic and interface specifications, and it is the only artifact required for deployment and integration testing [1]. Regarding the configuration in Cargo.toml: 1. crate-type: To generate a WebAssembly binary that the Soroban environment can execute, the crate-type must include cdylib [2][3]. This tells the Rust compiler to produce a dynamically loadable library that exports symbols [3]. While rlib is used for standard Rust library linking (e.g., when a crate is a dependency for another Rust project), cdylib is strictly necessary for the final contract binary targeted at the Wasm runtime [3]. It is common practice to specify both if the crate acts as both a contract and a library dependency: [lib] crate-type = ["cdylib", "rlib"] [4][2] 2. Build Target and Tooling: Soroban contracts must be built for the wasm32v1-none target [5]. While you can manually invoke the compiler, it is highly recommended to use the Stellar CLI (stellar contract build) to build contracts [5]. This tool automatically applies the correct build settings and environment constraints required by the Soroban runtime [5]. You should generally avoid using standard cargo build for the final contract binary to prevent issues with incompatible Wasm features [5]. 3. Workspace: In a cargo workspace, individual contract crates are managed like any other Rust crate, but each contract crate requiring a.wasm output must have its own [lib] configuration in its respective Cargo.toml [2]. 4. Artifact Naming: The resulting.wasm file is typically output in the target directory (e.g., target/wasm32v1-none/release/contract_name.wasm) [1]. While standard builds might produce a.wasm file, developers often rename or optimize these artifacts during the build process using the Stellar CLI or explicit build scripts to ensure they are ready for deployment [2].

Citations:


Add cdylib so `borrower-loyalty) can be built for WASM deployment.

Soroban contracts need cdylib to emit the deployable .wasm artifact. Since contracts/lending) imports this crate as a Rust dependency, keep rlib as well.

🛠️ Proposed fix
 [lib]
-crate-type = ["rlib"]
+crate-type = ["cdylib", "rlib"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[lib]
crate-type = ["rlib"]
[lib]
crate-type = ["cdylib", "rlib"]
🤖 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/borrower_loyalty/Cargo.toml` around lines 7 - 8, Update the [lib]
crate-type configuration in borrower-loyalty’s Cargo.toml to include both cdylib
and rlib, preserving Rust dependency usage while enabling generation of the
deployable WASM artifact.

Comment thread contracts/borrower_loyalty/src/lib.rs
Comment thread contracts/borrower_loyalty/src/lib.rs
Comment thread contracts/borrower_loyalty/src/lib.rs
Comment thread contracts/lending/src/lib.rs
Comment thread contracts/lending/src/lib.rs
…sx errors

- Fix treasury test: update create_loan_request to use LoanRequestInput struct
- Fix clippy: len % 2 == 0 -> is_multiple_of(2) in lending oracle code
- Fix clippy: assert_eq!(bool, false) -> assert!(!bool) in lending test
- Fix tsx: add missing </FocusTrap> closing tags in AuthAccessButton and WithdrawToFiatButton
- require_auth before state check in borrower_loyalty::initialize
- compute transferable amount once, update state before token transfer
- reject invalid reputation_tier (>4) instead of default multiplier
- validate reputation_tier 0..=4 in lending create_loan_request
- use try_invoke_contract in LoyaltyClient to handle contract failures gracefully

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

Caution

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

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

979-993: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Range check doesn't stop borrowers from self-reporting the maximum reputation tier.

reputation_tier is still taken verbatim from the borrower-supplied LoanRequestInput (lines 114-115, 979) and only range-checked (> 4 panics) before being persisted to LoanReputationTier(loan_id) and later used to compute the loyalty reward multiplier (Platinum = 3x). There is no cross-check against the borrower's actual profile (e.g. borrower_reputation::get_profile), so any borrower can simply always request tier 4 and receive the maximum reward multiplier regardless of true reputation — undermining the entire premise of tiered loyalty rewards and letting a borrower drain the reward pool faster than intended.

This addresses only the range portion of the previously flagged concern; the authenticity/trust gap remains.

🔒 Suggested direction

Derive reputation_tier from the borrower's stored profile (via a cross-contract call to borrower_reputation, or by persisting/reading a lending-side authoritative record) rather than trusting the value in LoanRequestInput.

🤖 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/lending/src/lib.rs` around lines 979 - 993, Stop trusting the
borrower-supplied reputation_tier in the loan request: in the loan creation flow
around the request destructuring and validation, derive the tier from the
borrower’s authoritative stored profile using borrower_reputation::get_profile
or an existing lending-side record. Validate and persist only the derived tier
for LoanReputationTier(loan_id) and subsequent loyalty reward calculations,
while retaining the existing range validation for the authoritative value.

1206-1245: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the repayment state before invoking the loyalty client.

In record_payment, the loan is read into a local mut loan, remaining_due and status are updated locally, then LoyaltyClient::distribute_reward is called while the persisted Loan(loan_id) still contains the pre-repayment state. The updated loan is only written at the end of the function. Save the updated loan before the cross-contract payout, and then publish the repayment event after the external call, to preserve the checks-effects-interactions pattern.

🤖 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/lending/src/lib.rs` around lines 1206 - 1245, Update record_payment
to persist the modified Loan(loan_id) before calling
LoyaltyClient::distribute_reward, then emit the repayment event only after the
external call completes. Preserve the existing repayment updates and reward
conditions while ordering storage, cross-contract interaction, and event
publication according to checks-effects-interactions.
🤖 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 `@components/dashboard/WithdrawToFiatButton.tsx`:
- Line 426: Update the FocusTrap configuration in WithdrawToFiatButton so it
remains active while busy instead of using active={!busy}. Ensure the processing
state retains a focusable control or status target inside the modal, preventing
focus from returning to the background trigger while the non-dismissible
aria-modal is open.

In `@contracts/lending/src/lib.rs`:
- Around line 1598-1599: Update the Rust toolchain configuration to pin channel
to Rust 1.87 or newer, ensuring the u32::is_multiple_of call in the lending
implementation remains compatible across CI environments.

---

Outside diff comments:
In `@contracts/lending/src/lib.rs`:
- Around line 979-993: Stop trusting the borrower-supplied reputation_tier in
the loan request: in the loan creation flow around the request destructuring and
validation, derive the tier from the borrower’s authoritative stored profile
using borrower_reputation::get_profile or an existing lending-side record.
Validate and persist only the derived tier for LoanReputationTier(loan_id) and
subsequent loyalty reward calculations, while retaining the existing range
validation for the authoritative value.
- Around line 1206-1245: Update record_payment to persist the modified
Loan(loan_id) before calling LoyaltyClient::distribute_reward, then emit the
repayment event only after the external call completes. Preserve the existing
repayment updates and reward conditions while ordering storage, cross-contract
interaction, and event publication according to checks-effects-interactions.
🪄 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 Plus

Run ID: 313a6992-bfe8-4761-8897-83110df4521d

📥 Commits

Reviewing files that changed from the base of the PR and between 72ef7e3 and 78f3093.

📒 Files selected for processing (23)
  • components/auth/AuthAccessButton.tsx
  • components/dashboard/WithdrawToFiatButton.tsx
  • contracts/borrower_loyalty/src/lib.rs
  • contracts/borrower_loyalty/test_snapshots/test/test_admin_can_update_config.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_different_tiers_receive_different_rewards.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_double_initialize_panics.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_eligible_borrower_receives_rewards.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_fund_and_drain_scenario.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_initialize.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_calculation_correctness.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_duration.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_loan_size.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_gets_zero.1.json
  • contracts/borrower_loyalty/test_snapshots/test/test_zero_loan_amount_returns_zero.1.json
  • contracts/lending/src/lib.rs
  • contracts/lending/src/test.rs
  • contracts/lending/test_snapshots/test/test_late_repayment_no_reward.1.json
  • contracts/lending/test_snapshots/test/test_price_oracle_median_resists_manipulation.1.json
  • contracts/lending/test_snapshots/test/test_price_oracle_twap_fallback_is_used.1.json
  • contracts/lending/test_snapshots/test/test_repayment_calls_loyalty_contract.1.json
  • contracts/treasury/src/lib.rs
  • contracts/treasury/src/test.rs
  • contracts/treasury/test_snapshots/test/test_collect_protocol_fees_from_lending.1.json

</p>
)}
</div>
</FocusTrap>

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 | 🟡 Minor | ⚡ Quick win

Keep focus contained while the withdrawal is busy.

When the flow becomes busy, active={!busy} deactivates FocusTrap; its cleanup restores focus to the background trigger while the non-dismissible aria-modal remains open. Keep the trap active and retain a focusable in-modal control/status target during processing.

🤖 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 `@components/dashboard/WithdrawToFiatButton.tsx` at line 426, Update the
FocusTrap configuration in WithdrawToFiatButton so it remains active while busy
instead of using active={!busy}. Ensure the processing state retains a focusable
control or status target inside the modal, preventing focus from returning to
the background trigger while the non-dismissible aria-modal is open.

Comment on lines 1598 to +1599
let midpoint = len / 2;
let even = len % 2 == 0;
let even = len.is_multiple_of(2);

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Which Rust version stabilized the integer is_multiple_of method (e.g. u32::is_multiple_of)?

💡 Result:

The integer is_multiple_of method for unsigned types (such as u32::is_multiple_of) was stabilized in Rust 1.87.0 [1][2][3]. This method, tracked under the feature name unsigned_is_multiple_of [4][5], was stabilized following the completion of its final comment period and the subsequent stabilization pull request [5]. It provides a way to check if an integer is a multiple of another without panicking on division by zero; specifically, n.is_multiple_of(0) returns true if n is 0, and false otherwise [6][5]. Note that while various third-party crates (such as the num crate) have provided an is_multiple_of method as part of their own Integer traits for some time [7][8][9], the native implementation for standard library unsigned integer types was not stabilized until the 1.87.0 release [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Toolchain/edition/config candidates:"
git ls-files | rg '(^|/)(rust-toolchain(\.toml)?$|Cargo\.toml$|Cargo\.lock$|clippy\.toml$|\.clippy\.toml$|config\.toml$|\.github/workflows/.*\.ya?ml$)' || true

echo
echo "Rust toolchain declarations:"
for f in $(git ls-files | rg '(^|/)rust-toolchain(\.toml)?$|(^|/)Cargo\.toml$'); do
  echo "--- $f"
  rg -n 'rust-version|package\.rust-version|channels|toolchain|channel' "$f" || true
done

echo
echo "Relevant code usages of is_multiple_of:"
rg -n '\.is_multiple_of\(' contracts Cargo.toml Cargo.lock rust-toolchain* .github 2>/dev/null || true

echo
echo "File excerpt:"
sed -n '1585,1606p' contracts/lending/src/lib.rs 2>/dev/null || true

Repository: thisisouvik/trustlend-stellar

Length of output: 2480


Pin the Rust toolchain to an is_multiple_of-compatible release.

contracts/rust-toolchain.toml only sets channel = "stable", while u32::is_multiple_of requires Rust 1.87+. Set an explicit channel = "1.87" or newer so older CI toolchains don’t silently build different compiler behavior or fail to compile this call.

🤖 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/lending/src/lib.rs` around lines 1598 - 1599, Update the Rust
toolchain configuration to pin channel to Rust 1.87 or newer, ensuring the
u32::is_multiple_of call in the lending implementation remains compatible across
CI environments.

@thisisouvik
thisisouvik merged commit f3da303 into thisisouvik:main Jul 30, 2026
4 of 11 checks 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.

[Smart Contracts] Implement dynamic reward token emission for borrower loyalty

2 participants