From 3cee101a55336d3e01fa7276154fb8d4f334aab0 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Wed, 26 Oct 2022 14:37:55 -0400 Subject: [PATCH 1/2] Add origination fees --- contracts/Loan.sol | 20 +++++++++-- contracts/LoanFactory.sol | 6 ++-- contracts/libraries/LoanLib.sol | 10 +++++- test/Loan.test.ts | 59 +++++++++++++++++++++++++++++++-- test/support/loan.ts | 7 +++- 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/contracts/Loan.sol b/contracts/Loan.sol index 655a68b8..f169e817 100644 --- a/contracts/Loan.sol +++ b/contracts/Loan.sol @@ -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_` @@ -96,7 +98,8 @@ contract Loan is ILoan { uint256 apr_, address liquidityAsset_, uint256 principal_, - uint256 dropDeadTimestamp + uint256 dropDeadTimestamp, + uint256 originationFeeBps_ ) { _serviceConfiguration = serviceConfiguration; _factory = factory; @@ -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); } /** @@ -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; @@ -344,7 +357,8 @@ contract Loan is ILoan { IPool(_pool).firstLossVault(), firstLossFee, IPool(_pool).manager(), - poolFee + poolFee, + originationFee ); LoanLib.completePayment( diff --git a/contracts/LoanFactory.sol b/contracts/LoanFactory.sol index 69e368f3..ab3c7d60 100644 --- a/contracts/LoanFactory.sol +++ b/contracts/LoanFactory.sol @@ -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, @@ -57,7 +58,8 @@ contract LoanFactory { apr, liquidityAsset, principal, - dropDeadDate + dropDeadDate, + originationFee ); address addr = address(loan); emit LoanCreated(addr); diff --git a/contracts/libraries/LoanLib.sol b/contracts/libraries/LoanLib.sol index a80b8fa1..d2109a9f 100644 --- a/contracts/libraries/LoanLib.sol +++ b/contracts/libraries/LoanLib.sol @@ -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( @@ -278,5 +279,12 @@ library LoanLib { poolFeePercentOfInterest ); } + if (originationFee > 0) { + IERC20(asset).safeTransferFrom( + msg.sender, + poolAdmin, + originationFee + ); + } } } diff --git a/test/Loan.test.ts b/test/Loan.test.ts index 97478007..dc45b296 100644 --- a/test/Loan.test.ts +++ b/test/Loan.test.ts @@ -5,6 +5,7 @@ import { DEFAULT_POOL_SETTINGS } from "./support/pool"; import { collateralizeLoan, collateralizeLoanNFT, + DEFAULT_LOAN_SETTINGS, fundLoan, matureLoan } from "./support/loan"; @@ -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(); @@ -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(); @@ -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( @@ -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) => { diff --git a/test/support/loan.ts b/test/support/loan.ts index acf2fdaf..38a17d6a 100644 --- a/test/support/loan.ts +++ b/test/support/loan.ts @@ -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 */ @@ -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(); From 0d7a53564247eb77406052b2074e6d6f34dd1a75 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Fri, 28 Oct 2022 14:55:29 -0400 Subject: [PATCH 2/2] Extract ILoanFees struct This is necessary because we ran out of call stack --- contracts/Loan.sol | 17 +++++++---------- contracts/LoanFactory.sol | 6 ++---- contracts/interfaces/ILoan.sol | 5 +++++ test/Loan.test.ts | 6 ++++-- test/support/loan.ts | 6 +++++- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/contracts/Loan.sol b/contracts/Loan.sol index b6106ad0..85bf07ba 100644 --- a/contracts/Loan.sol +++ b/contracts/Loan.sol @@ -38,9 +38,8 @@ contract Loan is ILoan { uint256 public immutable payment; uint256 public paymentsRemaining; uint256 public paymentDueDate; - uint256 public latePaymentFee; - uint256 public originationFeeBps; uint256 public originationFee; + ILoanFees fees; /** * @dev Modifier that requires the Loan be in the given `state_` @@ -100,8 +99,7 @@ contract Loan is ILoan { address liquidityAsset_, uint256 principal_, uint256 dropDeadTimestamp, - uint256 latePaymentFee_, - uint256 originationFeeBps_ + ILoanFees memory fees_ ) { _serviceConfiguration = serviceConfiguration; _factory = factory; @@ -116,7 +114,7 @@ contract Loan is ILoan { apr = apr_; liquidityAsset = liquidityAsset_; principal = principal_; - latePaymentFee = latePaymentFee_; + fees = fees_; LoanLib.validateLoan( serviceConfiguration, @@ -135,10 +133,9 @@ contract Loan is ILoan { .div(10000); payment = paymentsTotal.mul(RAY).div(paymentsRemaining).div(RAY); - // Persist origination fee and cache the computed value - originationFeeBps = originationFeeBps_; + // Persist origination fee per payment period originationFee = principal - .mul(originationFeeBps) + .mul(fees.originationBps) .mul(duration.mul(RAY).div(360)) .div(paymentsRemaining) .div(RAY) @@ -324,7 +321,7 @@ contract Loan is ILoan { payment, _serviceConfiguration.firstLossFeeBps(), IPool(_pool).poolFeePercentOfInterest(), - latePaymentFee, + fees.latePayment, paymentDueDate ); @@ -355,7 +352,7 @@ contract Loan is ILoan { amount, _serviceConfiguration.firstLossFeeBps(), IPool(_pool).poolFeePercentOfInterest(), - latePaymentFee, + fees.latePayment, paymentDueDate ); diff --git a/contracts/LoanFactory.sol b/contracts/LoanFactory.sol index 8f8f6178..b3287996 100644 --- a/contracts/LoanFactory.sol +++ b/contracts/LoanFactory.sol @@ -41,8 +41,7 @@ contract LoanFactory { address liquidityAsset, uint256 principal, uint256 dropDeadDate, - uint256 latePaymentFee, - uint256 originationFee + ILoanFees memory fees ) public virtual returns (address LoanAddress) { require( _serviceConfiguration.paused() == false, @@ -60,8 +59,7 @@ contract LoanFactory { liquidityAsset, principal, dropDeadDate, - latePaymentFee, - originationFee + fees ); address addr = address(loan); emit LoanCreated(addr); diff --git a/contracts/interfaces/ILoan.sol b/contracts/interfaces/ILoan.sol index 659c6cf9..0c124611 100644 --- a/contracts/interfaces/ILoan.sol +++ b/contracts/interfaces/ILoan.sol @@ -26,6 +26,11 @@ struct ILoanNonFungibleCollateral { uint256 tokenId; } +struct ILoanFees { + uint256 latePayment; + uint256 originationBps; +} + interface ILoan { /** * @dev Emitted when loan is funded. diff --git a/test/Loan.test.ts b/test/Loan.test.ts index 7845c42a..6edce602 100644 --- a/test/Loan.test.ts +++ b/test/Loan.test.ts @@ -104,8 +104,10 @@ describe("Loan", () => { liquidityAsset.address, 500_000, Math.floor(Date.now() / 1000) + SEVEN_DAYS, - 1_000, - loanSettings.originationFee + { + latePayment: 1_000, + originationBps: loanSettings.originationFee + } ); const tx2Receipt = await tx2.wait(); diff --git a/test/support/loan.ts b/test/support/loan.ts index 38a17d6a..9caf855a 100644 --- a/test/support/loan.ts +++ b/test/support/loan.ts @@ -4,6 +4,7 @@ import { deployServiceConfiguration } from "./serviceconfiguration"; const SEVEN_DAYS = 6 * 60 * 60 * 24; export const DEFAULT_LOAN_SETTINGS = { + latePayment: 0, originationFee: 0 }; @@ -47,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();