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 all commits
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
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