Skip to content

Commit

Permalink
Fix bugs in first loss cover
Browse files Browse the repository at this point in the history
Fix two bugs:
1. We are not using actual recovered amount when distributing loss recovery to First Loss Covers. We are incorrectly using the original recovery amount.
2. There is a bug in a test helper where the order of loss recovery is reversed.
  • Loading branch information
ljiatu committed Jan 2, 2024
1 parent e1a9622 commit 45729aa
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 27 deletions.
6 changes: 3 additions & 3 deletions contracts/FirstLossCover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ contract FirstLossCover is
(remainingRecovery, recoveredAmount) = _calcLossRecover(coveredLoss, recovery);

if (recoveredAmount > 0) {
poolSafe.withdraw(address(this), recovery);
poolSafe.withdraw(address(this), recoveredAmount);

uint256 currCoveredLoss = coveredLoss;
currCoveredLoss -= recovery;
currCoveredLoss -= recoveredAmount;
coveredLoss = currCoveredLoss;

emit LossRecovered(recovery, currCoveredLoss);
emit LossRecovered(recoveredAmount, currCoveredLoss);
}
}

Expand Down
2 changes: 2 additions & 0 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {ITranchesPolicy} from "./interfaces/ITranchesPolicy.sol";
import {ICreditManager} from "./credit/interfaces/ICreditManager.sol";
import {ICredit} from "./credit/interfaces/ICredit.sol";

import "hardhat/console.sol";

