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

VAL-143 Snapshot the pool on all loan payments #186

Merged
merged 5 commits into from
Feb 17, 2023
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
3 changes: 2 additions & 1 deletion contracts/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ contract Loan is ILoan, BeaconImplementation {
atState(ILoanLifeCycleState.Active)
{
require(paymentsRemaining > 0, "Loan: No more payments remain");

IPool(_pool).onLoanWillMakePayment();
ILoanFees memory _fees = LoanLib.previewFees(
settings,
payment,
Expand Down Expand Up @@ -456,6 +456,7 @@ contract Loan is ILoan, BeaconImplementation {
onlyBorrower
atState(ILoanLifeCycleState.Active)
{
IPool(_pool).onLoanWillMakePayment();
uint256 scalingValue = LoanLib.RAY;

if (settings.loanType == ILoanType.Open) {
Expand Down
8 changes: 8 additions & 0 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,14 @@ contract Pool is IPool, ERC20Upgradeable, BeaconImplementation {
_accountings.totalFirstLossApplied += firstLossApplied;
}

/**
* @inheritdoc IPool
*/
function onLoanWillMakePayment() external override {
require(_activeLoans.contains(msg.sender), "Pool: caller not loan");
_performSnapshot();
}

/**
* @inheritdoc IPool
*/
Expand Down
5 changes: 5 additions & 0 deletions contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ interface IPool is IERC4626, IRequestWithdrawable {
*/
function onLoanDefaulted(address loan, uint256 firstLossApplied) external;

/**
* @dev Called by an active loan, this notifies the Pool that payment will be made.
*/
function onLoanWillMakePayment() external;

/**
* @dev Called by the Pool Controller, it transfers the fixed fee
*/
Expand Down
95 changes: 89 additions & 6 deletions test/Loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ describe("Loan", () => {
await getCommonSigners();

// Create a pool
const { pool, poolController, liquidityAsset, serviceConfiguration } =
await deployPool({
poolAdmin: poolAdmin,
settings: poolSettings,
pauser
});
const {
pool,
poolController,
withdrawController,
liquidityAsset,
serviceConfiguration
} = await deployPool({
poolAdmin: poolAdmin,
settings: poolSettings,
pauser
});

await activatePool(pool, poolAdmin, liquidityAsset);

Expand Down Expand Up @@ -107,6 +112,7 @@ describe("Loan", () => {
pool,
deployer,
poolController,
withdrawController,
loan,
loanLib,
loanFactory,
Expand Down Expand Up @@ -1200,6 +1206,83 @@ describe("Loan", () => {
});
});

describe("PoolSnapshots", () => {
it("triggers a snapshot of the pool when completing the next payment", async () => {
const {
borrower,
collateralAsset,
liquidityAsset,
loan,
pool,
poolController,
withdrawController,
poolAdmin
} = await loadFixture(deployFixture);

// 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());

// Advance to 2nd withdraw period
expect(await withdrawController.withdrawPeriod()).to.equal(0);
await time.increase(
(
await pool.settings()
).withdrawRequestPeriodDuration
);
expect(await withdrawController.withdrawPeriod()).to.equal(1);

await liquidityAsset.connect(borrower).approve(loan.address, 2083);
await expect(loan.connect(borrower).completeNextPayment()).to.emit(
pool,
"PoolSnapshotted"
);
});

it("triggers a snapshot of the pool when completing the full payment", async () => {
const {
borrower,
collateralAsset,
liquidityAsset,
loan,
pool,
poolController,
withdrawController,
poolAdmin
} = await loadFixture(deployFixture);

// 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());

// Advance to 2nd withdraw period
expect(await withdrawController.withdrawPeriod()).to.equal(0);
await time.increase(
(
await pool.settings()
).withdrawRequestPeriodDuration
);
expect(await withdrawController.withdrawPeriod()).to.equal(1);

await liquidityAsset
.connect(borrower)
.approve(loan.address, 12498 + 500_000);
await liquidityAsset.mint(borrower.address, 12498);
await expect(loan.connect(borrower).completeFullPayment()).to.emit(
pool,
"PoolSnapshotted"
);
});
});

describe("payments", () => {
it("reverts if the protocol is paused", async () => {
const {
Expand Down
26 changes: 17 additions & 9 deletions test/scenarios/business/1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ describe("Business Scenario 1", () => {
"MUSDC",
6
);
const { pool, serviceConfiguration, poolController } = await deployPool({
poolAdmin: poolAdmin,
settings: poolSettings,
liquidityAsset: mockUSDC
});
const { pool, serviceConfiguration, poolController, withdrawController } =
await deployPool({
poolAdmin: poolAdmin,
settings: poolSettings,
liquidityAsset: mockUSDC
});

// Confirm FL fee is set to 5%
expect(await serviceConfiguration.firstLossFeeBps()).to.equal(500);
Expand Down Expand Up @@ -115,6 +116,7 @@ describe("Business Scenario 1", () => {
startTime,
pool,
poolController,
withdrawController,
lenderA,
lenderB,
mockUSDC,
Expand All @@ -131,6 +133,7 @@ describe("Business Scenario 1", () => {
startTime,
pool,
poolController,
withdrawController,
lenderA,
lenderB,
mockUSDC,
Expand Down Expand Up @@ -191,11 +194,15 @@ describe("Business Scenario 1", () => {
);
await loanOne.connect(borrowerOne).completeFullPayment();

// +14 days, request full withdrawal at start of 2nd window
await advanceToDay(startTime, 14);
// +15 days, request full withdrawal at start of 2nd window
Copy link
Contributor Author

@ams9198 ams9198 Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually a bug with the existing test -- +14 days didn't actually advance us to the expected window (the next one). This only became apparent with the loan payments triggering a snapshot.

expect(await withdrawController.withdrawPeriod()).to.equal(0);
await advanceToDay(startTime, 15);
expect(await withdrawController.withdrawPeriod()).to.equal(1);

await pool
.connect(lenderA)
.requestRedeem(await pool.maxRedeemRequest(lenderA.address));

await pool
.connect(lenderB)
.requestRedeem(await pool.maxRedeemRequest(lenderB.address));
Expand All @@ -208,10 +215,11 @@ describe("Business Scenario 1", () => {
loanTwo.address,
INPUTS.loanTwoPayment + INPUTS.loanTwo.principal
);

await loanTwo.connect(borrowerTwo).completeFullPayment();

// Request window is 14 days, so fast forward to +28 days to claim in next window
await advanceToDay(startTime, 28);
// Request window is 14 days, so fast forward to +29 days to claim in next window
await advanceToDay(startTime, 29);
await pool.snapshot();
await pool.connect(lenderA).claimSnapshots(10);
await pool.connect(lenderB).claimSnapshots(10);
Expand Down
8 changes: 4 additions & 4 deletions test/scenarios/business/permissioned/1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ describe("Permissioned Business Scenario 1", () => {
);
await loanOne.connect(borrowerOne).completeFullPayment();

// +14 days, request full withdrawal at start of 2nd window
await advanceToDay(startTime, 14);
// +15 days, request full withdrawal at start of 2nd window
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

await advanceToDay(startTime, 15);
await performVeriteVerification(poolAccessControl, poolAdmin, lenderA);
await pool
.connect(lenderA)
Expand All @@ -263,8 +263,8 @@ describe("Permissioned Business Scenario 1", () => {
);
await loanTwo.connect(borrowerTwo).completeFullPayment();

// Request window is 14 days, so fast forward to +28 days to claim in next window
await advanceToDay(startTime, 28);
// Request window is 14 days, so fast forward to +29 days to claim in next window
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

await advanceToDay(startTime, 29);
await performVeriteVerification(poolAccessControl, poolAdmin, lenderA);
await pool.connect(lenderA).snapshot();
await pool.connect(lenderA).claimSnapshots(1);
Expand Down
49 changes: 49 additions & 0 deletions test/scenarios/pool/snapshot-variations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,4 +627,53 @@ describe("Snapshot Variations", () => {
DEPOSIT_AMOUNT // 1:1, even though interest was paid back to the pool, making shares more valuable
);
});

it("eligible balances should not accumulate interest as active loans are repaid with no other pool activity", async () => {
const {
pool,
loan,
borrower,
aliceLender,
bobLender,
liquidityAsset,
poolAdmin,
withdrawController,
poolController,
withdrawRequestPeriodDuration
} = await loadFixture(loadPoolFixture);

await poolController.connect(poolAdmin).setWithdrawGate(10_000);
await poolController.connect(poolAdmin).setRequestFee(0);

// activate the pool
await activatePool(pool, poolAdmin, liquidityAsset);

// deposit 1M tokens from Alice and Bob
await depositToPool(pool, aliceLender, liquidityAsset, DEPOSIT_AMOUNT);
await depositToPool(pool, bobLender, liquidityAsset, DEPOSIT_AMOUNT);

// Request maximum in window 0 for Alice
expect(await withdrawController.withdrawPeriod()).to.equal(0);
await pool.connect(aliceLender).requestRedeem(DEPOSIT_AMOUNT);

// Fund loan immediately in window 0
await fundLoan(loan, poolController, poolAdmin);
await liquidityAsset.mint(borrower.address, 1_000_000); // mint extra to pay back interest
await liquidityAsset.connect(borrower).approve(loan.address, 2_000_000);
await loan.connect(borrower).drawdown(await loan.principal());

// Make payback loan in window 1
await time.increase(withdrawRequestPeriodDuration);
expect(await withdrawController.withdrawPeriod()).to.equal(1);
await loan.connect(borrower).completeFullPayment();

// Claim
// Since the lender requested a full redeem, and there's 100% liquidity gate,
// the full requested amount should be serviced at whenever the next snapshot is.
// We expect that the interest accrued in the intervening time should not accrue to the lender.
await pool.connect(aliceLender).claimSnapshots(1);
expect(await pool.maxWithdraw(aliceLender.address)).to.equal(
DEPOSIT_AMOUNT // 1:1, even though interest was paid back to the pool, making shares more valuable
);
});
});