vault: burn entire fund

This commit is contained in:
Mark Spanbroek 2025-02-06 10:58:21 +01:00
parent 682b65b24a
commit bbdd614579
4 changed files with 115 additions and 25 deletions

View File

@ -79,6 +79,11 @@ contract Vault is VaultBase {
_burn(controller, fund, recipient); _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 { function withdraw(Fund fund, Recipient recipient) public {
Controller controller = Controller.wrap(msg.sender); Controller controller = Controller.wrap(msg.sender);
_withdraw(controller, fund, recipient); _withdraw(controller, fund, recipient);

View File

@ -7,10 +7,27 @@ struct Lock {
Timestamp expiry; Timestamp expiry;
Timestamp maximum; Timestamp maximum;
uint128 value; uint128 value;
bool burned;
}
enum LockStatus {
NoLock,
Locked,
Unlocked,
Burned
} }
library Locks { library Locks {
function isLocked(Lock memory lock) internal view returns (bool) { function status(Lock memory lock) internal view returns (LockStatus) {
return Timestamps.currentTime() < lock.expiry; 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;
} }
} }

View File

@ -40,14 +40,19 @@ abstract contract VaultBase {
Fund fund, Fund fund,
Recipient recipient Recipient recipient
) internal view returns (Balance memory) { ) internal view returns (Balance memory) {
Account memory account = _accounts[controller][fund][recipient];
Lock memory lock = _locks[controller][fund]; 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()); account.update(Timestamps.currentTime());
} else { return account.balance;
account.update(lock.expiry);
} }
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( function _lock(
@ -70,7 +75,7 @@ abstract contract VaultBase {
Timestamp expiry Timestamp expiry
) internal { ) internal {
Lock memory lock = _locks[controller][fund]; Lock memory lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
require(lock.expiry <= expiry, InvalidExpiry()); require(lock.expiry <= expiry, InvalidExpiry());
lock.expiry = expiry; lock.expiry = expiry;
_checkLockInvariant(lock); _checkLockInvariant(lock);
@ -84,7 +89,7 @@ abstract contract VaultBase {
uint128 amount uint128 amount
) internal { ) internal {
Lock storage lock = _locks[controller][fund]; Lock storage lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
Account storage account = _accounts[controller][fund][recipient]; Account storage account = _accounts[controller][fund][recipient];
@ -105,7 +110,7 @@ abstract contract VaultBase {
uint128 amount uint128 amount
) internal { ) internal {
Lock memory lock = _locks[controller][fund]; Lock memory lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
Account memory account = _accounts[controller][fund][recipient]; Account memory account = _accounts[controller][fund][recipient];
require(amount <= account.balance.available, InsufficientBalance()); require(amount <= account.balance.available, InsufficientBalance());
@ -125,7 +130,7 @@ abstract contract VaultBase {
uint128 amount uint128 amount
) internal { ) internal {
Lock memory lock = _locks[controller][fund]; Lock memory lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
Account memory sender = _accounts[controller][fund][from]; Account memory sender = _accounts[controller][fund][from];
require(amount <= sender.balance.available, InsufficientBalance()); require(amount <= sender.balance.available, InsufficientBalance());
@ -146,7 +151,7 @@ abstract contract VaultBase {
TokensPerSecond rate TokensPerSecond rate
) internal { ) internal {
Lock memory lock = _locks[controller][fund]; Lock memory lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
Account memory sender = _accounts[controller][fund][from]; Account memory sender = _accounts[controller][fund][from];
sender.flowOut(rate); sender.flowOut(rate);
@ -164,7 +169,7 @@ abstract contract VaultBase {
Recipient recipient Recipient recipient
) internal { ) internal {
Lock storage lock = _locks[controller][fund]; Lock storage lock = _locks[controller][fund];
require(lock.isLocked(), LockRequired()); require(lock.status() == LockStatus.Locked, LockRequired());
Account memory account = _accounts[controller][fund][recipient]; Account memory account = _accounts[controller][fund][recipient];
require(account.flow.incoming == account.flow.outgoing, FlowMustBeZero()); require(account.flow.incoming == account.flow.outgoing, FlowMustBeZero());
@ -177,13 +182,23 @@ abstract contract VaultBase {
_token.safeTransfer(address(0xdead), amount); _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( function _withdraw(
Controller controller, Controller controller,
Fund fund, Fund fund,
Recipient recipient Recipient recipient
) internal { ) internal {
Lock memory lock = _locks[controller][fund]; Lock memory lock = _locks[controller][fund];
require(!lock.isLocked(), Locked()); require(lock.status() == LockStatus.Unlocked, Locked());
Account memory account = _accounts[controller][fund][recipient]; Account memory account = _accounts[controller][fund][recipient];
account.update(lock.expiry); account.update(lock.expiry);

View File

@ -56,8 +56,8 @@ describe("Vault", function () {
await expect(locking).to.be.revertedWith("ExpiryPastMaximum") await expect(locking).to.be.revertedWith("ExpiryPastMaximum")
}) })
describe("unlocked fund", function () { describe("fund is not locked", function () {
testUnlockedFund() testFundThatIsNotLocked()
}) })
}) })
@ -501,6 +501,15 @@ describe("Vault", function () {
await vault.flow(fund, account2.address, account.address, 5) await vault.flow(fund, account2.address, account.address, 5)
await expect(vault.burn(fund, account.address)).not.to.be.reverted 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 () { describe("withdrawing", function () {
@ -630,6 +639,8 @@ describe("Vault", function () {
setAutomine(true) setAutomine(true)
await token.connect(controller).approve(vault.address, amount) await token.connect(controller).approve(vault.address, amount)
await vault.deposit(fund, account.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 () { 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 () { 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() await expire()
const before = await token.balanceOf(account2.address) const before = await token.balanceOf(account3.address)
await vault.withdraw(fund, account2.address) await vault.withdraw(fund, account3.address)
const after = await token.balanceOf(account2.address) const after = await token.balanceOf(account3.address)
expect(after - before).to.equal(amount) expect(after - before).to.equal(amount)
}) })
it("cannot withdraw funds that were transfered out", async function () { 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() await expire()
const before = await token.balanceOf(account.address) const before = await token.balanceOf(account.address)
await vault.withdraw(fund, 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() { beforeEach(async function() {
setAutomine(true) setAutomine(true)
await expire() 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 () { it("does not allow extending of lock", async function () {
await expect( await expect(
vault.extendLock(fund, (await currentTime()) + 1) vault.extendLock(fund, (await currentTime()) + 1)
@ -755,10 +804,14 @@ describe("Vault", function () {
).to.be.revertedWith("LockRequired") ).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( await expect(vault.burn(fund, account.address)).to.be.revertedWith(
"LockRequired" "LockRequired"
) )
}) })
it("does not allow burning an entire fund", async function () {
await expect(vault.burnAll(fund)).to.be.revertedWith("LockRequired")
})
} }
}) })