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

feat: Global delayed insurance withdrawal #246

Open
wants to merge 3 commits into
base: contracts-v2
Choose a base branch
from
Open
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
37 changes: 35 additions & 2 deletions contracts/Insurance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ contract Insurance is IInsurance {
uint256 public override publicCollateralAmount; // amount of underlying collateral in public pool, in WAD format
uint256 public override bufferCollateralAmount; // amount of collateral in buffer pool, in WAD format
address public immutable token; // token representation of a users holding in the pool

uint256 private constant ONE_TOKEN = 1e18; // Constant for 10**18, i.e. one token in WAD format; used for drainPool

// The insurance pool funding rate calculation can be refactored to have 0.00000570775
Expand All @@ -31,10 +30,16 @@ contract Insurance is IInsurance {
// Target percent of leveraged notional value in the market for the insurance pool to meet; 1% by default
uint256 private constant INSURANCE_POOL_TARGET_PERCENT = 1e16;

uint256 public constant COOLDOWN_PERIOD = 5 days; // Cooldown period that users must wait for after notifying their intent to withdraw
uint256 public constant WITHDRAW_WINDOW = 2 days; // Window of time users have to withdraw funds after the cooldown period passes

ITracerPerpetualSwaps public tracer; // Tracer associated with Insurance Pool

mapping(address => uint256) public withdrawCooldown; // The timestamp of when stakers have notified their intent to withdraw

event InsuranceDeposit(address indexed market, address indexed user, uint256 indexed amount);
event InsuranceWithdraw(address indexed market, address indexed user, uint256 indexed amount);
event Cooldown(address indexed user, uint256 cooldown);

constructor(address _tracer) {
require(_tracer != address(0), "INS: _tracer = address(0)");
Expand Down Expand Up @@ -73,6 +78,28 @@ contract Insurance is IInsurance {
emit InsuranceDeposit(address(tracer), msg.sender, wadAmount);
}

/**
* @notice Activates the cooldown period to allow for withdrawals.
* @dev Can only be called if the caller holds some pool tokens
* @dev Emits a `Cooldown` event
*/
function intendToWithdraw() external {
uint256 balance = getPoolUserBalance(msg.sender);
require(balance > 0, "INS: Zero balance");
withdrawCooldown[msg.sender] = block.timestamp;
emit Cooldown(msg.sender, block.timestamp);
}

/**
* @notice Resets the cooldown period of a user
* @dev User must have called intendToWithdraw first
*/
function cancelWithdraw() external {
require(withdrawCooldown[msg.sender] != 0, "INS: Not withdrawing");
withdrawCooldown[msg.sender] = 0;
emit Cooldown(msg.sender, 0);
}

/**
* @notice Allows a user to withdraw their assets from a given insurance pool
* @dev burns amount of tokens from the pool token
Expand All @@ -81,7 +108,13 @@ contract Insurance is IInsurance {
function withdraw(uint256 amount) external override {
updatePoolAmount();
uint256 balance = getPoolUserBalance(msg.sender);
require(balance >= amount, "INS: balance < amount");
require(balance >= amount, "INS: Balance < amount");
// Ensure withdraw window is reached and not passed
require(
block.timestamp >= (withdrawCooldown[msg.sender] + COOLDOWN_PERIOD) &&
block.timestamp < (withdrawCooldown[msg.sender] + COOLDOWN_PERIOD + WITHDRAW_WINDOW),
"INS: Funds locked"
);

IERC20 collateralToken = IERC20(collateralAsset);
InsurancePoolToken poolToken = InsurancePoolToken(token);
Expand Down
191 changes: 168 additions & 23 deletions test/unit/InsuranceStaking.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ const {
getInsurance,
getTracer,
} = require("../util/DeploymentUtil")
const { depositToInsurance } = require("../util/InsuranceUtil")

const withdrawCooldown = 5 * 86400 // 5 days
const withdrawWindow = 2 * 86400 // 2 days

const forwardTime = async (seconds) => {
await network.provider.send("evm_increaseTime", [seconds])
await network.provider.send("evm_mine", [])
}

const setupTests = deployments.createFixture(async () => {
await deployments.fixture(["FullDeployTest"])
Expand Down Expand Up @@ -74,43 +83,179 @@ describe("Unit tests: Insurance.sol", function () {
})
})

describe("withdraw", async () => {
context("when the user does not have enough pool tokens", async () => {
describe("intendToWithdraw", async () => {
context("when user has zero balance", async () => {
it("reverts", async () => {
await expect(
insurance.withdraw(ethers.utils.parseEther("1"))
).to.be.revertedWith("INS: balance < amount")
await expect(insurance.intendToWithdraw()).to.be.revertedWith(
"INS: Zero balance"
)
})
})

context("when the user has enough pool tokens", async () => {
beforeEach(async () => {
// get user tp acquire some pool tokens
await quoteToken.approve(
insurance.address,
ethers.utils.parseEther("2")
context("when user has balance", async () => {
it("emits event", async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("1")
)
await expect(insurance.intendToWithdraw()).to.emit(
insurance,
"Cooldown"
)
await insurance.deposit(ethers.utils.parseEther("2"))
// get user to burn some pool tokens
await insurance.withdraw(ethers.utils.parseEther("1"))
})
})
})

it("burns pool tokens", async () => {
let poolTokenHolding = await insurance.getPoolUserBalance(
accounts[0].address
describe("cancelWithdraw", async () => {
context("when user has not called intendToWithdraw", async () => {
it("reverts", async () => {
await expect(insurance.cancelWithdraw()).to.be.revertedWith(
"INS: Not withdrawing"
)
expect(poolTokenHolding).to.equal(ethers.utils.parseEther("1"))
})
})

it("decreases the collateral holdings of the insurance fund", async () => {
let collateralHolding = await insurance.publicCollateralAmount()
expect(collateralHolding).to.equal(ethers.utils.parseEther("1"))
context("when user has called intendToWithdraw", async () => {
let tx

beforeEach(async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("1")
)
await insurance.intendToWithdraw()
tx = await insurance.cancelWithdraw()
})

it("pulls in collateral from the tracer market", async () => {})
it("resets the users cooldown", async () => {
expect(
await insurance.withdrawCooldown(accounts[0].address)
).to.equal(0)
})

it("resets the users cooldown", async () => {
await expect(tx)
.to.emit(insurance, "Cooldown")
.withArgs(accounts[0].address, 0)
})
})
})

it("emits an insurance withdraw event", async () => {})
describe("withdraw", async () => {
context("when the user has not called intendToWithdraw", async () => {
it("reverts", async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("2")
)
await expect(
insurance.withdraw(ethers.utils.parseEther("2"))
).to.be.revertedWith("INS: Funds locked")
})
})

context(
"when the user calls intendToWithdraw but cooldown window has not passed",
async () => {
it("reverts", async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("2")
)
await insurance.intendToWithdraw()
await expect(
insurance.withdraw(ethers.utils.parseEther("1"))
).to.be.revertedWith("INS: Funds locked")
})
}
)

context(
"when the user calls intendToWithdraw and is in withdraw window",
async () => {
context(
"when the user does not have enough pool tokens",
async () => {
it("reverts", async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("2")
)
await insurance.intendToWithdraw()
await forwardTime(withdrawCooldown)
await expect(
insurance.withdraw(ethers.utils.parseEther("5"))
).to.be.revertedWith("INS: Balance < amount")
})
}
)

context("when the user has enough pool tokens", async () => {
let tx, amount

beforeEach(async () => {
// get user tp acquire some pool tokens
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("2")
)
await insurance.intendToWithdraw()
await forwardTime(withdrawCooldown)
// get user to burn some pool tokens
amount = ethers.utils.parseEther("1")
tx = await insurance.withdraw(amount)
})

it("burns pool tokens", async () => {
let poolTokenHolding =
await insurance.getPoolUserBalance(
accounts[0].address
)
expect(poolTokenHolding).to.equal(amount)
})

it("decreases the collateral holdings of the insurance fund", async () => {
let collateralHolding =
await insurance.publicCollateralAmount()
expect(collateralHolding).to.equal(amount)
})

it("emits an insurance withdraw event", async () => {
expect(tx)
.to.emit(insurance, "InsuranceWithdraw")
.withArgs(
tracer.address,
accounts[0].address,
amount
)
})
})
}
)

context(
"when the user calls intendToWithdraw and withdraw window has passed",
async () => {
it("reverts", async () => {
await depositToInsurance(
insurance,
quoteToken,
ethers.utils.parseEther("2")
)
await insurance.intendToWithdraw()
await forwardTime(withdrawCooldown + withdrawWindow)
await expect(
insurance.withdraw(ethers.utils.parseEther("2"))
).to.be.revertedWith("INS: Funds locked")
})
}
)
})

describe("getPoolBalance", async () => {
Expand Down
6 changes: 6 additions & 0 deletions test/util/InsuranceUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@ const setAndDrainCollaterals = async (
await insurance.drainPool(amountToDrain)
}

const depositToInsurance = async (insurance, quoteToken, amount) => {
await quoteToken.approve(insurance.address, amount)
await insurance.deposit(amount)
}

module.exports = {
expectCollaterals,
setCollaterals,
setAndDrainCollaterals,
depositToInsurance,
}