Skip to content

fix(contracts): block first-depositor donation attack and accrue by loan term#33

Merged
ogazboiz merged 1 commit into
LabsCrypt:mainfrom
tosin-zoffun:feat/issues-1-4
Jun 20, 2026
Merged

fix(contracts): block first-depositor donation attack and accrue by loan term#33
ogazboiz merged 1 commit into
LabsCrypt:mainfrom
tosin-zoffun:feat/issues-1-4

Conversation

@tosin-zoffun

Copy link
Copy Markdown
Contributor

Summary

Two P0 contract bugs bundled into one PR.

  • [Contracts] LendingPool first-depositor share inflation/donation attack: no minimum liquidity lock #1 — LendingPool first-depositor inflation/donation attack (lending_pool/src/lib.rs): enforces a MINIMUM_INITIAL_DEPOSIT of 1_000 atomic units on the first deposit per token pool, so an attacker can no longer mint a 1-share-for-1-stroop foothold then donate tokens directly to the contract to round a later depositor's mint down to zero. Also turns the donation pathology in calc_assets_to_redeem (zero divisor / negative balances) into a typed PoolError::InvalidAmount instead of a raw expect panic. A new regression test runs the classic ERC-4626 inflation attack end-to-end and asserts both the 1-stroop deposit is rejected and the honest second depositor still mints non-zero shares that redeem back. Two existing test scenarios (test_deposit_withdraw_invariants, test_pro_rata_yield_distribution_on_withdrawal) used first deposits below the new floor and were scaled up x10 — same invariants, same ratios, just larger numbers.
  • [Contracts] LoanManager interest and late-fee accrual normalize by DEFAULT_TERM_LEDGERS instead of the loan's actual term #4 — LoanManager interest and late-fee accrual normalized by the wrong term (loan_manager/src/lib.rs): accrue_interest and accrue_late_fee now normalise by the loan's own term_ledgers (falling back to DEFAULT_TERM_LEDGERS only when the stored field is 0), so a loan approved with a non-default term accrues interest/late fees at the rate implied by its stated APR-over-term rather than the contract-wide default.

Test plan

  • cargo test -p lending_pool49 / 49 pass (48 existing + 1 new donation-attack regression).
  • cargo test -p loan_manager87 / 87 pass.
  • cargo fmt --all -- --check clean.
  • cargo clippy -p lending_pool -p loan_manager --all-targets clean.

Notes

Closes #1
Closes #4

@ogazboiz ogazboiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the logic here is good and ci is green. issue #1: the MINIMUM_INITIAL_DEPOSIT = 1_000 guard on the first deposit (cur_total_shares == 0 && amount < MIN) plus the existing shares_to_mint <= 0 revert raises the donation/inflation attack cost, and the test shows a 1-stroop first deposit is rejected while an honest second depositor still mints and redeems their principal. issue #4: interest and the late-fee divisor now use the loan's own term_ledgers (falling back to DEFAULT_TERM_LEDGERS only when it's 0) instead of the hardcoded default, and you scaled the share-math tests to respect the new minimum. nice work.

two things before it can land:

  1. please rebase on main and let ci re-run. #31 just merged and it reworked the same redeem path in lending_pool/src/lib.rs (it split redeem_shares into a side-effect-free core and added PoolError::NotPaused). your branch predates that, so even though github shows it mergeable, your green ci ran against a base that no longer matches main. git fetch origin && git rebase origin/main && git push --force-with-lease, resolve around redeem_shares/calc_assets_to_redeem, and i'll merge once ci is green against the new base.

  2. heads up that #34 overlaps with you: it also rewrites calc_assets_to_redeem to a Result and also gates the loan term by loan.term_ledgers, but from the approval angle (yours is the accrual angle). you two are actually complementary, so i'm treating yours as the lead since it carries the accrual fix and the stronger redeem hardening, and i've asked #34 to rebase on top of yours afterward.

non-blocking nit: the late-fee path still ends in .expect("late fee overflow") (lib.rs:602), so the redeem path is hardened against panics but that one is still there. fine to leave for now since it matches main, just noting it given the theme of the pr.

