feat: add borrower loyalty rewards system - #208
Conversation
- 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
|
@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. |
|
@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! 🚀 |
📝 WalkthroughWalkthroughThis 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 ChangesBorrower loyalty rewards
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 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: 6
🧹 Nitpick comments (1)
contracts/lending/src/test.rs (1)
2166-2346: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated setup between the two new loyalty integration tests.
test_repayment_calls_loyalty_contractandtest_late_repayment_no_rewardrepeat the same ~35 lines (register/initialize lending + loyalty, whitelist asset, fund loyalty contract, linkset_loyalty_contract). Consider extracting a sharedsetup_with_loyalty()helper (mirroring the existingsetup()/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
⛔ Files ignored due to path filters (1)
contracts/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (84)
contracts/Cargo.tomlcontracts/borrower_loyalty/Cargo.tomlcontracts/borrower_loyalty/src/lib.rscontracts/borrower_loyalty/src/test.rscontracts/borrower_loyalty/test_snapshots/test/test_admin_can_update_config.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_different_tiers_receive_different_rewards.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_double_initialize_panics.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_eligible_borrower_receives_rewards.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_fund_and_drain_scenario.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_initialize.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_calculation_correctness.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_duration.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_loan_size.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_cannot_distribute.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_gets_zero.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_unauthorized_returns_zero_reward.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_zero_loan_amount_returns_zero.1.jsoncontracts/lending/Cargo.tomlcontracts/lending/src/lib.rscontracts/lending/src/test.rscontracts/lending/test_snapshots/test/test_approve_loan_blocked_when_paused.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_max_duration_365_days.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_min_duration_1_day.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_request_basic.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_request_checks_borrowing_power.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_request_with_multi_asset_collateral.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_with_fixed_rate_model.1.jsoncontracts/lending/test_snapshots/test/test_create_loan_with_floating_rate_model.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_blocked_when_paused.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_multiple_assets.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_non_whitelisted_asset_rejected.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_same_asset_accumulates.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_single_asset.1.jsoncontracts/lending/test_snapshots/test/test_deposit_collateral_zero_amount_rejected.1.jsoncontracts/lending/test_snapshots/test/test_due_at_computed_correctly.1.jsoncontracts/lending/test_snapshots/test/test_empty_collateral_entries_rejected.1.jsoncontracts/lending/test_snapshots/test/test_failed_flash_loan_rolls_back_pool_balance.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_accepts_overpayment.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_fee_capped.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_rejects_amount_over_pool_liquidity.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_rejects_negative_amount.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_rejects_zero_amount.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_reverts_on_partial_repayment.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_reverts_when_receiver_repays_nothing.1.jsoncontracts/lending/test_snapshots/test/test_flash_loan_success_full_repayment.1.jsoncontracts/lending/test_snapshots/test/test_get_asset_collateral_config_default.1.jsoncontracts/lending/test_snapshots/test/test_get_borrowing_power_multiple_assets.1.jsoncontracts/lending/test_snapshots/test/test_get_borrowing_power_no_collateral.1.jsoncontracts/lending/test_snapshots/test/test_get_borrowing_power_single_asset.1.jsoncontracts/lending/test_snapshots/test/test_get_total_collateral_value.1.jsoncontracts/lending/test_snapshots/test/test_get_total_collateral_value_empty.1.jsoncontracts/lending/test_snapshots/test/test_get_user_collateral_entries_empty_for_new_borrower.1.jsoncontracts/lending/test_snapshots/test/test_interest_calculation_is_correct.1.jsoncontracts/lending/test_snapshots/test/test_late_repayment_no_reward.1.jsoncontracts/lending/test_snapshots/test/test_mark_defaulted_blocked_when_paused.1.jsoncontracts/lending/test_snapshots/test/test_no_overflow_at_maximum_valid_inputs.1.jsoncontracts/lending/test_snapshots/test/test_non_whitelisted_collateral_asset_rejected.1.jsoncontracts/lending/test_snapshots/test/test_only_admin_can_change_flash_loan_fee.1.jsoncontracts/lending/test_snapshots/test/test_platform_fee_is_one_percent_of_interest.1.jsoncontracts/lending/test_snapshots/test/test_pool_usable_again_immediately_after_a_failed_flash_loan.1.jsoncontracts/lending/test_snapshots/test/test_record_payment_allowed_when_paused.1.jsoncontracts/lending/test_snapshots/test/test_repayment_calls_loyalty_contract.1.jsoncontracts/lending/test_snapshots/test/test_repayment_without_loyalty_contract_works.1.jsoncontracts/lending/test_snapshots/test/test_resume_operations_after_unpause.1.jsoncontracts/lending/test_snapshots/test/test_set_asset_collateral_config_clamps_to_bounds.1.jsoncontracts/lending/test_snapshots/test/test_set_asset_collateral_config_non_admin_rejected.1.jsoncontracts/lending/test_snapshots/test/test_set_asset_collateral_config_non_whitelisted_rejected.1.jsoncontracts/lending/test_snapshots/test/test_set_loyalty_contract.1.jsoncontracts/lending/test_snapshots/test/test_set_loyalty_contract_non_admin_rejected.1.jsoncontracts/lending/test_snapshots/test/test_set_price_oracle.1.jsoncontracts/lending/test_snapshots/test/test_set_price_oracle_non_admin_rejected.1.jsoncontracts/lending/test_snapshots/test/test_switch_rate_model_allowed_after_cooldown.1.jsoncontracts/lending/test_snapshots/test/test_switch_rate_model_cooldown_enforced.1.jsoncontracts/lending/test_snapshots/test/test_switch_rate_model_fixed_to_floating.1.jsoncontracts/lending/test_snapshots/test/test_switch_rate_model_only_active.1.jsoncontracts/lending/test_snapshots/test/test_switch_rate_model_only_borrower.1.jsoncontracts/lending/test_snapshots/test/test_update_floating_rate.1.jsoncontracts/lending/test_snapshots/test/test_update_floating_rate_rejects_fixed.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_allowed_with_no_active_loans.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_basic.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_exceeds_balance_rejected.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_full_amount_removes_entry.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_nonexistent_asset_rejected.1.jsoncontracts/lending/test_snapshots/test/test_withdraw_collateral_zero_amount_rejected.1.json
| [lib] | ||
| crate-type = ["rlib"] |
There was a problem hiding this comment.
🩺 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 || trueRepository: 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 || trueRepository: 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:
- 1: https://deepwiki.com/stellar/stellar-docs/5.5-rust-dialect-for-soroban
- 2: https://developers.stellar.org/docs/build/smart-contracts/getting-started/hello-world
- 3: https://deepwiki.com/stellar/stellar-docs/5.2-contract-lifecycle-and-cli-operations
- 4: https://deepwiki.com/stellar/stellar-docs/5-smart-contract-development
- 5: Change command recommended for building .wasm files for deployment stellar-deprecated/soroban-docs#476
- 6: https://dev.to/esteblock/converting-a-soroban-smart-contract-into-a-crate-library-228m
🌐 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:
- 1: https://developers.stellar.org/docs/build/smart-contracts/getting-started/hello-world
- 2: script3/sep-40-oracle@053d62a
- 3: https://users.rust-lang.org/t/why-do-i-need-to-set-the-crate-type-to-cdylib-to-build-a-wasm-binary/93247
- 4: https://docs.rs/crate/base-minter/3.2.7/source/Cargo.toml.orig
- 5: https://crates.io/crates/soroban-sdk-macros
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.
| [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.
…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
There was a problem hiding this comment.
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 liftRange check doesn't stop borrowers from self-reporting the maximum reputation tier.
reputation_tieris still taken verbatim from the borrower-suppliedLoanRequestInput(lines 114-115, 979) and only range-checked (> 4panics) before being persisted toLoanReputationTier(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 tier4and 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_tierfrom the borrower's stored profile (via a cross-contract call toborrower_reputation, or by persisting/reading a lending-side authoritative record) rather than trusting the value inLoanRequestInput.🤖 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 winPersist the repayment state before invoking the loyalty client.
In
record_payment, the loan is read into a localmut loan,remaining_dueandstatusare updated locally, thenLoyaltyClient::distribute_rewardis called while the persistedLoan(loan_id)still contains the pre-repayment state. The updatedloanis 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
📒 Files selected for processing (23)
components/auth/AuthAccessButton.tsxcomponents/dashboard/WithdrawToFiatButton.tsxcontracts/borrower_loyalty/src/lib.rscontracts/borrower_loyalty/test_snapshots/test/test_admin_can_update_config.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_different_tiers_receive_different_rewards.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_double_initialize_panics.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_eligible_borrower_receives_rewards.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_fund_and_drain_scenario.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_initialize.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_calculation_correctness.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_duration.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_reward_scales_with_loan_size.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_unauthorized_caller_gets_zero.1.jsoncontracts/borrower_loyalty/test_snapshots/test/test_zero_loan_amount_returns_zero.1.jsoncontracts/lending/src/lib.rscontracts/lending/src/test.rscontracts/lending/test_snapshots/test/test_late_repayment_no_reward.1.jsoncontracts/lending/test_snapshots/test/test_price_oracle_median_resists_manipulation.1.jsoncontracts/lending/test_snapshots/test/test_price_oracle_twap_fallback_is_used.1.jsoncontracts/lending/test_snapshots/test/test_repayment_calls_loyalty_contract.1.jsoncontracts/treasury/src/lib.rscontracts/treasury/src/test.rscontracts/treasury/test_snapshots/test/test_collect_protocol_fees_from_lending.1.json
| </p> | ||
| )} | ||
| </div> | ||
| </FocusTrap> |
There was a problem hiding this comment.
🎯 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.
| let midpoint = len / 2; | ||
| let even = len % 2 == 0; | ||
| let even = len.is_multiple_of(2); |
There was a problem hiding this comment.
🎯 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:
- 1: Tracking issue for release notes of #128101: Tracking Issue for
unsigned_is_multiple_ofrust-lang/rust#137380 - 2: https://github.com/rust-lang/rust/releases/tag/1.87.0
- 3: https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/
- 4: petrochenkov/rust@88ed69c
- 5: Tracking Issue for
unsigned_is_multiple_ofrust-lang/rust#128101 - 6: add
is_multiple_offor unsigned integer types rust-lang/rust#128103 - 7: https://docs.rs/num/latest/num/integer/trait.Integer.html
- 8: https://docs.rs/num/latest/num/trait.Integer.html
- 9: https://docs.diesel.rs/master/num_integer/trait.Integer.html
🏁 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 || trueRepository: 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.
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_loyaltyAdded a dedicated
BorrowerLoyaltyContractwith the following functionality:initializeset_configset_reward_tokendistribute_rewardcalculate_reward_viewget_borrower_rewardsget_total_rewards_distributedReward calculation
Rewards are calculated using the formula:
base_amount × size_multiplier × duration_multiplier × tier_multiplierAll calculations are performed in basis points (bps) using
i128arithmetic for overflow safety.Supported borrower reputation tiers:
Lending contract integration
Updated
contracts/lending/src/lib.rsto integrate the loyalty system:Added
LoyaltyClientfor cross-contract calls usingenv.invoke_contractAdded new storage keys:
DataKey::LoyaltyContractDataKey::LoanReputationTier(u32)Added admin functions:
set_loyalty_contractget_loyalty_contractExtended
LoanRequestInputwith areputation_tierfieldAdded reward distribution hook during
record_paymentfor eligible full, on-time repaymentsTest coverage
Borrower loyalty contract
Added 11 unit tests covering:
Lending integration
Added integration tests covering:
Validation
All verification steps completed successfully:
cargo fmtcargo clippy -D warningsNo regressions or warnings were introduced.
Closes #77
Summary by CodeRabbit
New Features
Bug Fixes
Tests