/**
* @title Pool
* @notice Pool is a core contract that connects the lender side (via Tranches)
Expand Down
4 changes: 2 additions & 2 deletions test/BaseTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,12 +695,12 @@ async function calcLossRecovery(
lossRecovery = lossRecovery.sub(juniorRecovery);
const lossRecoveredInFirstLossCovers = [];
let recoveredAmount;
for (const coveredLoss of lossesCoveredByFirstLossCovers) {
for (const coveredLoss of lossesCoveredByFirstLossCovers.slice().reverse()) {
[lossRecovery, recoveredAmount] = calcLossRecoveryForFirstLossCover(
coveredLoss,
lossRecovery,
);
lossRecoveredInFirstLossCovers.push(recoveredAmount);
lossRecoveredInFirstLossCovers.unshift(recoveredAmount);
}

return [
Expand Down
48 changes: 43 additions & 5 deletions test/FirstLossCoverTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ describe("FirstLossCover Tests", function () {
});
});

describe("Loss cover and recover", function () {
describe("Loss cover and recovery", function () {
async function setCoverConfig(coverRatePerLossInBps: BN, coverCapPerLoss: BN) {
await overrideFirstLossCoverConfig(
affiliateFirstLossCoverContract,
Expand All @@ -990,7 +990,7 @@ describe("FirstLossCover Tests", function () {
);
}

describe("Cover loss", function () {
describe("coverLoss", function () {
async function testCoverLoss(
coverRatePerLossInBps: BN,
coverCapPerLoss: BN,
Expand Down Expand Up @@ -1076,15 +1076,15 @@ describe("FirstLossCover Tests", function () {
});
});

describe("Recover loss", function () {
describe("recoverLoss", function () {
let loss: BN;

beforeEach(async function () {
loss = toToken(9_000);
await poolConfigContract.connect(poolOwner).setPool(defaultDeployer.getAddress());
});

it("Should allow the pool to recover the loss", async function () {
it("Should allow the pool to fully recover the loss", async function () {
// Initiate loss coverage so that the loss can be recovered later,
const coverTotalAssets = await affiliateFirstLossCoverContract.totalAssets();
await setCoverConfig(CONSTANTS.BP_FACTOR, coverTotalAssets.add(1_000));
Expand All @@ -1103,7 +1103,45 @@ describe("FirstLossCover Tests", function () {
await affiliateFirstLossCoverContract.coverLoss(loss);

// Make sure the pool safe has enough balance to be transferred from.
const lossRecovery = loss;
const lossRecovery = loss.add(toToken(1));
await mockTokenContract.mint(poolSafeContract.address, lossRecovery);

const amountRecovered = minBigNumber(amountLossCovered, lossRecovery);
const oldCoveredLoss = await affiliateFirstLossCoverContract.coveredLoss();
const newCoveredLoss = oldCoveredLoss.sub(amountRecovered);
const oldPoolSafeAssets = await poolSafeContract.totalBalance();

await expect(affiliateFirstLossCoverContract.recoverLoss(lossRecovery))
.to.emit(affiliateFirstLossCoverContract, "LossRecovered")
.withArgs(amountRecovered, newCoveredLoss);
expect(await poolSafeContract.totalBalance()).to.equal(
oldPoolSafeAssets.sub(amountRecovered),
);
expect(await affiliateFirstLossCoverContract.coveredLoss()).to.equal(
newCoveredLoss,
);
});

it("Should allow the pool to partially recover the loss", async function () {
// Initiate loss coverage so that the loss can be recovered later,
const coverTotalAssets = await affiliateFirstLossCoverContract.totalAssets();
await setCoverConfig(CONSTANTS.BP_FACTOR, coverTotalAssets.add(1_000));
const config = await poolConfigContract.getFirstLossCoverConfig(
affiliateFirstLossCoverContract.address,
);
const amountLossCovered = minBigNumber(
loss.mul(config.coverRatePerLossInBps).div(CONSTANTS.BP_FACTOR),
config.coverCapPerLoss,
coverTotalAssets,
);
await mockTokenContract.mint(
affiliateFirstLossCoverContract.address,
amountLossCovered,
);
await affiliateFirstLossCoverContract.coverLoss(loss);

// Make sure the pool safe has enough balance to be transferred from.
const lossRecovery = loss.sub(toToken(1));
await mockTokenContract.mint(poolSafeContract.address, lossRecovery);

const amountRecovered = minBigNumber(amountLossCovered, lossRecovery);
Expand Down
158 changes: 142 additions & 16 deletions test/PoolTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let poolOwner: SignerWithAddress,
poolOwnerTreasury: SignerWithAddress,
evaluationAgent: SignerWithAddress,
poolOperator: SignerWithAddress;
let lender: SignerWithAddress;
let borrower: SignerWithAddress, lender: SignerWithAddress;

let eaNFTContract: EvaluationAgentNFT,
humaConfigContract: HumaConfig,
Expand Down Expand Up @@ -82,6 +82,7 @@ describe("Pool Test", function () {
poolOwnerTreasury,
evaluationAgent,
poolOperator,
borrower,
lender,
] = await ethers.getSigners();
});
Expand Down Expand Up @@ -315,6 +316,7 @@ describe("Pool Test", function () {
});

describe("PnL tests", function () {
let firstLossCovers: FirstLossCover[];
let coverTotalAssets: BN;

async function prepareForPnL() {
Expand All @@ -326,15 +328,29 @@ describe("Pool Test", function () {
await seniorTrancheVaultContract
.connect(lender)
.deposit(seniorDepositAmount, lender.address);

// Override the config so that first loss covers cover
// all losses up to the amount of their total assets.
const firstLossCovers = [
firstLossCovers = [
borrowerFirstLossCoverContract,
affiliateFirstLossCoverContract,
];
coverTotalAssets = sumBNArray(
await Promise.all(firstLossCovers.map((cover) => cover.totalAssets())),
);
// Make sure both first loss covers have some assets.
await mockTokenContract.mint(borrower.getAddress(), toToken(1_000_000_000));
await mockTokenContract
.connect(borrower)
.approve(borrowerFirstLossCoverContract.address, ethers.constants.MaxUint256);
await borrowerFirstLossCoverContract
.connect(poolOwner)
.addCoverProvider(borrower.getAddress());
await borrowerFirstLossCoverContract
.connect(borrower)
.depositCover(toToken(100_000));
const borrowerFLCAssets = await borrowerFirstLossCoverContract.totalAssets();
expect(borrowerFLCAssets).to.be.gt(0);
const affiliateFLCAssets = await affiliateFirstLossCoverContract.totalAssets();
expect(affiliateFLCAssets).to.be.gt(0);
coverTotalAssets = borrowerFLCAssets.add(affiliateFLCAssets);
for (const [index, cover] of firstLossCovers.entries()) {
await overrideFirstLossCoverConfig(
cover,
Expand Down Expand Up @@ -559,7 +575,6 @@ describe("Pool Test", function () {
(contract) => getFirstLossCoverInfo(contract, poolConfigContract),
),
);

newFirstLossCoverInfos.forEach((info, index) => {
expect(info.asset).to.equal(
firstLossCoverInfos[index].asset.add(
Expand All @@ -574,34 +589,63 @@ describe("Pool Test", function () {
const profit = toToken(12387);
const loss = toToken(0);
const recovery = toToken(0);

await testDistribution(profit, loss, recovery);
});

it("Should distribute loss correctly when first loss covers can cover loss", async function () {
const assets = await poolContract.currentTranchesAssets();
const profit = toToken(0),
loss = coverTotalAssets,
loss = coverTotalAssets.sub(toToken(1)),
recovery = toToken(0);

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(
toToken(1),
);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.JUNIOR_TRANCHE],
);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it("Should distribute loss correctly when the junior tranche can cover loss", async function () {
const assets = await poolContract.currentTranchesAssets();
const profit = toToken(0);
const loss = coverTotalAssets.add(assets[CONSTANTS.JUNIOR_TRANCHE]);
const loss = coverTotalAssets
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.sub(toToken(1));
const recovery = toToken(0);

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(toToken(1));
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it("Should distribute loss correctly when the senior tranche needs to cover loss", async function () {
const assets = await poolContract.currentTranchesAssets();
const profit = toToken(0);
const loss = coverTotalAssets.add(
assets[CONSTANTS.JUNIOR_TRANCHE].add(assets[CONSTANTS.SENIOR_TRANCHE]),
);
const loss = coverTotalAssets
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.add(assets[CONSTANTS.SENIOR_TRANCHE])
.sub(toToken(1));
const recovery = toToken(0);

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(0);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(toToken(1));
});

it("Should distribute loss recovery correctly when senior loss can be partially recovered", async function () {
Expand All @@ -613,38 +657,120 @@ describe("Pool Test", function () {
const recovery = assets[CONSTANTS.SENIOR_TRANCHE].sub(toToken(1));

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(0);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE].sub(toToken(1)),
);
});

it("Should distribute loss recovery correctly when junior loss can be recovered", async function () {
const assets = await poolContract.currentTranchesAssets();
const profit = toToken(0);
const loss = coverTotalAssets
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.add(assets[CONSTANTS.SENIOR_TRANCHE]);
const recovery = assets[CONSTANTS.SENIOR_TRANCHE].add(toToken(1));

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(toToken(1));
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it("Should distribute loss recovery correctly when the affiliate first loss can be partially recovered", async function () {
const assets = await poolContract.currentTranchesAssets();
const profit = toToken(0);
const loss = coverTotalAssets.add(
assets[CONSTANTS.JUNIOR_TRANCHE].add(assets[CONSTANTS.SENIOR_TRANCHE]),
);
const recovery = assets[CONSTANTS.JUNIOR_TRANCHE].add(
const recovery = assets[CONSTANTS.JUNIOR_TRANCHE]
.add(assets[CONSTANTS.SENIOR_TRANCHE])
.add(toToken(1));

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(0);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(
toToken(1),
);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.JUNIOR_TRANCHE],
);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it.only("Should distribute loss recovery correctly when the borrower first loss can be partially recovered", async function () {
const assets = await poolContract.currentTranchesAssets();
const borrowerFLCAssets = await borrowerFirstLossCoverContract.totalAssets();
const affiliateFLCAssets = await affiliateFirstLossCoverContract.totalAssets();
const profit = toToken(0);
const loss = coverTotalAssets
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.add(assets[CONSTANTS.SENIOR_TRANCHE]);
const recovery = affiliateFLCAssets
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.add(assets[CONSTANTS.SENIOR_TRANCHE])
.add(toToken(1));

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(
toToken(1),
);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(
affiliateFLCAssets,
);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.JUNIOR_TRANCHE],
);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it("Should distribute loss recovery correctly when first loss can be recovered", async function () {
it("Should distribute loss recovery correctly when all loss can be fully recovered", async function () {
const assets = await poolContract.currentTranchesAssets();
const borrowerFLCAssets = await borrowerFirstLossCoverContract.totalAssets();
const affiliateFLCAssets = await affiliateFirstLossCoverContract.totalAssets();
const profit = toToken(0);
const loss = coverTotalAssets.add(
assets[CONSTANTS.JUNIOR_TRANCHE].add(assets[CONSTANTS.SENIOR_TRANCHE]),
);
const recovery = coverTotalAssets.add(
assets[CONSTANTS.JUNIOR_TRANCHE].add(assets[CONSTANTS.SENIOR_TRANCHE]),
);
const recovery = borrowerFLCAssets
.add(affiliateFLCAssets)
.add(assets[CONSTANTS.JUNIOR_TRANCHE])
.add(assets[CONSTANTS.SENIOR_TRANCHE]);

await testDistribution(profit, loss, recovery);

expect(await borrowerFirstLossCoverContract.totalAssets()).to.equal(
borrowerFLCAssets,
);
expect(await affiliateFirstLossCoverContract.totalAssets()).to.equal(
affiliateFLCAssets,
);
expect(await juniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.JUNIOR_TRANCHE],
);
expect(await seniorTrancheVaultContract.totalAssets()).to.equal(
assets[CONSTANTS.SENIOR_TRANCHE],
);
});

it("Should distribute profit, loss and loss recovery correctly", async function () {
const profit = toToken(12387);
const loss = toToken(8493);
const recovery = toToken(3485);

await testDistribution(profit, loss, recovery);
});

Expand Down
Loading

0 comments on commit 45729aa

Please sign in to comment.