Skip to content

parv3213/lending-vault-assignment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ERC-4626 Lending Vault Assignment

This repository implements a basic ERC-4626 lending vault where:

  • investors deposit one ERC-20 asset and receive vault shares,
  • one whitelisted borrower can request one amortizing loan,
  • a Vault Admin approves and disburses the loan,
  • repayments return principal plus fixed-rate interest to the vault,
  • share value increases only when interest or late fees are actually paid.

Project layout

  • src/LendingVault.sol: ERC-4626 vault + loan lifecycle + repayment accounting
  • src/mocks/MockERC20.sol: mock ERC-20 used by tests and demo
  • script/DemoLendingVault.s.sol: end-to-end scenario script
  • test/LendingVault.t.sol: focused Foundry tests

How to run

Install Foundry (forge, cast, anvil)

If you don’t have Foundry installed, use the official installer:

curl -L https://foundry.paradigm.xyz | bash
foundryup

For more options (precompiled binaries, Docker, building from source), see the Foundry installation guide.

Install project dependencies

From the repo root, install Forge dependencies (e.g. forge-std, OpenZeppelin):

forge install

Build and run

forge build
forge test -vv
forge script script/DemoLendingVault.s.sol:DemoLendingVault -vv

Validated results in this environment:

  • forge test -vv: 5/5 tests passed
  • forge script ...DemoLendingVault -vv: ran successfully and printed deposit, disbursement, repayment, and share-price changes

Design overview

flowchart LR
    Investors[Investors] -->|deposit asset| Vault[LendingVault]
    Vault -->|mint ERC4626 shares| Investors
    Borrower[WhitelistedBorrower] -->|request terms| Vault
    Admin[VaultAdmin] -->|approve and disburse| Vault
    Vault -->|loan principal| Borrower
    Borrower -->|EMI principal+interest+lateFee| Vault
    Vault -->|interest and fees raise PPS| ShareValue[ShareValue]
Loading

Key design decisions

1. Single vault, single borrower, single originated loan

The vault stores only one borrower address and one LoanDetails struct.

  • The borrower must be explicitly whitelisted by the admin.
  • A rejected request can be resubmitted.
  • Once a loan is actually originated and later closed, the vault does not allow another loan.

Reasoning:

  • This matches the assignment’s simplifying assumption.
  • It keeps state transitions explicit and avoids needing a loan registry or per-loan accounting.

2. No new deposits after loan is disbursed (or closed)

The assignment does not require allowing deposits after a loan is disbursed. New deposits are disabled once the loan is Active or Closed by returning 0 from maxDeposit and maxMint. Calls to deposit or mint then revert via the standard ERC-4626 max checks.

Reasoning:

  • Avoids flash-loan and timing attacks: e.g. an attacker could otherwise borrow, deposit right before a repayment (or right after, to capture the next share-price bump), then redeem and repay the loan for a risk-free profit.
  • Keeps the vault lifecycle simple: capital is raised before the loan, and no new capital can enter while the single loan is active or after it is closed.

3. Vault accounting uses cash + outstanding principal

totalAssets() is defined as:

idle liquidity in vault + outstanding principal deployed to borrower

Reasoning:

  • Loan disbursement should not immediately reduce vault share value, because the vault still owns a receivable.
  • Principal repayments move value from receivable back to idle cash, so they should not create yield.
  • Only interest and late fees should increase share value.

This means:

  • disbursement keeps totalAssets flat,
  • principal repayment keeps totalAssets flat,
  • interest and penalties increase totalAssets and therefore increase share price.

4. Fixed-rate EMI schedule

The borrower submits:

  • principal
  • annualInterestBps
  • termPeriods

The vault stores one fixed paymentInterval for the vault. In the demo/tests it is 30 days.

Periodic rate is computed as:

annual rate * payment interval / 365 days

EMI is then calculated on-chain using the standard amortization formula. The last installment is trued up to repay the exact remaining principal so rounding dust does not leave the loan partially open.