if you want to keep contributing, join us on Telegram: https://t.me/+DOylgFv1jyJlNzM0

…oan term

- LabsCrypt#1: LendingPool now enforces a MINIMUM_INITIAL_DEPOSIT of 1_000 atomic
  units on the first deposit per token pool and turns the donation-attack
  pathology in calc_assets_to_redeem into a typed PoolError instead of a
  raw expect panic. An attacker can no longer mint a single share for one
  stroop and then donate tokens to round a later depositor's mint down to
  zero. Includes a regression test that runs the classic ERC-4626 inflation
  attack and asserts both the 1-stroop deposit is rejected and an honest
  second depositor still mints a non-zero share count that redeems back.
- LabsCrypt#4: LoanManager accrue_interest and accrue_late_fee now normalise by the
  loan's own term_ledgers (falling back to DEFAULT_TERM_LEDGERS only when
  the stored field is 0 / unset), so loans approved with a non-default
  term accrue interest and late fees at the rate implied by their stated
  APR-over-term instead of the contract-wide default.

cargo test -p lending_pool -p loan_manager green (49 + 87).

Closes LabsCrypt#1
Closes LabsCrypt#4
@ogazboiz

Copy link
Copy Markdown
Contributor

quick coordination update: #36 just came in with a more complete fix for the donation/inflation attack specifically, it reworks share accounting to track managed assets as the source of truth rather than the raw token balance. your accrue-by-loan-term change is still exactly what we need from this pr and is unaffected by that, but the MINIMUM_INITIAL_DEPOSIT donation guard here overlaps #36, and the two touch the same share math. so when you rebase, please focus this pr on the term accrual (and the typed calc_assets_to_redeem hardening), and we'll take the share-accounting donation fix from #36 to avoid two competing fixes on the same code. thanks, and sorry for the moving target, a lot landed on this file in the last hour.

david87131 added a commit to david87131/remitlend-contracts that referenced this pull request Jun 19, 2026
LoanManager.approve_loan now uses the loan's stored term_ledgers (falling
back to read_default_term() only when 0 / unset), instead of overwriting
the borrower-requested term with the contract-wide default. The
min_term/max_term limits checked at request time now actually govern the
approved due_date.

Regression test asserts that requesting a non-default term and approving
produces a due_date derived from the requested term.

Scope narrowed: the redeem-math hardening (LabsCrypt#3) has been dropped in favour
of LabsCrypt#33's version, which the maintainer is landing as the lead. This PR
keeps only the approve-time term fix from LabsCrypt#5; the redeem hardening will
arrive via LabsCrypt#33 and is no longer duplicated here.

cargo test -p lending_pool -p loan_manager green (50 + 89).

Closes LabsCrypt#5

@ogazboiz ogazboiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is clean now and ci is green against the current main. the donation/inflation fix is solid: MINIMUM_INITIAL_DEPOSIT (1_000) blocks the tiny first-deposit foothold (the new regression test shows a 1-stroop first deposit is rejected and an honest second depositor still redeems back their principal), and you scaled the existing share-math tests up to respect the minimum while keeping the same ratios. calc_assets_to_redeem now returns a typed PoolError (cur_total_shares <= 0, total_assets < 0, overflow) instead of the .expect panic that could trap funds, and it integrates cleanly with #31's redeem_shares core. the accrue-by-loan-term change (issue #4) rounds it out. merging.

quick coordination note: i'd said i'd take the donation fix from #36, but #36 still needs work first (it has to wire record_yield into the repayment path), so i'm landing your MINIMUM_INITIAL_DEPOSIT guard now as the interim defense. it and #36's managed-asset accounting are complementary, so #36 will just rebase on top. thanks for the quick turnaround.

if you want to keep contributing, join us on Telegram: https://t.me/+DOylgFv1jyJlNzM0

@ogazboiz ogazboiz merged commit 4c9e637 into LabsCrypt:main Jun 20, 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

2 participants