diff --git a/contracts/Loan.sol b/contracts/Loan.sol index 8af08276..85bf07ba 100644 --- a/contracts/Loan.sol +++ b/contracts/Loan.sol @@ -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_` @@ -98,7 +99,7 @@ contract Loan is ILoan { address liquidityAsset_, uint256 principal_, uint256 dropDeadTimestamp, - uint256 latePaymentFee_ + ILoanFees memory fees_ ) { _serviceConfiguration = serviceConfiguration; _factory = factory; @@ -113,7 +114,7 @@ contract Loan is ILoan { apr = apr_; liquidityAsset = liquidityAsset_; principal = principal_; - latePaymentFee = latePaymentFee_; + fees = fees_; LoanLib.validateLoan( serviceConfiguration, @@ -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); } /** @@ -312,7 +321,7 @@ contract Loan is ILoan { payment, _serviceConfiguration.firstLossFeeBps(), IPool(_pool).poolFeePercentOfInterest(), - latePaymentFee, + fees.latePayment, paymentDueDate ); @@ -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; @@ -342,7 +352,7 @@ contract Loan is ILoan { amount, _serviceConfiguration.firstLossFeeBps(), IPool(_pool).poolFeePercentOfInterest(), - latePaymentFee, + fees.latePayment, paymentDueDate ); @@ -351,7 +361,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 383acf38..b3287996 100644 --- a/contracts/LoanFactory.sol +++ b/contracts/LoanFactory.sol @@ -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, @@ -59,7 +59,7 @@ contract LoanFactory { liquidityAsset, principal, dropDeadDate, - latePaymentFee + 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/contracts/libraries/LoanLib.sol b/contracts/libraries/LoanLib.sol index dcfa8d40..de0b8740 100644 --- a/contracts/libraries/LoanLib.sol +++ b/contracts/libraries/LoanLib.sol @@ -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( @@ -287,5 +288,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 2b4b9ff0..6edce602 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(); @@ -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(); @@ -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( @@ -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) => { diff --git a/test/support/loan.ts b/test/support/loan.ts index 8985a4e7..9caf855a 100644 --- a/test/support/loan.ts +++ b/test/support/loan.ts @@ -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 */ @@ -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();