vault: freezeFund() instead of burnFund()

This commit is contained in:
Mark Spanbroek 2025-02-24 15:40:39 +01:00
parent 6db1a5fb3a
commit 62aea75295
6 changed files with 155 additions and 104 deletions

View File

@ -196,11 +196,12 @@ contract Vault is VaultBase, Pausable, Ownable {
_burnAccount(controller, fund, account);
}
/// Burns all tokens from all accounts in a fund.
/// Freezes a fund. Stops all tokens flows and disallows any operations on the
/// fund until it unlocks.
/// Only allowed when the fund is locked.
function burnFund(Fund fund) public whenNotPaused {
function freezeFund(Fund fund) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_burnFund(controller, fund);
_freezeFund(controller, fund);
}
/// Transfers all ERC20 tokens in the account out of the vault to the account

View File

@ -9,38 +9,40 @@ struct Lock {
Timestamp expiry;
/// The expiry can be extended no further than this
Timestamp maximum;
/// Indicates whether fund is frozen, and at what time
Timestamp frozenAt;
/// The total amount of tokens locked up in the fund
uint128 value;
/// Indicates whether the fund was burned
bool burned;
}
/// A lock can go through the following states:
///
/// ----------------------------------------
/// | |
/// --> NoLock ---> Locked ---> UnLocked --
/// \
/// ---> Burned
/// ------------------------------------------
/// | |
/// --> NoLock ---> Locked -----> UnLocked --
/// \ ^
/// \ /
/// --> Frozen --
///
enum LockStatus {
/// Indicates that no lock is set. This is the initial state, or the state
/// after all tokens have been withdrawn.
NoLock,
/// Indicates that the funds are locked. Withdrawing tokens is not allowed.
/// Indicates that the fund is locked. Withdrawing tokens is not allowed.
Locked,
/// Indicates that the fund is frozen. Flows have stopped, nothing is allowed
/// until the fund unlocks.
Frozen,
/// Indicates that the lock is unlocked. Withdrawing is allowed.
Unlocked,
/// Indicates that all tokens in the fund are burned
Burned
Unlocked
}
library Locks {
function status(Lock memory lock) internal view returns (LockStatus) {
if (lock.burned) {
return LockStatus.Burned;
}
if (Timestamps.currentTime() < lock.expiry) {
if (lock.frozenAt != Timestamp.wrap(0)) {
return LockStatus.Frozen;
}
return LockStatus.Locked;
}
if (lock.maximum == Timestamp.wrap(0)) {
@ -48,4 +50,11 @@ library Locks {
}
return LockStatus.Unlocked;
}
function flowEnd(Lock memory lock) internal pure returns (Timestamp) {
if (lock.frozenAt != Timestamp.wrap(0)) {
return lock.frozenAt;
}
return lock.expiry;
}
}

View File

@ -9,6 +9,7 @@ type Timestamp is uint40;
type Duration is uint40;
using {_timestampEquals as ==} for Timestamp global;
using {_timestampNotEqual as !=} for Timestamp global;
using {_timestampLessThan as <} for Timestamp global;
using {_timestampAtMost as <=} for Timestamp global;
@ -16,6 +17,10 @@ function _timestampEquals(Timestamp a, Timestamp b) pure returns (bool) {
return Timestamp.unwrap(a) == Timestamp.unwrap(b);
}
function _timestampNotEqual(Timestamp a, Timestamp b) pure returns (bool) {
return Timestamp.unwrap(a) != Timestamp.unwrap(b);
}
function _timestampLessThan(Timestamp a, Timestamp b) pure returns (bool) {
return Timestamp.unwrap(a) < Timestamp.unwrap(b);
}

View File

@ -85,9 +85,9 @@ abstract contract VaultBase {
account.update(Timestamps.currentTime());
return account.balance;
}
if (lockStatus == LockStatus.Unlocked) {
if (lockStatus == LockStatus.Unlocked || lockStatus == LockStatus.Frozen) {
Account memory account = _accounts[controller][fund][id];
account.update(lock.expiry);
account.update(lock.flowEnd());
return account.balance;
}
return Balance({available: 0, designated: 0});
@ -239,13 +239,11 @@ abstract contract VaultBase {
_token.safeTransfer(address(0xdead), amount);
}
function _burnFund(Controller controller, Fund fund) internal {
function _freezeFund(Controller controller, Fund fund) internal {
Lock storage lock = _locks[controller][fund];
require(lock.status() == LockStatus.Locked, VaultFundNotLocked());
lock.burned = true;
_token.safeTransfer(address(0xdead), lock.value);
lock.frozenAt = Timestamps.currentTime();
}
function _withdraw(Controller controller, Fund fund, AccountId id) internal {
@ -253,7 +251,7 @@ abstract contract VaultBase {
require(lock.status() == LockStatus.Unlocked, VaultFundNotUnlocked());
Account memory account = _accounts[controller][fund][id];
account.update(lock.expiry);
account.update(lock.flowEnd());
uint128 amount = account.balance.available + account.balance.designated;
lock.value -= amount;

View File

@ -588,31 +588,39 @@ describe("Vault", function () {
await expect(vault.burnAccount(fund, account1)).not.to.be.reverted
})
})
})
describe("burn fund", function () {
it("can burn an entire fund", async function () {
await vault.transfer(fund, account1, account2, 10)
await vault.transfer(fund, account1, account3, 10)
await vault.burnFund(fund)
expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Burned)
expect(await vault.getBalance(fund, account1)).to.equal(0)
expect(await vault.getBalance(fund, account2)).to.equal(0)
expect(await vault.getBalance(fund, account3)).to.equal(0)
})
describe("freezing", function () {
const deposit = 1000
it("moves all tokens in the fund to address 0xdead", async function () {
await vault.transfer(fund, account1, account2, 10)
await vault.transfer(fund, account1, account3, 10)
const before = await token.balanceOf(dead)
await vault.burnFund(fund)
const after = await token.balanceOf(dead)
expect(after - before).to.equal(amount)
})
let account1, account2, account3
it("can burn fund when tokens are flowing", async function () {
await vault.flow(fund, account1, account2, 5)
await expect(vault.burnFund(fund)).not.to.be.reverted
})
beforeEach(async function () {
account1 = await vault.encodeAccountId(holder.address, randomBytes(12))
account2 = await vault.encodeAccountId(holder2.address, randomBytes(12))
account3 = await vault.encodeAccountId(holder3.address, randomBytes(12))
await token.approve(vault.address, deposit)
await vault.deposit(fund, account1, deposit)
})
it("can freeze a fund", async function () {
await setAutomine(true)
await vault.freezeFund(fund)
expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Frozen)
})
it("stops all token flows", async function () {
await vault.flow(fund, account1, account2, 10)
await vault.flow(fund, account2, account3, 3)
await mine()
const start = await currentTime()
await setNextBlockTimestamp(start + 10)
await vault.freezeFund(fund)
await mine()
await advanceTimeTo(start + 20)
expect(await vault.getBalance(fund, account1)).to.equal(deposit - 100)
expect(await vault.getBalance(fund, account2)).to.equal(70)
expect(await vault.getBalance(fund, account3)).to.equal(30)
})
})
@ -717,39 +725,76 @@ describe("Vault", function () {
await vault.deposit(fund, account1, deposit)
})
it("stops flows when lock expires", async function () {
await vault.flow(fund, account1, account2, 2)
await mine()
const start = await currentTime()
const total = (expiry - start) * 2
let balance1, balance2
await advanceTimeTo(expiry)
balance1 = await vault.getBalance(fund, account1)
balance2 = await vault.getBalance(fund, account2)
expect(balance1).to.equal(deposit - total)
expect(balance2).to.equal(total)
await advanceTimeTo(expiry + 10)
balance1 = await vault.getBalance(fund, account1)
balance2 = await vault.getBalance(fund, account2)
expect(balance1).to.equal(deposit - total)
expect(balance2).to.equal(total)
describe("unlocked flows", function () {
let total
beforeEach(async function () {
await vault.flow(fund, account1, account2, 2)
await mine()
const start = await currentTime()
total = (expiry - start) * 2
await advanceTimeTo(expiry)
})
it("stops flows when lock expires", async function () {
let balance1, balance2
balance1 = await vault.getBalance(fund, account1)
balance2 = await vault.getBalance(fund, account2)
expect(balance1).to.equal(deposit - total)
expect(balance2).to.equal(total)
await advanceTimeTo(expiry + 10)
balance1 = await vault.getBalance(fund, account1)
balance2 = await vault.getBalance(fund, account2)
expect(balance1).to.equal(deposit - total)
expect(balance2).to.equal(total)
})
it("allows flowing tokens to be withdrawn", async function () {
const balance1Before = await token.balanceOf(holder.address)
const balance2Before = await token.balanceOf(holder2.address)
await vault.withdraw(fund, account1)
await vault.withdraw(fund, account2)
await mine()
const balance1After = await token.balanceOf(holder.address)
const balance2After = await token.balanceOf(holder2.address)
expect(balance1After - balance1Before).to.equal(deposit - total)
expect(balance2After - balance2Before).to.equal(total)
})
})
it("allows flowing tokens to be withdrawn", async function () {
await vault.flow(fund, account1, account2, 2)
await mine()
const start = await currentTime()
const total = (expiry - start) * 2
await advanceTimeTo(expiry + 10)
balance1Before = await token.balanceOf(holder.address)
balance2Before = await token.balanceOf(holder2.address)
await vault.withdraw(fund, account1)
await vault.withdraw(fund, account2)
await mine()
balance1After = await token.balanceOf(holder.address)
balance2After = await token.balanceOf(holder2.address)
expect(balance1After - balance1Before).to.equal(deposit - total)
expect(balance2After - balance2Before).to.equal(total)
describe("unlocked frozen flows", function () {
let total
beforeEach(async function () {
await vault.flow(fund, account1, account2, 2)
await mine()
const start = await currentTime()
await setNextBlockTimestamp(start + 10)
await vault.freezeFund(fund)
await mine()
const frozenAt = await currentTime()
total = (frozenAt - start) * 2
await advanceTimeTo(expiry)
})
it("stops flows at the time they were frozen", async function () {
const balance1 = await vault.getBalance(fund, account1)
const balance2 = await vault.getBalance(fund, account2)
expect(balance1).to.equal(deposit - total)
expect(balance2).to.equal(total)
})
it("allows frozen flows to be withdrawn", async function () {
balance1Before = await token.balanceOf(holder.address)
balance2Before = await token.balanceOf(holder2.address)
await vault.withdraw(fund, account1)
await vault.withdraw(fund, account2)
await mine()
balance1After = await token.balanceOf(holder.address)
balance2After = await token.balanceOf(holder2.address)
expect(balance1After - balance1Before).to.equal(deposit - total)
expect(balance2After - balance2Before).to.equal(total)
})
})
})
@ -874,7 +919,7 @@ describe("Vault", function () {
})
})
describe("when a fund is burned", function () {
describe("when a fund is frozen", function () {
const amount = 1000
let expiry
@ -886,32 +931,25 @@ describe("Vault", function () {
await token.connect(controller).approve(vault.address, amount)
await vault.lock(fund, expiry, expiry)
await vault.deposit(fund, account, amount)
await vault.burnFund(fund)
await vault.freezeFund(fund)
})
testBurnedFund()
describe("when the lock expires", function () {
beforeEach(async function () {
await advanceTimeTo(expiry)
})
testBurnedFund()
it("does not allow setting a lock", async function () {
const locking = vault.lock(fund, expiry, expiry)
await expect(locking).to.be.revertedWith("FundAlreadyLocked")
})
function testBurnedFund() {
it("cannot set lock", async function () {
const locking = vault.lock(fund, expiry, expiry)
await expect(locking).to.be.revertedWith("FundAlreadyLocked")
})
it("does not allow withdrawal", async function () {
const withdrawing = vault.withdraw(fund, account)
await expect(withdrawing).to.be.revertedWith("FundNotUnlocked")
})
it("cannot withdraw", async function () {
const withdrawing = vault.withdraw(fund, account)
await expect(withdrawing).to.be.revertedWith("FundNotUnlocked")
})
it("unlocks when the lock expires", async function () {
await advanceTimeTo(expiry)
expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Unlocked)
})
testFundThatIsNotLocked()
}
testFundThatIsNotLocked()
})
function testFundThatIsNotLocked() {
@ -966,8 +1004,8 @@ describe("Vault", function () {
)
})
it("does not allow burning an entire fund", async function () {
await expect(vault.burnFund(fund)).to.be.revertedWith("FundNotLocked")
it("does not allow freezing of a fund", async function () {
await expect(vault.freezeFund(fund)).to.be.revertedWith("FundNotLocked")
})
}
@ -1092,8 +1130,8 @@ describe("Vault", function () {
)
})
it("does not allow burning an entire fund", async function () {
await expect(vault.burnFund(fund)).to.be.revertedWith("EnforcedPause")
it("does not allow freezing of funds", async function () {
await expect(vault.freezeFund(fund)).to.be.revertedWith("EnforcedPause")
})
it("does not allow a controller to withdraw for a recipient", async function () {

View File

@ -1,8 +1,8 @@
const LockStatus = {
NoLock: 0,
Locked: 1,
Unlocked: 2,
Burned: 3,
Frozen: 2,
Unlocked: 3,
}
module.exports = { LockStatus }