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

Handle case where all assets are missing, but tokens exist #143

Merged
merged 1 commit into from
Dec 8, 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
62 changes: 29 additions & 33 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -615,15 +615,14 @@ contract Pool is IPool, ERC20Upgradeable, IBeaconImplementation {
public
view
override
returns (uint256)
returns (uint256 shares)
{
return
PoolLib.calculateConversion(
assets,
totalAvailableSupply(),
totalAvailableAssets(),
false
);
shares = PoolLib.calculateSharesFromAssets(
assets,
totalAvailableSupply(),
totalAvailableAssets(),
false
);
}

/**
Expand All @@ -634,15 +633,14 @@ contract Pool is IPool, ERC20Upgradeable, IBeaconImplementation {
public
view
override
returns (uint256)
returns (uint256 assets)
{
return
PoolLib.calculateConversion(
shares,
totalAvailableAssets(),
totalAvailableSupply(),
false
);
assets = PoolLib.calculateAssetsFromShares(
shares,
totalAvailableAssets(),
totalAvailableSupply(),
false
);
}

/**
Expand Down Expand Up @@ -671,16 +669,15 @@ contract Pool is IPool, ERC20Upgradeable, IBeaconImplementation {
public
view
override
returns (uint256)
returns (uint256 shares)
{
return
PoolLib.calculateConversion(
assets,
totalAvailableSupply(),
totalAvailableAssets() +
PoolLib.calculateExpectedInterest(_activeLoans),
false
);
shares = PoolLib.calculateSharesFromAssets(
assets,
totalAvailableSupply(),
totalAvailableAssets() +
PoolLib.calculateExpectedInterest(_activeLoans),
false
);
}

/**
Expand Down Expand Up @@ -732,14 +729,13 @@ contract Pool is IPool, ERC20Upgradeable, IBeaconImplementation {
override
returns (uint256 assets)
{
return
PoolLib.calculateConversion(
shares,
totalAvailableAssets() +
PoolLib.calculateExpectedInterest(_activeLoans),
totalAvailableSupply(),
true
);
assets = PoolLib.calculateAssetsFromShares(
shares,
totalAvailableAssets() +
PoolLib.calculateExpectedInterest(_activeLoans),
totalAvailableSupply(),
true
);
}

/**
Expand Down
13 changes: 7 additions & 6 deletions contracts/controllers/WithdrawController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,11 @@ contract WithdrawController is IWithdrawController, IBeaconImplementation {
view
returns (uint256 assets)
{
assets = PoolLib.calculateConversion(
IPoolWithdrawState memory withdrawState = _currentWithdrawState(owner);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was just an optimization that was in the sister function that wasn't applied here

assets = PoolLib.calculateAssetsFromShares(
shares,
_currentWithdrawState(owner).withdrawableAssets,
_currentWithdrawState(owner).redeemableShares,
withdrawState.withdrawableAssets,
withdrawState.redeemableShares,
false
);
}
Expand All @@ -259,7 +260,7 @@ contract WithdrawController is IWithdrawController, IBeaconImplementation {
returns (uint256 shares)
{
IPoolWithdrawState memory withdrawState = _currentWithdrawState(owner);
shares = PoolLib.calculateConversion(
shares = PoolLib.calculateSharesFromAssets(
assets,
withdrawState.redeemableShares,
withdrawState.withdrawableAssets,
Expand Down Expand Up @@ -517,7 +518,7 @@ contract WithdrawController is IWithdrawController, IBeaconImplementation {
IPoolWithdrawState memory state = crankLender(owner);

// Calculate how many assets should be transferred
assets = PoolLib.calculateConversion(
assets = PoolLib.calculateAssetsFromShares(
shares,
state.withdrawableAssets,
state.redeemableShares,
Expand All @@ -538,7 +539,7 @@ contract WithdrawController is IWithdrawController, IBeaconImplementation {
IPoolWithdrawState memory state = crankLender(owner);

// Calculate how many shares should be burned
shares = PoolLib.calculateConversion(
shares = PoolLib.calculateSharesFromAssets(
assets,
state.redeemableShares,
state.withdrawableAssets,
Expand Down
45 changes: 44 additions & 1 deletion contracts/libraries/PoolLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ library PoolLib {
uint256 numerator,
uint256 denominator,
bool roundUp
) external pure returns (uint256 output) {
) public pure returns (uint256 output) {
if (numerator == 0 || denominator == 0) {
return input;
}
Expand All @@ -238,6 +238,49 @@ library PoolLib {
}
}

/**
* @dev Calculates the exchange rate for converting assets to shares
*/
function calculateSharesFromAssets(
uint256 assets,
uint256 totalShares,
uint256 totalAssets,
bool roundUp
) external pure returns (uint256) {
require(isSolvent(totalAssets, totalShares), "POOL_INSOLVENT");

return calculateConversion(assets, totalShares, totalAssets, roundUp);
}

/**
* @dev Calculates the exchange rate for converting shares to assets
*/
function calculateAssetsFromShares(
uint256 shares,
uint256 totalAssets,
uint256 totalShares,
bool roundUp
) external pure returns (uint256) {
require(isSolvent(totalAssets, totalShares), "POOL_INSOLVENT");

return calculateConversion(shares, totalAssets, totalShares, roundUp);
}

/**
* @dev Private method to determine if a pool is solvent given
* the parameters.
*
* If the pool has assets, it is solvent. If no assets are available,
* but no shares have been issued, it is solvent. Otherwise, it is insolvent.
*/
function isSolvent(uint256 totalAssets, uint256 totalShares)
private
pure
returns (bool)
{
return totalAssets > 0 || totalShares == 0;
}

/**
* @dev Calculates total assets held by Vault (including those marked for withdrawal)
* @param asset Amount of total assets held by the Vault
Expand Down
32 changes: 31 additions & 1 deletion contracts/mocks/PoolLibTestWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,41 @@ contract PoolLibTestWrapper is ERC20("PoolLibTest", "PLT") {
uint256 numerator,
uint256 denominator,
bool roundUp
) external pure returns (uint256) {
) public pure returns (uint256) {
return
PoolLib.calculateConversion(input, numerator, denominator, roundUp);
}

function calculateSharesFromAssets(
uint256 assets,
uint256 totalShares,
uint256 totalAssets,
bool roundUp
) external pure returns (uint256) {
return
PoolLib.calculateSharesFromAssets(
assets,
totalShares,
totalAssets,
roundUp
);
}

function calculateAssetsFromShares(
uint256 shares,
uint256 totalAssets,
uint256 totalShares,
bool roundUp
) external pure returns (uint256) {
return
PoolLib.calculateAssetsFromShares(
shares,
totalAssets,
totalShares,
roundUp
);
}

function calculateTotalAssets(
address asset,
address vault,
Expand Down
4 changes: 1 addition & 3 deletions test/scenarios/business/2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ describe("Business Scenario 2", () => {
}

async function fixtures() {
const [operator, poolAdmin, lenderA, lenderB, borrower] =
await ethers.getSigners();
const [poolAdmin, lenderA, lenderB, borrower] = await ethers.getSigners();
const endTime = (await time.latest()) + 5_184_000; // 60 days.
const poolSettings = {
endDate: endTime, // Jan 1, 2050
Expand All @@ -53,7 +52,6 @@ describe("Business Scenario 2", () => {
);
const { pool, serviceConfiguration, withdrawController, poolController } =
await deployPool({
operator,
poolAdmin: poolAdmin,
settings: poolSettings,
liquidityAsset: mockUSDC
Expand Down
95 changes: 90 additions & 5 deletions test/scenarios/pool/direct-deposit.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import { deployLoan, fundLoan } from "../../support/loan";
import { activatePool, deployPool } from "../../support/pool";
import { getCommonSigners } from "../../support/utils";

describe("Direct Deposit", () => {
async function loadPoolFixture() {
const { poolAdmin, lender } = await getCommonSigners();
const { poolAdmin, borrower, lender } = await getCommonSigners();

const { pool, liquidityAsset } = await deployPool({
poolAdmin
});
const { pool, poolController, liquidityAsset, serviceConfiguration } =
await deployPool({
poolAdmin,
settings: {
firstLossInitialMinimum: 0
}
});

await activatePool(pool, poolAdmin, liquidityAsset);

Expand All @@ -18,8 +24,12 @@ describe("Direct Deposit", () => {

return {
pool,
poolController,
poolAdmin,
liquidityAsset,
lender
lender,
borrower,
serviceConfiguration
};
}

Expand Down Expand Up @@ -58,6 +68,14 @@ describe("Direct Deposit", () => {
// Ensure 200,000 assets are in the pool, an 100,000 pool tokens
expect(await pool.totalAssets()).to.equal(200_000);
expect(await pool.totalSupply()).to.equal(100_000);

// Check withdrawal amounts
expect(await pool.connect(lender).previewWithdrawRequest(100_000)).to.equal(
52_500 // 50_000 plus fees
);
expect(await pool.connect(lender).previewRedeemRequest(100_000)).to.equal(
190_000 // 200_000 minus fees
);
});

it("properly handles the case where someone directly transfers the liquidity asset to the pool without using deposit()", async () => {
Expand Down Expand Up @@ -85,5 +103,72 @@ describe("Direct Deposit", () => {
// Ensure 200,000 assets are in the pool, an 100,000 pool tokens
expect(await pool.totalAssets()).to.equal(200_000);
expect(await pool.totalSupply()).to.equal(100_000);

// Check withdrawal amounts
expect(await pool.connect(lender).previewWithdrawRequest(100_000)).to.equal(
52_500 // 50_000 plus fees
);
expect(await pool.connect(lender).previewRedeemRequest(100_000)).to.equal(
190_000 // 200_000 minus fees
);
});

it("properly handles when the pool has zero assets, but has more tokens", async () => {
// Deploy fresh fixtures and activate pool
const {
pool,
poolController,
poolAdmin,
liquidityAsset,
lender,
borrower,
serviceConfiguration
} = await loadFixture(loadPoolFixture);

// Deploy the loan
const { loan } = await deployLoan(
pool.address,
borrower.address,
liquidityAsset.address,
serviceConfiguration,
{
principal: 100_000
}
);

// Ensure nothing is in the pool
expect(await pool.totalAssets()).to.equal(0);
expect(await pool.totalSupply()).to.equal(0);

// Deposit money
await pool.connect(lender).deposit(100_000, lender.address);

// fund loan
await fundLoan(loan, poolController, poolAdmin);

// draw down
await loan.connect(borrower).drawdown(await loan.principal());

// Mark loan as in default
await poolController.connect(poolAdmin).defaultLoan(loan.address);

// Ensure 0 assets are in the pool, an 100,000 pool tokens
expect(await pool.totalAssets()).to.equal(0);
expect(await pool.totalSupply()).to.equal(100_000);

// Check what deposit, mint would look like:
await expect(pool.connect(lender).previewMint(100_000)).to.be.revertedWith(
"POOL_INSOLVENT"
);

await expect(
pool.connect(lender).previewDeposit(100_000)
).to.be.revertedWith("POOL_INSOLVENT");
await expect(
pool.connect(lender).previewWithdrawRequest(100_000)
).to.be.revertedWith("POOL_INSOLVENT");
await expect(
pool.connect(lender).previewRedeemRequest(100_000)
).to.be.revertedWith("POOL_INSOLVENT");
});
});
Loading