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-39 Apply first-loss to the pool in case of default #49

Merged
merged 4 commits into from
Oct 17, 2022
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
10 changes: 7 additions & 3 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,13 @@ contract Pool is IPool, ERC20 {
"Pool: invalid loan"
);

ILoan(loan).markDefaulted();
_accountings.activeLoanPrincipals -= ILoan(loan).principal();
emit LoanDefaulted(loan);
PoolLib.executeDefault(
asset(),
address(_firstLossVault),
loan,
address(this),
_accountings
);
}

/*//////////////////////////////////////////////////////////////
Expand Down
6 changes: 3 additions & 3 deletions contracts/interfaces/ILoan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,17 @@ interface ILoan {
/**
* @dev Number of payments remaining
*/
function paymentsRemaining() external returns (uint256);
function paymentsRemaining() external view returns (uint256);
Copy link
Contributor Author

@ams9198 ams9198 Oct 17, 2022

Choose a reason for hiding this comment

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

Pretty sure these can / should be view, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice call


/**
* @dev Amount expected in each payment
*/
function payment() external returns (uint256);
function payment() external view returns (uint256);

/**
* @dev Due date for the next payment
*/
function paymentDueDate() external returns (uint256);
function paymentDueDate() external view returns (uint256);

function postFungibleCollateral(address asset, uint256 amount)
external
Expand Down
9 changes: 9 additions & 0 deletions contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ interface IPool is IERC4626 {
*/
event PoolSettingsUpdated(IPoolConfigurableSettings settings);

/**
* @dev Emitted when first loss capital is used to cover loan defaults
*/
event FirstLossApplied(
address indexed loan,
uint256 amount,
uint256 outstandingLoss
);

/**
* @dev Returns the current pool lifecycle state.
*/
Expand Down
90 changes: 70 additions & 20 deletions contracts/libraries/PoolLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ library PoolLib {
uint256 shares
);

/**
* @dev See IPool
*/
event FirstLossApplied(
address indexed loan,
uint256 amount,
uint256 outstandingLosses
);

/**
* @dev Determines whether an address corresponds to a pool loan
* @param loan address of loan
* @param serviceConfiguration address of service configuration
* @param pool address of pool
*/
function isPoolLoan(
address loan,
address serviceConfiguration,
address pool
) public view returns (bool) {
address factory = ILoan(loan).factory();
return
IServiceConfiguration(serviceConfiguration).isLoanFactory(
factory
) &&
LoanFactory(factory).isLoan(loan) &&
ILoan(loan).pool() == pool;
}

/**
* @dev See IPool for event definition
*/
Expand Down Expand Up @@ -216,6 +245,47 @@ library PoolLib {
return shares;
}

/**
* @dev Executes a default, supplying first-loss to cover losses.
* @param asset Pool liquidity asset
* @param firstLossVault Vault holding first-loss capital
* @param loan Address of loan in default
* @param accountings Pool accountings to update
*/
function executeDefault(
address asset,
address firstLossVault,
address loan,
address pool,
IPoolAccountings storage accountings
) external {
ILoan(loan).markDefaulted();
accountings.activeLoanPrincipals -= ILoan(loan).principal();

uint256 firstLossBalance = IERC20(asset).balanceOf(
address(firstLossVault)
);

// TODO - handle open-term loans where principal may
// not be fully oustanding.
uint256 outstandingLoanDebt = ILoan(loan).principal() +
ILoan(loan).paymentsRemaining() *
ILoan(loan).payment();

uint256 firstLossRequired = firstLossBalance >= outstandingLoanDebt
? outstandingLoanDebt
: firstLossBalance;

FirstLossVault(firstLossVault).withdraw(firstLossRequired, pool);

emit LoanDefaulted(loan);
emit FirstLossApplied(
loan,
firstLossRequired,
outstandingLoanDebt.sub(firstLossRequired)
);
}

/*//////////////////////////////////////////////////////////////
Withdrawal Request Methods
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -245,24 +315,4 @@ library PoolLib {
) public view returns (uint256) {
return currentWithdrawPeriod(activatedAt, withdrawalWindowDuration) + 1;
}

/**
* @dev Determines whether an address corresponds to a pool loan
* @param loan address of loan
* @param serviceConfiguration address of service configuration
* @param pool address of pool
*/
function isPoolLoan(
address loan,
address serviceConfiguration,
address pool
) public view returns (bool) {
address factory = ILoan(loan).factory();
return
IServiceConfiguration(serviceConfiguration).isLoanFactory(
factory
) &&
LoanFactory(factory).isLoan(loan) &&
ILoan(loan).pool() == pool;
}
}
41 changes: 34 additions & 7 deletions test/Pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,22 +232,49 @@ describe("Pool", () => {
await depositToPool(pool, otherAccount, liquidityAsset, loanPrincipal);
await fundLoan(loan, pool, poolManager);

// Confirm that pool liquidity reserve is now empty
expect(await liquidityAsset.balanceOf(pool.address)).to.equal(0);

// Get an accounting snapshot prior to the default
const activeLoanPrincipalBefore = (await pool.accountings())
.activeLoanPrincipals;
const firstLossAvailable = await pool.firstLoss();

// Trigger default
await expect(pool.connect(poolManager).defaultLoan(loan.address)).to.emit(
pool,
"LoanDefaulted"
// Expected loan outstanding stand = principal + numberPayments * payments
const loanPaymentsRemaining = await loan.paymentsRemaining();
const loanPaymentAmount = await loan.payment();
const loanOustandingDebt = loanPrincipal.add(
loanPaymentsRemaining.mul(loanPaymentAmount)
);

// Confirm that first loss is NOT enough to cover the oustanding loan debt
expect(firstLossAvailable).to.be.lessThan(loanOustandingDebt);

// Trigger default
// Since first-loss is not enough to cover oustanding debt, all of it is used
await expect(pool.connect(poolManager).defaultLoan(loan.address))
.to.emit(pool, "LoanDefaulted")
.withArgs(loan.address)
.to.emit(pool, "FirstLossApplied")
.withArgs(
loan.address,
firstLossAvailable,
loanOustandingDebt.sub(firstLossAvailable)
);

// Check accountings after
const activeLoanPrincipalsAfter = (await pool.accountings())
.activeLoanPrincipals;
expect(activeLoanPrincipalsAfter).is.equal(
// Pool accountings should be updated
expect((await pool.accountings()).activeLoanPrincipals).is.equal(
activeLoanPrincipalBefore.sub(loanPrincipal)
);

// First loss vault should be empty
expect(await pool.firstLoss()).to.equal(0);

// Pool liquidity reserve should now contain the first loss
expect(await liquidityAsset.balanceOf(pool.address)).to.equal(
firstLossAvailable
);
});
});

Expand Down