Skip to content

Commit

Permalink
Handle case where all assets are missing, but tokens exist (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
venables authored Dec 8, 2022
1 parent c7394a8 commit 5acd163
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 65 deletions.
62 changes: 29 additions & 33 deletions contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -638,15 +638,14 @@ contract Pool is IPool, ERC20Upgradeable, BeaconImplementation {
public
view
override
returns (uint256)
returns (uint256 shares)
{
return
PoolLib.calculateConversion(
assets,
totalAvailableSupply(),
totalAvailableAssets(),
false
);
shares = PoolLib.calculateSharesFromAssets(
assets,
totalAvailableSupply(),
totalAvailableAssets(),
false
);
}

/**
Expand All @@ -657,15 +656,14 @@ contract Pool is IPool, ERC20Upgradeable, BeaconImplementation {
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 @@ -694,16 +692,15 @@ contract Pool is IPool, ERC20Upgradeable, BeaconImplementation {
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 @@ -756,14 +753,13 @@ contract Pool is IPool, ERC20Upgradeable, BeaconImplementation {
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, BeaconImplementation {
view
returns (uint256 assets)
{
assets = PoolLib.calculateConversion(
IPoolWithdrawState memory withdrawState = _currentWithdrawState(owner);
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, BeaconImplementation {
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, BeaconImplementation {
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, BeaconImplementation {
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

0 comments on commit 5acd163

Please sign in to comment.