From bbdd614579397c120ba26535d29563f41e2ba2ef Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Thu, 6 Feb 2025 10:58:21 +0100 Subject: [PATCH] vault: burn entire fund --- contracts/Vault.sol | 5 +++ contracts/vault/Locks.sol | 21 +++++++++- contracts/vault/VaultBase.sol | 39 ++++++++++++------ test/Vault.tests.js | 75 ++++++++++++++++++++++++++++++----- 4 files changed, 115 insertions(+), 25 deletions(-) diff --git a/contracts/Vault.sol b/contracts/Vault.sol index d16edb2..a9fc7d2 100644 --- a/contracts/Vault.sol +++ b/contracts/Vault.sol @@ -79,6 +79,11 @@ contract Vault is VaultBase { _burn(controller, fund, recipient); } + function burnAll(Fund fund) public { + Controller controller = Controller.wrap(msg.sender); + _burnAll(controller, fund); + } + function withdraw(Fund fund, Recipient recipient) public { Controller controller = Controller.wrap(msg.sender); _withdraw(controller, fund, recipient); diff --git a/contracts/vault/Locks.sol b/contracts/vault/Locks.sol index b0742ab..fff8416 100644 --- a/contracts/vault/Locks.sol +++ b/contracts/vault/Locks.sol @@ -7,10 +7,27 @@ struct Lock { Timestamp expiry; Timestamp maximum; uint128 value; + bool burned; +} + +enum LockStatus { + NoLock, + Locked, + Unlocked, + Burned } library Locks { - function isLocked(Lock memory lock) internal view returns (bool) { - return Timestamps.currentTime() < lock.expiry; + function status(Lock memory lock) internal view returns (LockStatus) { + if (lock.burned) { + return LockStatus.Burned; + } + if (Timestamps.currentTime() < lock.expiry) { + return LockStatus.Locked; + } + if (lock.maximum == Timestamp.wrap(0)) { + return LockStatus.NoLock; + } + return LockStatus.Unlocked; } } diff --git a/contracts/vault/VaultBase.sol b/contracts/vault/VaultBase.sol index 136aa15..a7f16ad 100644 --- a/contracts/vault/VaultBase.sol +++ b/contracts/vault/VaultBase.sol @@ -40,14 +40,19 @@ abstract contract VaultBase { Fund fund, Recipient recipient ) internal view returns (Balance memory) { - Account memory account = _accounts[controller][fund][recipient]; Lock memory lock = _locks[controller][fund]; - if (lock.isLocked()) { + LockStatus lockStatus = lock.status(); + if (lockStatus == LockStatus.Locked) { + Account memory account = _accounts[controller][fund][recipient]; account.update(Timestamps.currentTime()); - } else { - account.update(lock.expiry); + return account.balance; } - return account.balance; + if (lockStatus == LockStatus.Unlocked) { + Account memory account = _accounts[controller][fund][recipient]; + account.update(lock.expiry); + return account.balance; + } + return Balance({available: 0, designated: 0}); } function _lock( @@ -70,7 +75,7 @@ abstract contract VaultBase { Timestamp expiry ) internal { Lock memory lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); require(lock.expiry <= expiry, InvalidExpiry()); lock.expiry = expiry; _checkLockInvariant(lock); @@ -84,7 +89,7 @@ abstract contract VaultBase { uint128 amount ) internal { Lock storage lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); Account storage account = _accounts[controller][fund][recipient]; @@ -105,7 +110,7 @@ abstract contract VaultBase { uint128 amount ) internal { Lock memory lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); Account memory account = _accounts[controller][fund][recipient]; require(amount <= account.balance.available, InsufficientBalance()); @@ -125,7 +130,7 @@ abstract contract VaultBase { uint128 amount ) internal { Lock memory lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); Account memory sender = _accounts[controller][fund][from]; require(amount <= sender.balance.available, InsufficientBalance()); @@ -146,7 +151,7 @@ abstract contract VaultBase { TokensPerSecond rate ) internal { Lock memory lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); Account memory sender = _accounts[controller][fund][from]; sender.flowOut(rate); @@ -164,7 +169,7 @@ abstract contract VaultBase { Recipient recipient ) internal { Lock storage lock = _locks[controller][fund]; - require(lock.isLocked(), LockRequired()); + require(lock.status() == LockStatus.Locked, LockRequired()); Account memory account = _accounts[controller][fund][recipient]; require(account.flow.incoming == account.flow.outgoing, FlowMustBeZero()); @@ -177,13 +182,23 @@ abstract contract VaultBase { _token.safeTransfer(address(0xdead), amount); } + function _burnAll( + Controller controller, + Fund fund + ) internal { + Lock storage lock = _locks[controller][fund]; + require(lock.status() == LockStatus.Locked, LockRequired()); + + lock.burned = true; + } + function _withdraw( Controller controller, Fund fund, Recipient recipient ) internal { Lock memory lock = _locks[controller][fund]; - require(!lock.isLocked(), Locked()); + require(lock.status() == LockStatus.Unlocked, Locked()); Account memory account = _accounts[controller][fund][recipient]; account.update(lock.expiry); diff --git a/test/Vault.tests.js b/test/Vault.tests.js index f42a920..b02a6ea 100644 --- a/test/Vault.tests.js +++ b/test/Vault.tests.js @@ -56,8 +56,8 @@ describe("Vault", function () { await expect(locking).to.be.revertedWith("ExpiryPastMaximum") }) - describe("unlocked fund", function () { - testUnlockedFund() + describe("fund is not locked", function () { + testFundThatIsNotLocked() }) }) @@ -501,6 +501,15 @@ describe("Vault", function () { await vault.flow(fund, account2.address, account.address, 5) await expect(vault.burn(fund, account.address)).not.to.be.reverted }) + + it("can burn an entire fund", async function () { + await vault.transfer(fund, account.address, account2.address, 10) + await vault.transfer(fund, account.address, account3.address, 10) + await vault.burnAll(fund) + await expect(await vault.getBalance(fund, account.address)).to.equal(0) + await expect(await vault.getBalance(fund, account2.address)).to.equal(0) + await expect(await vault.getBalance(fund, account3.address)).to.equal(0) + }) }) describe("withdrawing", function () { @@ -630,6 +639,8 @@ describe("Vault", function () { setAutomine(true) await token.connect(controller).approve(vault.address, amount) await vault.deposit(fund, account.address, amount) + await token.connect(controller).approve(vault.address, amount) + await vault.deposit(fund, account2.address, amount) }) it("allows controller to withdraw for a recipient", async function () { @@ -676,16 +687,16 @@ describe("Vault", function () { }) it("can withdraw funds that were transfered in", async function () { - await vault.transfer(fund, account.address, account2.address, amount) + await vault.transfer(fund, account.address, account3.address, amount) await expire() - const before = await token.balanceOf(account2.address) - await vault.withdraw(fund, account2.address) - const after = await token.balanceOf(account2.address) + const before = await token.balanceOf(account3.address) + await vault.withdraw(fund, account3.address) + const after = await token.balanceOf(account3.address) expect(after - before).to.equal(amount) }) it("cannot withdraw funds that were transfered out", async function () { - await vault.transfer(fund, account.address, account2.address, amount) + await vault.transfer(fund, account.address, account3.address, amount) await expire() const before = await token.balanceOf(account.address) await vault.withdraw(fund, account.address) @@ -712,17 +723,55 @@ describe("Vault", function () { }) }) - describe("unlocked fund", function () { + describe("fund is not locked", function () { beforeEach(async function() { setAutomine(true) await expire() }) - testUnlockedFund() + testFundThatIsNotLocked() }) }) - function testUnlockedFund() { + describe("when a fund is burned", function () { + const amount = 1000 + + let expiry + + beforeEach(async function () { + expiry = (await currentTime()) + 100 + await token.connect(controller).approve(vault.address, amount) + await vault.lock(fund, expiry, expiry) + await vault.deposit(fund, account.address, amount) + await vault.burnAll(fund) + }) + + testBurnedFund() + + describe("when the lock expires", function () { + beforeEach(async function () { + await advanceTimeTo(expiry) + }) + + testBurnedFund() + }) + + function testBurnedFund() { + it("cannot set lock", async function () { + const locking = vault.lock(fund, expiry, maximum) + await expect(locking).to.be.revertedWith("AlreadyLocked") + }) + + it("cannot withdraw", async function () { + const withdrawing = vault.withdraw(fund, account.address) + await expect(withdrawing).to.be.revertedWith("Locked") + }) + + testFundThatIsNotLocked() + } + }) + + function testFundThatIsNotLocked() { it("does not allow extending of lock", async function () { await expect( vault.extendLock(fund, (await currentTime()) + 1) @@ -755,10 +804,14 @@ describe("Vault", function () { ).to.be.revertedWith("LockRequired") }) - it("does not allow burning of tokens", async function () { + it("does not allow burning of accounts", async function () { await expect(vault.burn(fund, account.address)).to.be.revertedWith( "LockRequired" ) }) + + it("does not allow burning an entire fund", async function () { + await expect(vault.burnAll(fund)).to.be.revertedWith("LockRequired") + }) } })