fix(contracts): block first-depositor donation attack and accrue by loan term#33
Conversation
ogazboiz
left a comment
There was a problem hiding this comment.
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:
-
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. -
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
|
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. |
58fb417 to
56d7716
Compare
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
left a comment
There was a problem hiding this comment.
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
Summary
Two P0 contract bugs bundled into one PR.
lending_pool/src/lib.rs): enforces aMINIMUM_INITIAL_DEPOSITof 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 incalc_assets_to_redeem(zero divisor / negative balances) into a typedPoolError::InvalidAmountinstead of a rawexpectpanic. 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.loan_manager/src/lib.rs):accrue_interestandaccrue_late_feenow normalise by the loan's ownterm_ledgers(falling back toDEFAULT_TERM_LEDGERSonly 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_pool— 49 / 49 pass (48 existing + 1 new donation-attack regression).cargo test -p loan_manager— 87 / 87 pass.cargo fmt --all -- --checkclean.cargo clippy -p lending_pool -p loan_manager --all-targetsclean.Notes
loan.term_ledgers != DEFAULT_TERM_LEDGERS. Withapprove_loancurrently overwriting the borrower-requested term with the default (issue [Contracts] LoanManager approve_loan overrides the borrower-requested term with the default term #5, scoped to a separate PR), this fix is a no-op in the steady state today and turns on the moment [Contracts] LoanManager approve_loan overrides the borrower-requested term with the default term #5 lands.Closes #1
Closes #4