From 62aea7529590028561031895893d6fb10cdcc113 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 24 Feb 2025 15:40:39 +0100 Subject: [PATCH] vault: freezeFund() instead of burnFund() --- contracts/Vault.sol | 7 +- contracts/vault/Locks.sol | 37 ++++--- contracts/vault/Timestamps.sol | 5 + contracts/vault/VaultBase.sol | 12 +- test/Vault.tests.js | 194 ++++++++++++++++++++------------- test/vault.js | 4 +- 6 files changed, 155 insertions(+), 104 deletions(-) diff --git a/contracts/Vault.sol b/contracts/Vault.sol index 17f3f5e..89f9831 100644 --- a/contracts/Vault.sol +++ b/contracts/Vault.sol @@ -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 diff --git a/contracts/vault/Locks.sol b/contracts/vault/Locks.sol index 7c192e4..25efe89 100644 --- a/contracts/vault/Locks.sol +++ b/contracts/vault/Locks.sol @@ -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; + } } diff --git a/contracts/vault/Timestamps.sol b/contracts/vault/Timestamps.sol index 27343ac..dc63fa4 100644 --- a/contracts/vault/Timestamps.sol +++ b/contracts/vault/Timestamps.sol @@ -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); } diff --git a/contracts/vault/VaultBase.sol b/contracts/vault/VaultBase.sol index 54f08ea..843ddff 100644 --- a/contracts/vault/VaultBase.sol +++ b/contracts/vault/VaultBase.sol @@ -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; diff --git a/test/Vault.tests.js b/test/Vault.tests.js index 5f80e7d..6538fea 100644 --- a/test/Vault.tests.js +++ b/test/Vault.tests.js @@ -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 () { diff --git a/test/vault.js b/test/vault.js index 88f8197..4b43dea 100644 --- a/test/vault.js +++ b/test/vault.js @@ -1,8 +1,8 @@ const LockStatus = { NoLock: 0, Locked: 1, - Unlocked: 2, - Burned: 3, + Frozen: 2, + Unlocked: 3, } module.exports = { LockStatus }