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

Add initial withdrawWindow calculations #35

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
57 changes: 47 additions & 10 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ contract Pool is IPool, ERC20 {
FirstLossVault private _firstLossVault;
IPoolAccountings private _accountings;

/**
* @dev a timestamp of when the pool was first put in this state
*/
uint256 private _poolLifeCycleStateTimestamp;

/**
* @dev Modifier that checks that the caller is the pool's manager.
*/
Expand Down Expand Up @@ -84,9 +89,8 @@ contract Pool is IPool, ERC20 {
_liquidityAsset = IERC20(liquidityAsset);
_poolSettings = poolSettings;
_manager = poolManager;
_poolLifeCycleState = IPoolLifeCycleState.Initialized;

_firstLossVault = new FirstLossVault(address(this), liquidityAsset);
_setPoolLifeCycleState(IPoolLifeCycleState.Initialized);
}

/**
Expand Down Expand Up @@ -136,13 +140,16 @@ contract Pool is IPool, ERC20 {
onlyManager
atInitializedOrActiveState
{
_poolLifeCycleState = PoolLib.executeFirstLossContribution(
address(_liquidityAsset),
amount,
address(_firstLossVault),
_poolLifeCycleState,
_poolSettings.firstLossInitialMinimum
);
IPoolLifeCycleState poolLifeCycleState = PoolLib
.executeFirstLossContribution(
address(_liquidityAsset),
amount,
address(_firstLossVault),
_poolLifeCycleState,
_poolSettings.firstLossInitialMinimum
);

_setPoolLifeCycleState(poolLifeCycleState);
}

/**
Expand Down Expand Up @@ -201,7 +208,37 @@ contract Pool is IPool, ERC20 {
function markLoanAsInDefault(address) external onlyManager {}

/*//////////////////////////////////////////////////////////////
ERC-4246 Methods
Withdrawal Request Methods
//////////////////////////////////////////////////////////////*/

function currentWithdrawWindowIndex() external view returns (uint256) {
if (_poolLifeCycleState != IPoolLifeCycleState.Active) {
return 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

are there any unintended consequences of this if the pool is not active, but actually matured or faulted?

}

return
(block.timestamp - _poolLifeCycleStateTimestamp) /
_poolSettings.withdrawWindowDurationSeconds;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is user-configurable. Is it possible it's set to 0?

}

function nextWithdrawWindowIndex() external view returns (uint256) {
Copy link
Contributor

@ams9198 ams9198 Sep 28, 2022

Choose a reason for hiding this comment

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

Would this be used externally by lenders or frontends to time their claims? So they would take this value, and multiply it by the duration then + startTime basically?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally we'd have another method to do that method directly, and this will become private since it's really an implementation detail.

return 0;
}

/**
* @dev Set the pool lifecycle state. If the state changes, this method
* will also update the _poolLifeCycleStateTimestamp variable
*/
function _setPoolLifeCycleState(IPoolLifeCycleState state) internal {
if (_poolLifeCycleState != state) {
_poolLifeCycleStateTimestamp = block.timestamp;
}

_poolLifeCycleState = state;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR really, but I wonder if this function should be the only one responsible for emitting the PoolLifeCycleTransition event (I think the PoolLib emits it right now, which felt strange to me).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thats a great point -- we should absolutely move it. Will do that.

}

/*//////////////////////////////////////////////////////////////
ERC-4626 Methods
//////////////////////////////////////////////////////////////*/

/**
Expand Down
6 changes: 4 additions & 2 deletions contracts/PoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ contract PoolFactory {
address liquidityAsset,
uint256 maxCapacity,
uint256 endDate,
uint256 withdrawalFee
uint256 withdrawalFee,
uint256 withdrawWindowDurationSeconds
) public virtual returns (address poolAddress) {
uint256 firstLossInitialMinimum = 0; // TODO: take from ServiceConfig
IPoolConfigurableSettings memory settings = IPoolConfigurableSettings(
maxCapacity,
endDate,
withdrawalFee,
firstLossInitialMinimum
firstLossInitialMinimum,
withdrawWindowDurationSeconds
);
Pool pool = new Pool(
liquidityAsset,
Expand Down
1 change: 1 addition & 0 deletions contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct IPoolConfigurableSettings {
uint256 endDate; // epoch seconds
uint256 withdrawalFee; // bips
uint256 firstLossInitialMinimum; // amount
uint256 withdrawWindowDurationSeconds; // seconds (e.g. 30 days)
// TODO: add in Pool fees
}

Expand Down
6 changes: 4 additions & 2 deletions contracts/permissioned/PermissionedPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ contract PermissionedPoolFactory is PoolFactory {
address liquidityAsset,
uint256 maxCapacity,
uint256 endDate,
uint256 withdrawalFee
uint256 withdrawalFee,
uint256 withdrawWindowDurationSeconds
) public override onlyVerifiedPoolManager returns (address poolAddress) {
uint256 firstLossInitialMinimum = 0; // TODO: take from ServiceConfig
IPoolConfigurableSettings memory settings = IPoolConfigurableSettings(
maxCapacity,
endDate,
withdrawalFee,
firstLossInitialMinimum
firstLossInitialMinimum,
withdrawWindowDurationSeconds
);
Pool pool = new PermissionedPool(
liquidityAsset,
Expand Down
9 changes: 9 additions & 0 deletions test/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"env": {
"mocha": true
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
8 changes: 7 additions & 1 deletion test/Loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ describe("Loan", () => {
await loanFactory.deployed();

// Create a pool
const tx1 = await poolFactory.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0);
const tx1 = await poolFactory.createPool(
MOCK_LIQUIDITY_ADDRESS,
0,
0,
0,
0
);
const tx1Receipt = await tx1.wait();

// Extract its address from the PoolCreated event
Expand Down
133 changes: 83 additions & 50 deletions test/Pool.test.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,23 @@
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import {
deployActivePool,
deployPool,
DEFAULT_POOL_SETTINGS
} from "./support/pool";

describe("Pool", () => {
const POOL_SETTINGS = {
maxCapacity: 10_000_000,
endDate: 2524611601, // Jan 1, 2050
withdrawalFee: 50, // bips,
firstLossInitialMinimum: 100_000
};

async function deployLiquidityAssetFixture() {
const LiquidityAsset = await ethers.getContractFactory("MockERC20");
const liquidityAsset = await LiquidityAsset.deploy("Test Coin", "TC");

await liquidityAsset.deployed();
return {
liquidityAsset
};
}

async function loadPoolFixture() {
const [poolManager, otherAccount] = await ethers.getSigners();
const { liquidityAsset } = await deployLiquidityAssetFixture();

const PoolLib = await ethers.getContractFactory("PoolLib");
const poolLib = await PoolLib.deploy();

const Pool = await ethers.getContractFactory("Pool", {
libraries: {
PoolLib: poolLib.address
}
});
const { pool, liquidityAsset } = await deployPool(poolManager);

const pool = await Pool.deploy(
liquidityAsset.address,
poolManager.address,
POOL_SETTINGS,
"Valyria PoolToken",
"VPT"
);
await pool.deployed();
return { pool, liquidityAsset, poolManager, otherAccount };
}

await liquidityAsset.mint(
poolManager.address,
POOL_SETTINGS.firstLossInitialMinimum
);
async function loadActivePoolFixture() {
const [poolManager, otherAccount] = await ethers.getSigners();
const { pool, liquidityAsset } = await deployActivePool(poolManager);

return { pool, liquidityAsset, poolManager, otherAccount };
}
Expand All @@ -66,11 +38,19 @@ describe("Pool", () => {
it("sets the pool settings", async () => {
const { pool } = await loadFixture(loadPoolFixture);

const { endDate, maxCapacity, withdrawalFee } = await pool.settings();

expect(endDate).to.equal(POOL_SETTINGS.endDate);
expect(maxCapacity).to.equal(POOL_SETTINGS.maxCapacity);
expect(withdrawalFee).to.equal(POOL_SETTINGS.withdrawalFee);
const {
endDate,
maxCapacity,
withdrawalFee,
withdrawWindowDurationSeconds
} = await pool.settings();

expect(endDate).to.equal(DEFAULT_POOL_SETTINGS.endDate);
expect(maxCapacity).to.equal(DEFAULT_POOL_SETTINGS.maxCapacity);
expect(withdrawalFee).to.equal(DEFAULT_POOL_SETTINGS.withdrawalFee);
expect(withdrawWindowDurationSeconds).to.equal(
DEFAULT_POOL_SETTINGS.withdrawWindowDurationSeconds
);
});
});

Expand All @@ -80,7 +60,8 @@ describe("Pool", () => {
loadPoolFixture
);

const firstLossAmount = POOL_SETTINGS.firstLossInitialMinimum;
const { firstLossInitialMinimum: firstLossAmount } =
await pool.settings();

// Grant allowance
await liquidityAsset
Expand Down Expand Up @@ -115,15 +96,15 @@ describe("Pool", () => {
const { pool, otherAccount, liquidityAsset, poolManager } =
await loadFixture(loadPoolFixture);

const { firstLossInitialMinimum } = await pool.settings();

// First loss must be provided for deposits to open
await liquidityAsset
.connect(poolManager)
.approve(pool.address, POOL_SETTINGS.firstLossInitialMinimum);
.approve(pool.address, firstLossInitialMinimum);

// Contribute first loss
await pool
.connect(poolManager)
.supplyFirstLoss(POOL_SETTINGS.firstLossInitialMinimum);
await pool.connect(poolManager).supplyFirstLoss(firstLossInitialMinimum);

// Provide capital to lender
const depositAmount = 1000;
Expand Down Expand Up @@ -259,4 +240,56 @@ describe("Pool", () => {
).to.be.revertedWith("ERC20: transfer to the zero address");
});
});

describe("Withdrawal Requests", () => {
describe("currentWithdrawWindowIndex()", () => {
it("returns 0 if the pool is not active", async () => {
const { pool } = await loadFixture(loadPoolFixture);

expect(await pool.currentWithdrawWindowIndex()).to.equal(0);
});

it("returns 0 if the pool has not reached it's first withdrawal window", async () => {
const { pool } = await loadFixture(loadActivePoolFixture);

expect(await pool.currentWithdrawWindowIndex()).to.equal(0);
});

it("returns 0 if the pool is one second before the first withdrawal window", async () => {
const { pool } = await loadFixture(loadActivePoolFixture);

const { withdrawWindowDurationSeconds } = await pool.settings();
await time.increase(withdrawWindowDurationSeconds.toNumber() - 1);

expect(await pool.currentWithdrawWindowIndex()).to.equal(0);
});

it("returns 1 if the pool reached it's first withdraw window", async () => {
const { pool } = await loadFixture(loadActivePoolFixture);

const { withdrawWindowDurationSeconds } = await pool.settings();
await time.increase(withdrawWindowDurationSeconds.toNumber());

expect(await pool.currentWithdrawWindowIndex()).to.equal(1);
});

it("returns 1 if the pool is past it's first withdraw window", async () => {
const { pool } = await loadFixture(loadActivePoolFixture);

const { withdrawWindowDurationSeconds } = await pool.settings();
await time.increase(withdrawWindowDurationSeconds.toNumber() + 1);

expect(await pool.currentWithdrawWindowIndex()).to.equal(1);
});

it("returns 2 if the pool reached it's second withdraw window", async () => {
const { pool } = await loadFixture(loadActivePoolFixture);

const { withdrawWindowDurationSeconds } = await pool.settings();
await time.increase(withdrawWindowDurationSeconds.toNumber() * 2);

expect(await pool.currentWithdrawWindowIndex()).to.equal(2);
});
});
});
});
2 changes: 1 addition & 1 deletion test/PoolFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("PoolFactory", () => {
const { poolFactory } = await loadFixture(deployFixture);

await expect(
poolFactory.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0)
poolFactory.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0, 0)
).to.emit(poolFactory, "PoolCreated");
});
});
4 changes: 2 additions & 2 deletions test/permissioned/PermissionedPoolFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("PermissionedPoolFactory", () => {
await expect(
poolFactory
.connect(otherAccount)
.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0)
.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0, 0)
).to.emit(poolFactory, "PoolCreated");
});

Expand All @@ -80,7 +80,7 @@ describe("PermissionedPoolFactory", () => {
await poolManagerAccessControl.allow(otherAccount.getAddress());

await expect(
poolFactory.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0)
poolFactory.createPool(MOCK_LIQUIDITY_ADDRESS, 0, 0, 0, 0)
).to.be.revertedWith("caller is not a pool manager");
});
});
14 changes: 14 additions & 0 deletions test/support/erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ethers } from "hardhat";

/**
* Deploy a Mock ERC20 token
*/
export async function deployMockERC20() {
const MockERC20 = await ethers.getContractFactory("MockERC20");
const mockERC20 = await MockERC20.deploy("Test Coin", "TC");
await mockERC20.deployed();

return {
mockERC20
};
}
Loading