Skip to content

Commit

Permalink
Add origination fees (#65)
Browse files Browse the repository at this point in the history
* Add origination fees

* Extract ILoanFees struct

This is necessary because we ran out of call stack
  • Loading branch information
bricestacey authored Oct 28, 2022
1 parent ef0d5a8 commit 9c9df3c
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 13 deletions.
25 changes: 18 additions & 7 deletions contracts/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ contract Loan is ILoan {
uint256 public immutable payment;
uint256 public paymentsRemaining;
uint256 public paymentDueDate;
uint256 public latePaymentFee;
uint256 public originationFee;
ILoanFees fees;

/**
* @dev Modifier that requires the Loan be in the given `state_`
Expand Down Expand Up @@ -98,7 +99,7 @@ contract Loan is ILoan {
address liquidityAsset_,
uint256 principal_,
uint256 dropDeadTimestamp,
uint256 latePaymentFee_
ILoanFees memory fees_
) {
_serviceConfiguration = serviceConfiguration;
_factory = factory;
Expand All @@ -113,7 +114,7 @@ contract Loan is ILoan {
apr = apr_;
liquidityAsset = liquidityAsset_;
principal = principal_;
latePaymentFee = latePaymentFee_;
fees = fees_;

LoanLib.validateLoan(
serviceConfiguration,
Expand All @@ -131,6 +132,14 @@ contract Loan is ILoan {
.div(RAY)
.div(10000);
payment = paymentsTotal.mul(RAY).div(paymentsRemaining).div(RAY);

// Persist origination fee per payment period
originationFee = principal
.mul(fees.originationBps)
.mul(duration.mul(RAY).div(360))
.div(paymentsRemaining)
.div(RAY)
.div(10000);
}

/**
Expand Down Expand Up @@ -312,7 +321,7 @@ contract Loan is ILoan {
payment,
_serviceConfiguration.firstLossFeeBps(),
IPool(_pool).poolFeePercentOfInterest(),
latePaymentFee,
fees.latePayment,
paymentDueDate
);

Expand All @@ -321,7 +330,8 @@ contract Loan is ILoan {
IPool(_pool).firstLossVault(),
firstLossFee,
IPool(_pool).feeVault(),
poolFee
poolFee,
originationFee
);
LoanLib.completePayment(liquidityAsset, _pool, poolPayment);
paymentsRemaining -= 1;
Expand All @@ -342,7 +352,7 @@ contract Loan is ILoan {
amount,
_serviceConfiguration.firstLossFeeBps(),
IPool(_pool).poolFeePercentOfInterest(),
latePaymentFee,
fees.latePayment,
paymentDueDate
);

Expand All @@ -351,7 +361,8 @@ contract Loan is ILoan {
IPool(_pool).firstLossVault(),
firstLossFee,
IPool(_pool).manager(),
poolFee
poolFee,
originationFee
);

LoanLib.completePayment(
Expand Down
4 changes: 2 additions & 2 deletions contracts/LoanFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ contract LoanFactory {
address liquidityAsset,
uint256 principal,
uint256 dropDeadDate,
uint256 latePaymentFee
ILoanFees memory fees
) public virtual returns (address LoanAddress) {
require(
_serviceConfiguration.paused() == false,
Expand All @@ -59,7 +59,7 @@ contract LoanFactory {
liquidityAsset,
principal,
dropDeadDate,
latePaymentFee
fees
);
address addr = address(loan);
emit LoanCreated(addr);
Expand Down
5 changes: 5 additions & 0 deletions contracts/interfaces/ILoan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ struct ILoanNonFungibleCollateral {
uint256 tokenId;
}

struct ILoanFees {
uint256 latePayment;
uint256 originationBps;
}

interface ILoan {
/**
* @dev Emitted when loan is funded.
Expand Down
10 changes: 9 additions & 1 deletion contracts/libraries/LoanLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ library LoanLib {
address firstLossVault,
uint256 firstLoss,
address poolAdmin,
uint256 poolFeePercentOfInterest
uint256 poolFeePercentOfInterest,
uint256 originationFee
) public {
if (firstLoss > 0) {
IERC20(asset).safeTransferFrom(
Expand All @@ -287,5 +288,12 @@ library LoanLib {
poolFeePercentOfInterest
);
}
if (originationFee > 0) {
IERC20(asset).safeTransferFrom(
msg.sender,
poolAdmin,
originationFee
);
}
}
}
61 changes: 59 additions & 2 deletions test/Loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DEFAULT_POOL_SETTINGS } from "./support/pool";
import {
collateralizeLoan,
collateralizeLoanNFT,
DEFAULT_LOAN_SETTINGS,
fundLoan,
matureLoan
} from "./support/loan";
Expand All @@ -14,7 +15,10 @@ describe("Loan", () => {
const SEVEN_DAYS = 6 * 60 * 60 * 24;
const THIRTY_DAYS = 30 * 60 * 60 * 24;

async function deployFixture(poolSettings = DEFAULT_POOL_SETTINGS) {
async function deployFixture(
poolSettings = DEFAULT_POOL_SETTINGS,
loanSettings = DEFAULT_LOAN_SETTINGS
) {
// Contracts are deployed using the first signer/account by default
const [operator, poolManager, borrower, lender, other] =
await ethers.getSigners();
Expand Down Expand Up @@ -100,7 +104,10 @@ describe("Loan", () => {
liquidityAsset.address,
500_000,
Math.floor(Date.now() / 1000) + SEVEN_DAYS,
1_000
{
latePayment: 1_000,
originationBps: loanSettings.originationFee
}
);
const tx2Receipt = await tx2.wait();

Expand Down Expand Up @@ -149,6 +156,15 @@ describe("Loan", () => {
return deployFixture(poolSettings);
}

async function deployFixtureOriginationFees() {
return deployFixture(
DEFAULT_POOL_SETTINGS,
Object.assign({}, DEFAULT_LOAN_SETTINGS, {
originationFee: 100
})
);
}

describe("after initialization", () => {
it("is initialized!", async () => {
const { loan, pool, borrower, loanFactory } = await loadFixture(
Expand Down Expand Up @@ -1099,6 +1115,47 @@ describe("Loan", () => {
const newDueDate = await loan.paymentDueDate();
expect(newDueDate).to.equal(dueDate.add(THIRTY_DAYS));
});

it("can collect origination fees from the next payment", async () => {
const fixture = await loadFixture(deployFixtureOriginationFees);
const {
borrower,
collateralAsset,
liquidityAsset,
loan,
pool,
poolManager
} = fixture;

// Setup
await collateralAsset.connect(borrower).approve(loan.address, 100);
await loan
.connect(borrower)
.postFungibleCollateral(collateralAsset.address, 100);
await pool.connect(poolManager).fundLoan(loan.address);
await loan.connect(borrower).drawdown();
expect(await loan.originationFee()).to.equal(416);

// Make payment
const firstLoss = await pool.firstLossVault();
const feeVault = await pool.feeVault();
const dueDate = await loan.paymentDueDate();
expect(await loan.paymentsRemaining()).to.equal(6);
await liquidityAsset.connect(borrower).approve(loan.address, 2083 + 416);
const tx = loan.connect(borrower).completeNextPayment();
await expect(tx).to.not.be.reverted;
await expect(tx).to.changeTokenBalance(
liquidityAsset,
borrower,
-2083 - 416
);
await expect(tx).to.changeTokenBalance(liquidityAsset, pool, 1979);
await expect(tx).to.changeTokenBalance(liquidityAsset, feeVault, 416);
await expect(tx).to.changeTokenBalance(liquidityAsset, firstLoss, 104);
expect(await loan.paymentsRemaining()).to.equal(5);
const newDueDate = await loan.paymentDueDate();
expect(newDueDate).to.equal(dueDate.add(THIRTY_DAYS));
});
});

const findEventByName = (receipt, name) => {
Expand Down
10 changes: 9 additions & 1 deletion test/support/loan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { deployServiceConfiguration } from "./serviceconfiguration";

const SEVEN_DAYS = 6 * 60 * 60 * 24;

export const DEFAULT_LOAN_SETTINGS = {
latePayment: 0,
originationFee: 0
};

/**
* Deploy a loan
*/
Expand Down Expand Up @@ -43,7 +48,10 @@ export async function deployLoan(
liquidityAsset,
1_000_000,
Math.floor(Date.now() / 1000) + SEVEN_DAYS,
0
{
latePayment: 1_000,
originationBps: 0
}
);

const txnReceipt = await txn.wait();
Expand Down

0 comments on commit 9c9df3c

Please sign in to comment.