vault: stop flowing when lock expires

This commit is contained in:
Mark Spanbroek 2025-01-23 10:29:53 +01:00
parent d9722d55fd
commit d9452a7ac2
3 changed files with 82 additions and 23 deletions

View File

@ -3,9 +3,14 @@ pragma solidity 0.8.28;
type Timestamp is uint64; type Timestamp is uint64;
using {_notEquals as !=} for Timestamp global;
using {_lessThan as <} for Timestamp global; using {_lessThan as <} for Timestamp global;
using {_atMost as <=} for Timestamp global; using {_atMost as <=} for Timestamp global;
function _notEquals(Timestamp a, Timestamp b) pure returns (bool) {
return Timestamp.unwrap(a) != Timestamp.unwrap(b);
}
function _lessThan(Timestamp a, Timestamp b) pure returns (bool) { function _lessThan(Timestamp a, Timestamp b) pure returns (bool) {
return Timestamp.unwrap(a) < Timestamp.unwrap(b); return Timestamp.unwrap(a) < Timestamp.unwrap(b);
} }
@ -18,4 +23,15 @@ library Timestamps {
function currentTime() internal view returns (Timestamp) { function currentTime() internal view returns (Timestamp) {
return Timestamp.wrap(uint64(block.timestamp)); return Timestamp.wrap(uint64(block.timestamp));
} }
function earliest(
Timestamp a,
Timestamp b
) internal pure returns (Timestamp) {
if (a <= b) {
return a;
} else {
return b;
}
}
} }

View File

@ -47,8 +47,7 @@ abstract contract VaultBase {
Recipient recipient Recipient recipient
) internal view returns (Balance memory) { ) internal view returns (Balance memory) {
Balance memory balance = _balances[controller][context][recipient]; Balance memory balance = _balances[controller][context][recipient];
Flow memory flow = _flows[controller][context][recipient]; int256 accumulated = _accumulateFlow(controller, context, recipient);
int256 accumulated = _accumulate(flow, Timestamps.currentTime());
if (accumulated >= 0) { if (accumulated >= 0) {
balance.designated += uint256(accumulated); balance.designated += uint256(accumulated);
} else { } else {
@ -57,14 +56,18 @@ abstract contract VaultBase {
return balance; return balance;
} }
function _accumulate( function _accumulateFlow(
Flow memory flow, Controller controller,
Timestamp end Context context,
) private pure returns (int256) { Recipient recipient
if (TokensPerSecond.unwrap(flow.rate) == 0) { ) private view returns (int256) {
Flow memory flow = _flows[controller][context][recipient];
if (flow.rate == TokensPerSecond.wrap(0)) {
return 0; return 0;
} }
uint64 duration = Timestamp.unwrap(end) - Timestamp.unwrap(flow.start); Timestamp expiry = _getLock(controller, context).expiry;
Timestamp flowEnd = Timestamps.earliest(Timestamps.currentTime(), expiry);
uint64 duration = Timestamp.unwrap(flowEnd) - Timestamp.unwrap(flow.start);
return TokensPerSecond.unwrap(flow.rate) * int256(uint256(duration)); return TokensPerSecond.unwrap(flow.rate) * int256(uint256(duration));
} }
@ -99,7 +102,10 @@ abstract contract VaultBase {
Context context, Context context,
Recipient recipient Recipient recipient
) internal { ) internal {
require(_getLock(controller, context).expiry <= Timestamps.currentTime(), Locked()); require(
_getLock(controller, context).expiry <= Timestamps.currentTime(),
Locked()
);
delete _locks[controller][context]; delete _locks[controller][context];
Balance memory balance = _getBalance(controller, context, recipient); Balance memory balance = _getBalance(controller, context, recipient);
uint256 amount = balance.available + balance.designated; uint256 amount = balance.available + balance.designated;
@ -178,6 +184,10 @@ abstract contract VaultBase {
Recipient to, Recipient to,
TokensPerSecond rate TokensPerSecond rate
) internal { ) internal {
require(
_getLock(controller, context).expiry != Timestamp.wrap(0),
LockRequired()
);
Timestamp start = Timestamps.currentTime(); Timestamp start = Timestamps.currentTime();
_flows[controller][context][to] = Flow({start: start, rate: rate}); _flows[controller][context][to] = Flow({start: start, rate: rate});
_flows[controller][context][from] = Flow({start: start, rate: -rate}); _flows[controller][context][from] = Flow({start: start, rate: -rate});
@ -189,4 +199,5 @@ abstract contract VaultBase {
error ExpiryPastMaximum(); error ExpiryPastMaximum();
error InvalidExpiry(); error InvalidExpiry();
error LockExpired(); error LockExpired();
error LockRequired();
} }

View File

@ -385,9 +385,14 @@ describe("Vault", function () {
const context = randomBytes(32) const context = randomBytes(32)
const amount = 42 const amount = 42
let sender
let receiver
beforeEach(async function () { beforeEach(async function () {
await token.connect(account).approve(vault.address, amount) await token.connect(account).approve(vault.address, amount)
await vault.deposit(context, account.address, amount) await vault.deposit(context, account.address, amount)
sender = account.address
receiver = account2.address
}) })
async function advanceTimeTo(timestamp) { async function advanceTimeTo(timestamp) {
@ -395,22 +400,49 @@ describe("Vault", function () {
await mine() await mine()
} }
it("requires that a lock is set", async function () {
await expect(vault.flow(context, sender, receiver, 2)).to.be.revertedWith(
"LockRequired"
)
})
describe("when a lock is set", async function () {
let expiry
beforeEach(async function () {
expiry = (await currentTime()) + 20
await vault.lockup(context, expiry, expiry)
})
it("moves tokens over time", async function () { it("moves tokens over time", async function () {
await vault.flow(context, account.address, account2.address, 2) await vault.flow(context, sender, receiver, 2)
const start = await currentTime() const start = await currentTime()
await advanceTimeTo(start + 2) await advanceTimeTo(start + 2)
expect(await vault.balance(context, account.address)).to.equal(amount - 4) expect(await vault.balance(context, sender)).to.equal(amount - 4)
expect(await vault.balance(context, account2.address)).to.equal(4) expect(await vault.balance(context, receiver)).to.equal(4)
await advanceTimeTo(start + 4) await advanceTimeTo(start + 4)
expect(await vault.balance(context, account.address)).to.equal(amount - 8) expect(await vault.balance(context, sender)).to.equal(amount - 8)
expect(await vault.balance(context, account2.address)).to.equal(8) expect(await vault.balance(context, receiver)).to.equal(8)
}) })
it("designates tokens that flow for the recipient", async function () { it("designates tokens that flow for the recipient", async function () {
await vault.flow(context, account.address, account2.address, 3) await vault.flow(context, sender, receiver, 3)
const start = await currentTime() const start = await currentTime()
await advanceTimeTo(start + 7) await advanceTimeTo(start + 7)
expect(await vault.designated(context, account2.address)).to.equal(21) expect(await vault.designated(context, receiver)).to.equal(21)
})
it("stops flowing when lock expires", async function () {
await vault.flow(context, sender, receiver, 2)
const start = await currentTime()
await advanceTimeTo(expiry)
const total = (expiry - start) * 2
expect(await vault.balance(context, sender)).to.equal(amount - total)
expect(await vault.balance(context, receiver)).to.equal(total)
await advanceTimeTo(expiry + 10)
expect(await vault.balance(context, sender)).to.equal(amount - total)
expect(await vault.balance(context, receiver)).to.equal(total)
})
}) })
}) })
}) })