Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add origination fees #65

Merged
merged 3 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions contracts/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ contract Loan is ILoan {
uint256 public immutable payment;
uint256 public paymentsRemaining;
uint256 public paymentDueDate;
uint256 public originationFeeBps;
uint256 public originationFee;

/**
* @dev Modifier that requires the Loan be in the given `state_`
Expand Down Expand Up @@ -96,7 +98,8 @@ contract Loan is ILoan {
uint256 apr_,
address liquidityAsset_,
uint256 principal_,
uint256 dropDeadTimestamp
uint256 dropDeadTimestamp,
uint256 originationFeeBps_
) {
_serviceConfiguration = serviceConfiguration;
_factory = factory;
Expand Down Expand Up @@ -128,6 +131,15 @@ contract Loan is ILoan {
.div(RAY)
.div(10000);
payment = paymentsTotal.mul(RAY).div(paymentsRemaining).div(RAY);

// Persist origination fee and cache the computed value
originationFeeBps = originationFeeBps_;
originationFee = principal
.mul(originationFeeBps)
.mul(duration.mul(RAY).div(360))
.div(paymentsRemaining)
.div(RAY)
.div(10000);
}

/**
Expand Down Expand Up @@ -316,7 +328,8 @@ contract Loan is ILoan {
IPool(_pool).firstLossVault(),
firstLossFee,
IPool(_pool).feeVault(),
poolFee
poolFee,
originationFee
);
LoanLib.completePayment(liquidityAsset, _pool, poolPayment);
paymentsRemaining -= 1;
Expand Down Expand Up @@ -344,7 +357,8 @@ contract Loan is ILoan {
IPool(_pool).firstLossVault(),
firstLossFee,
IPool(_pool).manager(),
poolFee
poolFee,
originationFee
);

LoanLib.completePayment(
Expand Down
6 changes: 4 additions & 2 deletions contracts/LoanFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ contract LoanFactory {
uint256 apr,
address liquidityAsset,
uint256 principal,
uint256 dropDeadDate
uint256 dropDeadDate,
uint256 originationFee
) public virtual returns (address LoanAddress) {
require(
_serviceConfiguration.paused() == false,
Expand All @@ -57,7 +58,8 @@ contract LoanFactory {
apr,
liquidityAsset,
principal,
dropDeadDate
dropDeadDate,
originationFee
);
address addr = address(loan);
emit LoanCreated(addr);
Expand Down
10 changes: 9 additions & 1 deletion contracts/libraries/LoanLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ library LoanLib {
address firstLossVault,
uint256 firstLoss,
address poolAdmin,
uint256 poolFeePercentOfInterest
uint256 poolFeePercentOfInterest,
uint256 originationFee
) public {
if (firstLoss > 0) {
IERC20(asset).safeTransferFrom(
Expand All @@ -278,5 +279,12 @@ library LoanLib {
poolFeePercentOfInterest
);
}
if (originationFee > 0) {
IERC20(asset).safeTransferFrom(
msg.sender,
poolAdmin,
originationFee
);
}
}
}
59 changes: 57 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 @@ -99,7 +103,8 @@ describe("Loan", () => {
500,
liquidityAsset.address,
500_000,
Math.floor(Date.now() / 1000) + SEVEN_DAYS
Math.floor(Date.now() / 1000) + SEVEN_DAYS,
loanSettings.originationFee
);
const tx2Receipt = await tx2.wait();

Expand Down Expand Up @@ -148,6 +153,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 @@ -1060,6 +1074,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
7 changes: 6 additions & 1 deletion test/support/loan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { deployServiceConfiguration } from "./serviceconfiguration";

const SEVEN_DAYS = 6 * 60 * 60 * 24;

export const DEFAULT_LOAN_SETTINGS = {
originationFee: 0
};

/**
* Deploy a loan
*/
Expand Down Expand Up @@ -42,7 +46,8 @@ export async function deployLoan(
500,
liquidityAsset,
1_000_000,
Math.floor(Date.now() / 1000) + SEVEN_DAYS
Math.floor(Date.now() / 1000) + SEVEN_DAYS,
0
);

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