Skip to content

Commit

Permalink
Allow Pool Admin to modify pool fees (#120)
Browse files Browse the repository at this point in the history
* Rename poolFeePercentOfInterest to serviceFeeBps

* Fill out some documentation on the IPoolConfigurableSettings

* Pool Admin can update service fee mid-cycle

* Allow Pool Admin to change fixed fee of pool mid cycle

* Fix formatting

* Fix format
  • Loading branch information
bricestacey authored Dec 2, 2022
1 parent 0e5ecdd commit 7b0e227
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 26 deletions.
6 changes: 3 additions & 3 deletions contracts/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ contract Loan is ILoan {
settings,
payment,
_serviceConfiguration.firstLossFeeBps(),
IPool(_pool).poolFeePercentOfInterest(),
IPool(_pool).serviceFeeBps(),
block.timestamp,
paymentDueDate,
RAY
Expand Down Expand Up @@ -407,7 +407,7 @@ contract Loan is ILoan {
settings,
amount,
_serviceConfiguration.firstLossFeeBps(),
IPool(_pool).poolFeePercentOfInterest(),
IPool(_pool).serviceFeeBps(),
block.timestamp,
paymentDueDate,
RAY
Expand Down Expand Up @@ -447,7 +447,7 @@ contract Loan is ILoan {
settings,
payment,
_serviceConfiguration.firstLossFeeBps(),
IPool(_pool).poolFeePercentOfInterest(),
IPool(_pool).serviceFeeBps(),
block.timestamp,
paymentDueDate,
scalingValue
Expand Down
4 changes: 2 additions & 2 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ contract Pool is IPool, ERC20 {
/**
* @dev The fee
*/
function poolFeePercentOfInterest() external view returns (uint256) {
return settings().poolFeePercentOfInterest;
function serviceFeeBps() external view returns (uint256) {
return settings().serviceFeeBps;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions contracts/controllers/PoolController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,27 @@ contract PoolController is IPoolController {
return _liquidityAsset.balanceOf(address(_firstLossVault));
}

/**
* @inheritdoc IPoolController
*/
function setServiceFeeBps(uint256 serviceFeeBps) external onlyAdmin {
require(serviceFeeBps <= 10000, "Pool: invalid service fee");
_settings.serviceFeeBps = serviceFeeBps;
emit PoolSettingsUpdated();
}

/**
* @inheritdoc IPoolController
*/
function setFixedFee(uint256 amount, uint256 interval) external onlyAdmin {
if (amount > 0) {
require(interval > 0, "Pool: invalid fixed fee");
}
_settings.fixedFee = amount;
_settings.fixedFeeInterval = interval;
emit PoolSettingsUpdated();
}

/*//////////////////////////////////////////////////////////////
State
//////////////////////////////////////////////////////////////*/
Expand Down
20 changes: 15 additions & 5 deletions contracts/controllers/interfaces/IPoolController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ struct IPoolConfigurableSettings {
uint256 endDate; // epoch seconds
uint256 requestFeeBps; // bips
uint256 requestCancellationFeeBps; // bips
uint256 withdrawGateBps; // Percent of liquidity pool available to withdraw, represented in BPS
uint256 firstLossInitialMinimum; // amount
uint256 withdrawGateBps; // bips, percent of liquidity pool available to withdraw
uint256 serviceFeeBps; // bips, percent taken from borrower payments to be paid to pool admin
uint256 firstLossInitialMinimum; // amount of tokens to be deposited to first loss before a pool is active
uint256 withdrawRequestPeriodDuration; // seconds (e.g. 30 days)
uint256 fixedFee;
uint256 fixedFeeInterval;
uint256 poolFeePercentOfInterest; // bips
uint256 fixedFee; // amount of tokens the pool admin can claim every interval
uint256 fixedFeeInterval; // interval in days at which a pool admin can claim fixed fees from the pool
}

/**
Expand Down Expand Up @@ -67,6 +67,16 @@ interface IPoolController {
view
returns (IPoolConfigurableSettings memory);

/**
* @dev Allow the current pool admin to update the service fee.
*/
function setServiceFeeBps(uint256) external;

/**
* @dev Allow the current pool admin to update the fixed fee.
*/
function setFixedFee(uint256 amount, uint256 interval) external;

/**
* @dev Allow the current pool admin to update the pool fees
* before the pool has been activated.
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ interface IPool is IERC4626 {
/**
* @dev The pool fee, in bps, taken from each interest payment
*/
function poolFeePercentOfInterest() external view returns (uint256);
function serviceFeeBps() external view returns (uint256);

/**
* @dev Submits a withdrawal request, incurring a fee.
Expand Down
4 changes: 2 additions & 2 deletions contracts/libraries/LoanLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ library LoanLib {
ILoanSettings calldata settings,
uint256 payment,
uint256 firstLoss,
uint256 poolFeePercentOfInterest,
uint256 serviceFeeBps,
uint256 blockTimestamp,
uint256 paymentDueDate,
uint256 scalingValue
Expand All @@ -342,7 +342,7 @@ library LoanLib {
ILoanFees memory fees;
fees.payment = payment;
fees.firstLossFee = previewFirstLossFee(payment, firstLoss);
fees.serviceFee = previewServiceFee(payment, poolFeePercentOfInterest);
fees.serviceFee = previewServiceFee(payment, serviceFeeBps);
fees.originationFee = previewOriginationFee(settings, scalingValue);
fees.latePaymentFee = previewLatePaymentFee(
settings,
Expand Down
55 changes: 52 additions & 3 deletions test/Loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe("Loan", () => {

async function deployFixturePoolFees() {
const poolSettings = Object.assign({}, DEFAULT_POOL_SETTINGS, {
poolFeePercentOfInterest: 100
serviceFeeBps: 100
});
return deployFixture(poolSettings);
}
Expand Down Expand Up @@ -138,7 +138,7 @@ describe("Loan", () => {
principal: 500_000,
loanType: 1,
originationBps: 100,
poolFeePercentOfInterest: 100
serviceFeeBps: 100
})
);
}
Expand Down Expand Up @@ -1124,7 +1124,7 @@ describe("Loan", () => {
.postFungibleCollateral(collateralAsset.address, 100);
await poolController.connect(poolAdmin).fundLoan(loan.address);
await loan.connect(borrower).drawdown(await loan.principal());
expect(await pool.poolFeePercentOfInterest()).to.equal(100);
expect(await pool.serviceFeeBps()).to.equal(100);

// Make payment
const firstLoss = await poolController.firstLossVault();
Expand All @@ -1143,6 +1143,55 @@ describe("Loan", () => {
expect(newDueDate).to.equal(dueDate.add(THIRTY_DAYS));
});

it("pool service fees can change between payments", async () => {
const {
borrower,
collateralAsset,
liquidityAsset,
loan,
pool,
poolController,
poolAdmin
} = await loadFixture(deployFixturePoolFees);

// Setup
await collateralAsset.connect(borrower).approve(loan.address, 100);
await loan
.connect(borrower)
.postFungibleCollateral(collateralAsset.address, 100);
await poolController.connect(poolAdmin).fundLoan(loan.address);
await loan.connect(borrower).drawdown(await loan.principal());
expect(await pool.serviceFeeBps()).to.equal(100);

// Make payment
const firstLoss = await poolController.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);
const tx = loan.connect(borrower).completeNextPayment();
await expect(tx).to.not.be.reverted;
await expect(tx).to.changeTokenBalance(liquidityAsset, borrower, -2083);
await expect(tx).to.changeTokenBalance(liquidityAsset, pool, 1959);
await expect(tx).to.changeTokenBalance(liquidityAsset, feeVault, 20);
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));

// Change Service Fee
poolController.connect(poolAdmin).setServiceFeeBps(200);

// Make second payment, service fee is doubled and all other fees remain the same
await liquidityAsset.connect(borrower).approve(loan.address, 2083);
const tx2 = loan.connect(borrower).completeNextPayment();
await expect(tx2).to.not.be.reverted;
await expect(tx2).to.changeTokenBalance(liquidityAsset, borrower, -2083);
await expect(tx2).to.changeTokenBalance(liquidityAsset, pool, 1938);
await expect(tx2).to.changeTokenBalance(liquidityAsset, feeVault, 41);
await expect(tx2).to.changeTokenBalance(liquidityAsset, firstLoss, 104);
});

it("can collect origination fees from the next payment", async () => {
const {
borrower,
Expand Down
35 changes: 30 additions & 5 deletions test/Pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { deployLoan, collateralizeLoan, fundLoan } from "./support/loan";

describe("Pool", () => {
const ONE_DAY = 86400;

async function loadPoolFixture() {
const [operator, poolAdmin, borrower, otherAccount, ...otherAccounts] =
await ethers.getSigners();
Expand Down Expand Up @@ -1200,15 +1202,38 @@ describe("Pool", () => {
const { pool, poolAdmin, poolController, liquidityAsset, otherAccount } =
await loadFixture(loadPoolFixture);

// Set fixed fee to 100 tokens every 30 days
await poolController.connect(poolAdmin).setFixedFee(100, 30);

await activatePool(pool, poolAdmin, liquidityAsset);
await depositToPool(pool, otherAccount, liquidityAsset, 1_000_000);

const { withdrawRequestPeriodDuration } = await pool.settings();
await time.increase(withdrawRequestPeriodDuration);
// Fast forward 1 interval
const { fixedFeeInterval } = await pool.settings();
await time.increase(fixedFeeInterval.mul(ONE_DAY));

await expect(poolController.connect(poolAdmin).claimFixedFee()).to.emit(
pool,
"PoolCranked"
// Claim the fixed fee
const tx = poolController.connect(poolAdmin).claimFixedFee();
await expect(tx).to.emit(pool, "PoolCranked");
await expect(tx).to.changeTokenBalance(
liquidityAsset,
poolAdmin.address,
100
);

// Set fixed fee to 200 tokens every 30 days
await poolController.connect(poolAdmin).setFixedFee(200, 30);

// Fast forward 1 interval
await time.increase(fixedFeeInterval.mul(ONE_DAY));

//
const tx2 = poolController.connect(poolAdmin).claimFixedFee();
await expect(tx2).to.emit(pool, "PoolCranked");
await expect(tx2).to.changeTokenBalance(
liquidityAsset,
poolAdmin.address,
200
);
});
});
Expand Down
64 changes: 64 additions & 0 deletions test/controllers/PoolController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,70 @@ describe("PoolController", () => {
});
});

describe("setServiceFeeBps()", () => {
it("allows change the pool service fee", async () => {
const { poolController, poolAdmin } = await loadFixture(loadPoolFixture);

expect((await poolController.settings()).serviceFeeBps).to.equal(0);

const tx = poolController.connect(poolAdmin).setServiceFeeBps(500);

await expect(tx).to.emit(poolController, "PoolSettingsUpdated");
expect((await poolController.settings()).serviceFeeBps).to.equal(500);
});

it("reverts if set above 10,000", async () => {
const { poolController, poolAdmin } = await loadFixture(loadPoolFixture);

const tx = poolController.connect(poolAdmin).setServiceFeeBps(10_000);
await expect(tx).to.not.be.reverted;

const tx2 = poolController.connect(poolAdmin).setServiceFeeBps(10_001);
await expect(tx2).to.be.revertedWith("Pool: invalid service fee");
});

it("reverts if not called by Pool Admin", async () => {
const { poolController, otherAccount } = await loadFixture(
loadPoolFixture
);

const tx = poolController.connect(otherAccount).setServiceFeeBps(0);
await expect(tx).to.be.revertedWith("Pool: caller is not admin");
});
});

describe("setFixedFee()", () => {
it("changes the pool fixed fee", async () => {
const { poolController, poolAdmin } = await loadFixture(loadPoolFixture);

expect((await poolController.settings()).fixedFee).to.equal(0);
expect((await poolController.settings()).fixedFeeInterval).to.equal(0);

const tx = poolController.connect(poolAdmin).setFixedFee(100, 1);

await expect(tx).to.emit(poolController, "PoolSettingsUpdated");
expect((await poolController.settings()).fixedFee).to.equal(100);
expect((await poolController.settings()).fixedFeeInterval).to.equal(1);
});

it("reverts if the amount is greater than 0 and the interval is 0", async () => {
const { poolController, poolAdmin } = await loadFixture(loadPoolFixture);

const tx = poolController.connect(poolAdmin).setFixedFee(100, 0);

await expect(tx).to.be.revertedWith("Pool: invalid fixed fee");
});

it("reverts if not called by Pool Admin", async () => {
const { poolController, otherAccount } = await loadFixture(
loadPoolFixture
);

const tx = poolController.connect(otherAccount).setFixedFee(100, 1);
await expect(tx).to.be.revertedWith("Pool: caller is not admin");
});
});

describe("state()", () => {
it("is closed when pool end date passes", async () => {
const { poolController } = await loadFixture(loadPoolFixture);
Expand Down
2 changes: 1 addition & 1 deletion test/scenarios/business/1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Business Scenario 1", () => {
withdrawRequestPeriodDuration: 14 * 24 * 60 * 60, // 14 days
fixedFee: 0,
fixedFeeInterval: 0,
poolFeePercentOfInterest: 0
serviceFeeBps: 0
},
loanOne: {
duration: 7,
Expand Down
2 changes: 1 addition & 1 deletion test/scenarios/business/2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Business Scenario 2", () => {
withdrawRequestPeriodDuration: 7 * 24 * 60 * 60, // 7 days
fixedFee: 0,
fixedFeeInterval: 0,
poolFeePercentOfInterest: 2_000 // 20%
serviceFeeBps: 2_000 // 20%
},
loan: {
duration: 14,
Expand Down
2 changes: 1 addition & 1 deletion test/scenarios/business/3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Business Scenario 3", () => {
withdrawRequestPeriodDuration: 7 * 24 * 60 * 60, // 7 days
fixedFee: 0,
fixedFeeInterval: 0,
poolFeePercentOfInterest: 2_000 // 20%
serviceFeeBps: 2_000 // 20%
},
loan: {
duration: 14,
Expand Down
2 changes: 1 addition & 1 deletion test/scenarios/business/4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Business Scenario 4", () => {
withdrawRequestPeriodDuration: 7 * 24 * 60 * 60, // 7 days
fixedFee: 0,
fixedFeeInterval: 0,
poolFeePercentOfInterest: 2_000 // 20%
serviceFeeBps: 2_000 // 20%
},
loan: {
duration: 28,
Expand Down
2 changes: 1 addition & 1 deletion test/support/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const DEFAULT_POOL_SETTINGS = {
withdrawRequestPeriodDuration: 30 * 24 * 60 * 60, // 30 days
fixedFee: 0,
fixedFeeInterval: 0,
poolFeePercentOfInterest: 0 // bps (0%)
serviceFeeBps: 0 // bps (0%)
};

type DeployPoolProps = {
Expand Down

0 comments on commit 7b0e227

Please sign in to comment.