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

Allow PM withdrawals of first loss once a pool is closed #34

Merged
merged 4 commits into from
Sep 29, 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
4 changes: 2 additions & 2 deletions contracts/FirstLossVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract FirstLossVault {
/**
* @dev Modifier restricting access to pool
*/
modifier isPool() {
modifier onlyPool() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For consistency

require(msg.sender == _pool, "FirstLossVault: caller not pool");
_;
}
Expand Down Expand Up @@ -50,7 +50,7 @@ contract FirstLossVault {
/**
* @dev Allows withdrawal of funds held by vault.
*/
function withdraw(uint256 amount, address receiver) external isPool {
function withdraw(uint256 amount, address receiver) external onlyPool {
require(receiver != address(0), "FirstLossVault: 0 address");
_asset.safeTransfer(receiver, amount);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/Loan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.16;

import "./interfaces/ILoan.sol";
import "./library/LoanLib.sol";
import "./libraries/LoanLib.sol";
import "./CollateralVault.sol";

/**
Expand Down
24 changes: 21 additions & 3 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "./interfaces/IPool.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./library/PoolLib.sol";
import "./libraries/PoolLib.sol";
import "./FirstLossVault.sol";

/**
Expand Down Expand Up @@ -135,14 +135,15 @@ contract Pool is IPool, ERC20 {
/**
* @dev Supplies first-loss to the pool. Can only be called by the Pool Manager.
*/
function supplyFirstLoss(uint256 amount)
function depositFirstLoss(uint256 amount, address spender)
external
onlyManager
atInitializedOrActiveState
{
IPoolLifeCycleState poolLifeCycleState = PoolLib
.executeFirstLossContribution(
.executeFirstLossDeposit(
address(_liquidityAsset),
spender,
amount,
address(_firstLossVault),
_poolLifeCycleState,
Expand All @@ -152,6 +153,23 @@ contract Pool is IPool, ERC20 {
_setPoolLifeCycleState(poolLifeCycleState);
}

/**
* @dev inheritdoc IPool
*/
function withdrawFirstLoss(uint256 amount, address receiver)
external
onlyManager
atState(IPoolLifeCycleState.Closed)
returns (uint256)
{
return
PoolLib.executeFirstLossWithdraw(
amount,
receiver,
address(_firstLossVault)
);
}

/**
* @dev Updates the pool capacity. Can only be called by the Pool Manager.
*/
Expand Down
11 changes: 9 additions & 2 deletions contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,16 @@ interface IPool is IERC4626 {
function accountings() external view returns (IPoolAccountings memory);

/**
* @dev Supplies first-loss to the pool. Can only be called by the Pool Manager.
* @dev Deposits first-loss to the pool. Can only be called by the Pool Manager.
*/
function supplyFirstLoss(uint256 amount) external;
function depositFirstLoss(uint256 amount, address spender) external;

/**
* @dev Withdraws first-loss from the pool. Can only be called by the Pool Manager.
*/
function withdrawFirstLoss(uint256 amount, address receiver)
external
returns (uint256);

/**
* @dev Updates the pool capacity. Can only be called by the Pool Manager.
Expand Down
File renamed without changes.
45 changes: 40 additions & 5 deletions contracts/library/PoolLib.sol → contracts/libraries/PoolLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../interfaces/IPool.sol";
import "../FirstLossVault.sol";

/**
* @title Collection of functions used by the Pool
Expand All @@ -21,9 +22,22 @@ library PoolLib {
event LifeCycleStateTransition(IPoolLifeCycleState state);

/**
* @dev Emitted when first loss is supplied to the pool.
* @dev Emitted when first loss is deposited to the pool.
*/
event FirstLossSupplied(address indexed supplier, uint256 amount);
event FirstLossDeposited(
address indexed caller,
address indexed spender,
uint256 amount
);

/**
* @dev Emitted when first loss is withdrawn from the pool.
*/
event FirstLossWithdrawn(
address indexed caller,
address indexed receiver,
uint256 amount
);

/**
* @dev See IERC4626 for event definition.
Expand All @@ -43,8 +57,9 @@ library PoolLib {
* @param minFirstLossRequired The minimum amount of first loss the pool needs to become active
* @return newState The updated Pool lifecycle state
*/
function executeFirstLossContribution(
function executeFirstLossDeposit(
address liquidityAsset,
address spender,
uint256 amount,
address firstLossVault,
IPoolLifeCycleState currentState,
Expand All @@ -53,7 +68,7 @@ library PoolLib {
require(firstLossVault != address(0), "Pool: 0 address");

IERC20(liquidityAsset).safeTransferFrom(
msg.sender,
spender,
firstLossVault,
amount
);
Expand All @@ -69,7 +84,27 @@ library PoolLib {
newState = IPoolLifeCycleState.Active;
emit LifeCycleStateTransition(newState);
}
emit FirstLossSupplied(msg.sender, amount);
emit FirstLossDeposited(msg.sender, spender, amount);
}

/**
* @dev Withdraws first loss capital. Can only be called by the Pool manager under certain conditions.
* @param amount Amount of first loss being withdrawn
* @param withdrawReceiver Where the liquidity should be withdrawn to
* @param firstLossVault Vault holding first loss
* @return newState The updated Pool lifecycle state
*/
function executeFirstLossWithdraw(
uint256 amount,
address withdrawReceiver,
address firstLossVault
) external returns (uint256) {
require(firstLossVault != address(0), "Pool: 0 address");
require(withdrawReceiver != address(0), "Pool: 0 address");

FirstLossVault(firstLossVault).withdraw(amount, withdrawReceiver);
emit FirstLossWithdrawn(msg.sender, withdrawReceiver, amount);
return amount;
}

/**
Expand Down
32 changes: 28 additions & 4 deletions contracts/mocks/PoolLibTestWrapper.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../library/PoolLib.sol";
import "../libraries/PoolLib.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
Expand All @@ -10,30 +10,54 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
*/
contract PoolLibTestWrapper is ERC20("PoolLibTest", "PLT") {
event LifeCycleStateTransition(IPoolLifeCycleState state);
event FirstLossSupplied(address indexed supplier, uint256 amount);
event FirstLossDeposited(
address indexed caller,
address indexed supplier,
uint256 amount
);
event FirstLossWithdrawn(
address indexed caller,
address indexed receiver,
uint256 amount
);
event Deposit(
address indexed caller,
address indexed owner,
uint256 assets,
uint256 shares
);

function executeFirstLossContribution(
function executeFirstLossDeposit(
address liquidityAsset,
address spender,
uint256 amount,
address firstLossVault,
IPoolLifeCycleState currentState,
uint256 minFirstLossRequired
) external {
PoolLib.executeFirstLossContribution(
PoolLib.executeFirstLossDeposit(
liquidityAsset,
spender,
amount,
firstLossVault,
currentState,
minFirstLossRequired
);
}

function executeFirstLossWithdraw(
uint256 amount,
address withdrawReceiver,
address firstLossVault
) external returns (uint256) {
return
PoolLib.executeFirstLossWithdraw(
amount,
withdrawReceiver,
firstLossVault
);
}

function calculateAssetsToShares(
uint256 assets,
uint256 sharesTotalSupply,
Expand Down
53 changes: 41 additions & 12 deletions test/Pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ describe("Pool", () => {
});
});

describe("supplyFirstLoss", async () => {
it("first loss can be supplied and transitions lifecycle state", async () => {
describe("depositFirstLoss()", async () => {
it("first loss can be deposited and transitions lifecycle state", async () => {
const { pool, poolManager, liquidityAsset } = await loadFixture(
loadPoolFixture
);
Expand All @@ -70,8 +70,10 @@ describe("Pool", () => {

// Contribute first loss
expect(
await pool.connect(poolManager).supplyFirstLoss(firstLossAmount)
).to.emit(pool.address, "FirstLossSupplied");
await pool
.connect(poolManager)
.depositFirstLoss(firstLossAmount, poolManager.address)
).to.emit(pool.address, "FirstLossDeposited");

// Check balance
expect(await pool.firstLoss()).to.equal(firstLossAmount);
Expand All @@ -81,6 +83,16 @@ describe("Pool", () => {
});
});

describe("withdrawFirstLoss()", async () => {
it("reverts if pool is not closed", async () => {
const { pool, poolManager } = await loadFixture(loadPoolFixture);

await expect(
pool.connect(poolManager).withdrawFirstLoss(10, poolManager.address)
).to.be.revertedWith("Pool: FunctionInvalidAtThisLifeCycleState");
});
});

describe("deposit()", async () => {
it("deposit cannot be called if pool is initialized", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);
Expand All @@ -104,7 +116,12 @@ describe("Pool", () => {
.approve(pool.address, firstLossInitialMinimum);

// Contribute first loss
await pool.connect(poolManager).supplyFirstLoss(firstLossInitialMinimum);
await pool
.connect(poolManager)
.depositFirstLoss(
DEFAULT_POOL_SETTINGS.firstLossInitialMinimum,
poolManager.address
);

// Provide capital to lender
const depositAmount = 1000;
Expand All @@ -128,7 +145,7 @@ describe("Pool", () => {
});

describe("Permissions", () => {
describe("updatePoolCapacity", () => {
describe("updatePoolCapacity()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

Expand All @@ -138,7 +155,7 @@ describe("Pool", () => {
});
});

describe("updatePoolEndDate", () => {
describe("updatePoolEndDate()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

Expand All @@ -148,7 +165,7 @@ describe("Pool", () => {
});
});

describe("requestWithdrawal", () => {
describe("requestWithdrawal()", () => {
it("reverts if not called by lender", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

Expand All @@ -158,7 +175,7 @@ describe("Pool", () => {
});
});

describe("fundLoan", () => {
describe("fundLoan()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

Expand All @@ -168,7 +185,7 @@ describe("Pool", () => {
});
});

describe("markLoanAsInDefault", () => {
describe("markLoanAsInDefault()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

Expand All @@ -178,12 +195,24 @@ describe("Pool", () => {
});
});

describe("supplyFirstLoss", () => {
describe("depositFirstLoss()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

await expect(
pool.connect(otherAccount).depositFirstLoss(100, otherAccount.address)
).to.be.revertedWith("Pool: caller is not manager");
});
});

describe("withdrawFirstLoss()", () => {
it("reverts if not called by Pool Manager", async () => {
const { pool, otherAccount } = await loadFixture(loadPoolFixture);

await expect(
pool.connect(otherAccount).supplyFirstLoss(100)
pool
.connect(otherAccount)
.withdrawFirstLoss(100, otherAccount.address)
).to.be.revertedWith("Pool: caller is not manager");
});
});
Expand Down
Loading