Use this checklist for every pull request that modifies stellar-lend/contracts/.
Copy the relevant sections into your PR description and check off each item before
requesting a review.
Scope: The canonical deployment target is
contracts/lending. See ARCHITECTURE.md for crate ownership boundaries.
All new behaviour must have a test. Tests must be in the same crate as the code
they cover (#[cfg(test)] mod in src/).
- Every new public function has at least one success-path test and one
failure-path test (
try_*variant orassert_eq!(result, Err(Ok(...)))). - All error codes introduced by the PR are reachable through a test.
- Zero-amount, negative-amount, and boundary values are explicitly tested for any function that accepts numeric input.
- Tests do not use
unwrap()orexpect()onResult/Optionvalues that could plausibly fail — use explicit assertions instead. -
cargo testpasses locally with no new failures beyond the pre-existing baseline (currently:borrow_health_factor_test::borrow_zero_collateral_rejected,borrow_health_factor_test::borrow_rejects_when_weighted_collateral_multiplication_overflows,zero_amount_semantics_test::borrow_zero_returns_invalid_amount, and pause-related failures ingranular_pause_ops_test,cross_asset_test, andliquidate_pause_test). - New test modules are registered in
lib.rsunder#[cfg(test)].
Relevant docs: BORROW_TESTS.md, REPAY_SEMANTICS.md, ZERO_AMOUNT_SEMANTICS.md
The following protocol invariants must not be broken and should be covered by at least one test each if the PR touches the relevant code paths.
- G-1 Read-only:
get_cross_position_summarydoes not mutate ledger state. - G-2 Idempotent: calling the view N times returns identical values.
- G-3 Collateral accuracy:
total_collateral_usdequals the sum of deposits × price. - G-4 Debt accuracy:
total_debt_usdequals the sum of outstanding debt × price. - G-5 HF formula:
health_factor = weighted_collateral × 10_000 / total_debt_usdwhen debt > 0; sentinel1_000_000when debt = 0. - G-6 Monotonicity: adding collateral never decreases HF; adding debt never increases it.
- G-7 User isolation: one user's operations do not affect another user's summary.
- G-8 Order invariance: depositing assets in any order produces the same totals.
- G-9 Conservative rounding: weighted-collateral uses floor division (never over-counts capacity).
- G-10 No view exploitation: view calls cannot mutate state or bypass access controls.
Relevant docs: CROSS_ASSET_RULES.md — View Guarantees
- Borrow-system
repayreturnsRepayAmountTooHigh(error 1012) on overpay (no silent clamp). - Cross-asset
repay_assetsilently clamps overpay to the outstanding balance. -
get_debt_balance()after a full repay returns 0;health_factorreturns sentinel. - Interest is settled before principal on every borrow-system repay.
Relevant docs: REPAY_SEMANTICS.md
- No unchecked arithmetic on user-supplied values (use checked ops or Rust's debug-mode overflow panics confirmed via test).
- Interest ceiling division is preserved — any change to
calculate_interestmust maintaininterest ≥ 1forprincipal > 0 && elapsed > 0.
Relevant docs: INTEREST_NUMERIC_ASSUMPTIONS.md
Required for any PR that changes storage keys, persistent data types, or
#[contractimpl] function signatures.
- No existing storage key has been renamed or removed without a migration path.
- Any new
PersistentorInstancestorage entry is documented in docs/storage.md with its key name, type, and TTL. - Existing ledger entries can still be decoded after the upgrade (backward-compatible XDR or explicit migration logic added).
-
initializeis still idempotent — a second call returnsAlreadyInitializedand leaves state unchanged. - If function signatures changed: client-side call patterns in integration tests or scripts have been updated.
- Upgrade proposal/approval/execute flow tested if the WASM hash changes.
Relevant docs: storage.md, UPGRADE_AUTHORIZATION.md, deployment.md — Mainnet checklist
Required if the PR adds, removes, or modifies emitted events or analytics state.
- Every new user-facing operation emits a corresponding event (topic + data).
- Event topic strings follow the existing naming pattern (e.g.,
pause_event,RepayEvent); no topic has been silently renamed. - Downstream consumers (indexers, monitoring dashboards) are noted in the PR description if event schema changed.
-
get_protocol_report/get_user_reportstill return complete data after the change. - If a pause switch was added or removed:
set_pause_switchtests cover the new granularity and thepause_eventis verified.
Include this block in the PR description for any change that touches auth, arithmetic, oracle reads, admin controls, or pause logic. Delete items that do not apply.
### Security notes
**Auth / access control**
- [ ] `require_auth()` is called for every entry point that modifies user state.
- [ ] Admin-only functions check caller against stored admin address.
- [ ] No new function bypasses the pause guard without explicit justification.
**Arithmetic**
- [ ] All i128 arithmetic on untrusted values uses checked ops or is range-bounded
by prior validation.
- [ ] No new division site can produce a divide-by-zero (guarded by a prior `== 0`
check or provably non-zero invariant).
**Oracle / price feed**
- [ ] New price reads go through the staleness check in `oracle.rs`.
- [ ] Price manipulation cannot cause a state transition that benefits the caller
at the protocol's expense.
**Reentrancy**
- [ ] Any new cross-contract call follows the Checks-Effects-Interactions pattern
(state updated before the external call, not after).
- [ ] If a token transfer is involved, verify it cannot re-enter through a callback.
**Dust / rounding**
- [ ] Rounding direction is conservative (floor for collateral capacity,
ceiling for interest owed) — protocol never under-collects or over-extends.
**Recovery mode**
- [ ] If adding a new pause type: confirm whether it should be exempt during
`EmergencyState::Recovery` (repay is; new borrows are not).
Relevant docs: SECURITY_ASSUMPTIONS.md, REENTRANCY_GUARANTEES.md
- Public functions added or changed have a doc comment explaining parameters, return value, and error codes (one short line per item is enough).
- If a new error code was introduced: it appears in the relevant docs section
(e.g.,
REPAY_SEMANTICS.mderror table,CROSS_ASSET_RULES.mdinvariants). -
docs/storage.mdis up to date if storage layout changed. -
CROSS_ASSET_RULES.mdView Guarantees section updated ifget_cross_position_summarysemantics changed. - This checklist is complete and included in the PR description.
-
cargo fmt --checkpasses (no formatting diffs). -
cargo clippy -- -D warningspasses with no new warnings. -
cargo testoutput reviewed — no newFAILEDentries beyond the known baseline. -
cargo auditshows no new critical advisories (cargo install cargo-auditif needed). - Commit messages are imperative mood, ≤ 72 chars on the subject line.
- No secrets, key material, or environment-specific paths committed.
- Branch is rebased on (or merged from)
mainbefore opening the PR.
These failures exist on main and are not a blocker for new PRs. Do not mask
or skip them; investigate separately.
| Test | Likely cause |
|---|---|
borrow_health_factor_test::borrow_zero_collateral_rejected |
Borrow guard logic mismatch |
borrow_health_factor_test::borrow_rejects_when_weighted_collateral_multiplication_overflows |
Extreme-value edge case in borrow module |
zero_amount_semantics_test::borrow_zero_returns_invalid_amount |
Math-safety / zero-amount validation gap |
granular_pause_ops_test::unauthorized_caller_rejected |
Pause authorization rejection |
cross_asset_test::test_deposit_collateral_asset_paused |
Cross-asset pause behavior |
cross_asset_test::test_borrow_asset_paused |
Cross-asset pause behavior |
liquidate_pause_test::liquidate_blocked_when_global_pause |
Global pause interaction |