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);
}
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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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")
})
}
})