Reasoning:

  • Using a vault-wide fixed interval keeps the contract simpler than supporting arbitrary calendars.
  • Using an annual rate is easier to reason about than a raw per-period rate.
  • Final-payment true-up makes the loan finish cleanly despite integer math.

5. Admin-controlled approval and disbursement

The borrower can request a loan, but cannot self-fund it.

Required admin actions:

  1. approveLoanRequest()
  2. disburseLoan()

Reasoning:

  • Approval and cash release are intentionally separate.
  • The admin can approve first, then wait until the vault has enough idle liquidity to actually disburse.

6. Late fee policy

The implementation applies a simple late fee:

late fee = current installment due * lateFeeBps * missedPeriods / 10_000

Reasoning:

  • This is easy to audit and explain.
  • It avoids building a more complex delinquency/default engine for a basic assignment.

Tradeoff:

  • It is intentionally simple and not meant to model a production collections system.

7. Pro-rata redemption under partial deployment

Withdrawals are limited by idle liquidity, not by total assets including the loan receivable.

The practical redemption cap is:

maxWithdraw(owner) = idleLiquidity * ownerShares / totalSupply

Reasoning:

  • This directly matches the requirement that only currently available liquidity can be redeemed.
  • It prevents one investor from draining all idle cash while others are still exposed to the outstanding loan.

Tools and libraries used

  • Foundry: Solidity-native testing and scripting in one toolchain
  • OpenZeppelin Contracts 5.x:
    • ERC4626 for vault mechanics
    • ERC20 for shares and the mock asset
    • Ownable for Vault Admin access control
    • Math and SafeERC20 for safer accounting/transfers
  • forge-std: test assertions, cheatcodes, and script logging

I also enabled optimizer + via_ir in foundry.toml. The contract compiles cleanly this way with Solidity 0.8.24, and it avoids stack-depth issues from the richer ERC-4626 + reporting logic.

Assumptions

  • One vault supports one borrower and one originated loan.
  • Yield comes only from realized interest and late-fee payments.
  • The underlying asset uses standard ERC-20 transfer semantics.
  • The Vault Admin can be any address, including a multisig controlled off-chain.

Limitations

  • No default/workout flow beyond simple late fees
  • No partial prepayments or refinancing flow
  • No support for multiple concurrent borrowers or loans
  • Future unpaid interest is not marked-to-market in totalAssets

That final point is deliberate for accounting simplicity, but it means a new depositor could still share in interest that is economically close to being realized but not yet paid.

What the demo shows

The script runs an end-to-end scenario with three investors, one borrower, and a 6-period amortizing loan (600e18 principal, 12% APR, 30-day payment interval):

  1. Investor deposits and share issuance — Three investors deposit 500, 300, and 200 units of the mock asset; the vault mints shares and reports idle liquidity, total assets, and share price.

  2. Borrower whitelisting and loan request — The admin sets the borrower; the borrower requests a 6-installment loan. The script logs the fixed EMI amount.

  3. Admin approval and disbursement — The admin approves the request and then disburses; the vault state is logged (idle liquidity drops, outstanding principal appears).

  4. Full loan repayment in installments — The borrower repays all six installments. For each installment the script logs:

    • Installment breakdown — Principal component, interest component, late fee (if any), total due, and missed periods (from nextInstallmentBreakdown()).
    • Vault state — Idle liquidity, outstanding principal, total assets, and share price after the payment.
  5. On-time vs late behavior — Installment 1 is paid on the due date (no late fee). Installment 2 is paid 5 days late, so a late fee and one missed period apply. Installment 3 is still in the “delayed” window, so it also incurs a late fee. For installment 4, the script warps to the exact nextDueDate so the payment is on time; installments 4, 5, and 6 then have no late fee and use the normal EMI amount. After the sixth payment the loan closes (outstanding principal 0).

  6. Share price and total assets — The logs show how idle liquidity, total assets, and share price evolve as principal is repaid (no yield) and as interest and late fees are paid (yield increases share price).

About

ERC-4626 Lending Vault Assignment

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors