From edee270eca445bb887966eb7487bb77a108c11e7 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 3 Mar 2025 13:20:58 +0100 Subject: [PATCH] vault: rename Lock -> Fund --- contracts/Vault.sol | 10 +- contracts/vault/{Locks.sol => Funds.sol} | 43 ++++--- contracts/vault/VaultBase.sol | 140 +++++++++++------------ test/Vault.tests.js | 18 +-- test/vault.js | 4 +- 5 files changed, 106 insertions(+), 109 deletions(-) rename contracts/vault/{Locks.sol => Funds.sol} (58%) diff --git a/contracts/Vault.sol b/contracts/Vault.sol index a1b3ef6..4fdd5b7 100644 --- a/contracts/Vault.sol +++ b/contracts/Vault.sol @@ -90,12 +90,12 @@ contract Vault is VaultBase, Pausable, Ownable { return balance.designated; } - /// Returns the status of the lock on the fund. Most operations on the vault - /// can only be done by the controller when the funds are locked. Withdrawal - /// can only be done when the funds are unlocked. - function getLockStatus(FundId fundId) public view returns (LockStatus) { + /// Returns the status of the fund. Most operations on the vault can only be + /// done by the controller when the funds are locked. Withdrawals can only be + /// done in the withdrawing state. + function getFundStatus(FundId fundId) public view returns (FundStatus) { Controller controller = Controller.wrap(msg.sender); - return _getLockStatus(controller, fundId); + return _getFundStatus(controller, fundId); } /// Returns the expiry time of the lock on the fund. A locked fund unlocks diff --git a/contracts/vault/Locks.sol b/contracts/vault/Funds.sol similarity index 58% rename from contracts/vault/Locks.sol rename to contracts/vault/Funds.sol index 298fd51..06bbfa8 100644 --- a/contracts/vault/Locks.sol +++ b/contracts/vault/Funds.sol @@ -3,19 +3,18 @@ pragma solidity 0.8.28; import "./Timestamps.sol"; -/// A time-lock for funds -struct Lock { - /// The lock unlocks at this time - Timestamp expiry; - /// The expiry can be extended no further than this - Timestamp maximum; +struct Fund { + /// The time-lock unlocks at this time + Timestamp lockExpiry; + /// The lock expiry can be extended no further than this + Timestamp lockMaximum; /// Indicates whether fund is frozen, and at what time Timestamp frozenAt; - /// The total amount of tokens locked up in the fund + /// The total amount of tokens in the fund uint128 value; } -/// A lock can go through the following states: +/// A fund can go through the following states: /// /// ----------------------------------------------- /// | | @@ -24,7 +23,7 @@ struct Lock { /// \ / /// --> Frozen -- /// -enum LockStatus { +enum FundStatus { /// Indicates that the fund is inactive and contains no tokens. This is the /// initial state, or the state after all tokens have been withdrawn. Inactive, @@ -40,24 +39,24 @@ enum LockStatus { Withdrawing } -library Locks { - function status(Lock memory lock) internal view returns (LockStatus) { - if (Timestamps.currentTime() < lock.expiry) { - if (lock.frozenAt != Timestamp.wrap(0)) { - return LockStatus.Frozen; +library Funds { + function status(Fund memory fund) internal view returns (FundStatus) { + if (Timestamps.currentTime() < fund.lockExpiry) { + if (fund.frozenAt != Timestamp.wrap(0)) { + return FundStatus.Frozen; } - return LockStatus.Locked; + return FundStatus.Locked; } - if (lock.maximum == Timestamp.wrap(0)) { - return LockStatus.Inactive; + if (fund.lockMaximum == Timestamp.wrap(0)) { + return FundStatus.Inactive; } - return LockStatus.Withdrawing; + return FundStatus.Withdrawing; } - function flowEnd(Lock memory lock) internal pure returns (Timestamp) { - if (lock.frozenAt != Timestamp.wrap(0)) { - return lock.frozenAt; + function flowEnd(Fund memory fund) internal pure returns (Timestamp) { + if (fund.frozenAt != Timestamp.wrap(0)) { + return fund.frozenAt; } - return lock.expiry; + return fund.lockExpiry; } } diff --git a/contracts/vault/VaultBase.sol b/contracts/vault/VaultBase.sol index ac2d603..bbbf1e6 100644 --- a/contracts/vault/VaultBase.sol +++ b/contracts/vault/VaultBase.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.28; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Accounts.sol"; -import "./Locks.sol"; +import "./Funds.sol"; /// Records account balances and token flows. Accounts are separated into funds. /// Funds are kept separate between controllers. @@ -17,30 +17,30 @@ import "./Locks.sol"; /// The lock invariant ensures that there is a maximum time that a fund can be /// locked: /// -/// (∀ controller ∈ Controller, fund ∈ FundId: -/// lock.expiry <= lock.maximum -/// where lock = _locks[controller][fund]) +/// (∀ controller ∈ Controller, fundId ∈ FundId: +/// fund.lockExpiry <= fund.lockMaximum +/// where fund = _funds[controller][fundId]) /// /// The account invariant ensures that the outgoing token flow can be sustained /// for the maximum time that a fund can be locked: /// -/// (∀ controller ∈ Controller, fund ∈ FundId, account ∈ AccountId: -/// flow.outgoing * (lock.maximum - flow.updated) <= balance.available -/// where lock = _locks[controller][fund]) -/// and flow = _accounts[controller][fund][account].flow -/// and balance = _accounts[controller][fund][account].balance +/// (∀ controller ∈ Controller, fundId ∈ FundId, accountId ∈ AccountId: +/// flow.outgoing * (fund.lockMaximum - flow.updated) <= balance.available +/// where fund = _funds[controller][fundId]) +/// and flow = _accounts[controller][fundId][accountId].flow +/// and balance = _accounts[controller][fundId][accountId].balance /// /// The flow invariant ensures that incoming and outgoing flow rates match: /// -/// (∀ controller ∈ Controller, fund ∈ FundId: -/// (∑ account ∈ AccountId: accounts[account].flow.incoming) = -/// (∑ account ∈ AccountId: accounts[account].flow.outgoing) -/// where accounts = _accounts[controller][fund]) +/// (∀ controller ∈ Controller, fundId ∈ FundId: +/// (∑ accountId ∈ AccountId: accounts[accountId].flow.incoming) = +/// (∑ accountId ∈ AccountId: accounts[accountId].flow.outgoing) +/// where accounts = _accounts[controller][fundId]) /// abstract contract VaultBase { using SafeERC20 for IERC20; using Accounts for Account; - using Locks for Lock; + using Funds for Fund; IERC20 internal immutable _token; @@ -49,8 +49,8 @@ abstract contract VaultBase { /// Unique identifier for a fund, chosen by the controller type FundId is bytes32; - /// Each fund has its own time lock - mapping(Controller => mapping(FundId => Lock)) private _locks; + /// Each controller has its own set of funds + mapping(Controller => mapping(FundId => Fund)) private _funds; /// Each account holder has its own set of accounts in a fund mapping(Controller => mapping(FundId => mapping(AccountId => Account))) private _accounts; @@ -59,18 +59,18 @@ abstract contract VaultBase { _token = token; } - function _getLockStatus( + function _getFundStatus( Controller controller, FundId fundId - ) internal view returns (LockStatus) { - return _locks[controller][fundId].status(); + ) internal view returns (FundStatus) { + return _funds[controller][fundId].status(); } function _getLockExpiry( Controller controller, FundId fundId ) internal view returns (Timestamp) { - return _locks[controller][fundId].expiry; + return _funds[controller][fundId].lockExpiry; } function _getBalance( @@ -78,18 +78,16 @@ abstract contract VaultBase { FundId fundId, AccountId accountId ) internal view returns (Balance memory) { - Lock memory lock = _locks[controller][fundId]; - LockStatus lockStatus = lock.status(); - if (lockStatus == LockStatus.Locked) { + Fund memory fund = _funds[controller][fundId]; + FundStatus status = fund.status(); + if (status == FundStatus.Locked) { Account memory account = _accounts[controller][fundId][accountId]; account.update(Timestamps.currentTime()); return account.balance; } - if ( - lockStatus == LockStatus.Withdrawing || lockStatus == LockStatus.Frozen - ) { + if (status == FundStatus.Withdrawing || status == FundStatus.Frozen) { Account memory account = _accounts[controller][fundId][accountId]; - account.update(lock.flowEnd()); + account.update(fund.flowEnd()); return account.balance; } return Balance({available: 0, designated: 0}); @@ -101,12 +99,12 @@ abstract contract VaultBase { Timestamp expiry, Timestamp maximum ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Inactive, VaultFundAlreadyLocked()); - lock.expiry = expiry; - lock.maximum = maximum; - _checkLockInvariant(lock); - _locks[controller][fundId] = lock; + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Inactive, VaultFundAlreadyLocked()); + fund.lockExpiry = expiry; + fund.lockMaximum = maximum; + _checkLockInvariant(fund); + _funds[controller][fundId] = fund; } function _extendLock( @@ -114,12 +112,12 @@ abstract contract VaultBase { FundId fundId, Timestamp expiry ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); - require(lock.expiry <= expiry, VaultInvalidExpiry()); - lock.expiry = expiry; - _checkLockInvariant(lock); - _locks[controller][fundId] = lock; + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); + require(fund.lockExpiry <= expiry, VaultInvalidExpiry()); + fund.lockExpiry = expiry; + _checkLockInvariant(fund); + _funds[controller][fundId] = fund; } function _deposit( @@ -128,13 +126,13 @@ abstract contract VaultBase { AccountId accountId, uint128 amount ) internal { - Lock storage lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund storage fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account storage account = _accounts[controller][fundId][accountId]; account.balance.available += amount; - lock.value += amount; + fund.value += amount; _token.safeTransferFrom( Controller.unwrap(controller), @@ -149,15 +147,15 @@ abstract contract VaultBase { AccountId accountId, uint128 amount ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account memory account = _accounts[controller][fundId][accountId]; require(amount <= account.balance.available, VaultInsufficientBalance()); account.balance.available -= amount; account.balance.designated += amount; - _checkAccountInvariant(account, lock); + _checkAccountInvariant(account, fund); _accounts[controller][fundId][accountId] = account; } @@ -169,14 +167,14 @@ abstract contract VaultBase { AccountId to, uint128 amount ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account memory sender = _accounts[controller][fundId][from]; require(amount <= sender.balance.available, VaultInsufficientBalance()); sender.balance.available -= amount; - _checkAccountInvariant(sender, lock); + _checkAccountInvariant(sender, fund); _accounts[controller][fundId][from] = sender; @@ -190,12 +188,12 @@ abstract contract VaultBase { AccountId to, TokensPerSecond rate ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account memory sender = _accounts[controller][fundId][from]; sender.flowOut(rate); - _checkAccountInvariant(sender, lock); + _checkAccountInvariant(sender, fund); _accounts[controller][fundId][from] = sender; Account memory receiver = _accounts[controller][fundId][to]; @@ -209,15 +207,15 @@ abstract contract VaultBase { AccountId accountId, uint128 amount ) internal { - Lock storage lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund storage fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account storage account = _accounts[controller][fundId][accountId]; require(account.balance.designated >= amount, VaultInsufficientBalance()); account.balance.designated -= amount; - lock.value -= amount; + fund.value -= amount; _token.safeTransfer(address(0xdead), amount); } @@ -227,14 +225,14 @@ abstract contract VaultBase { FundId fundId, AccountId accountId ) internal { - Lock storage lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund storage fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); Account memory account = _accounts[controller][fundId][accountId]; require(account.flow.incoming == account.flow.outgoing, VaultFlowNotZero()); uint128 amount = account.balance.available + account.balance.designated; - lock.value -= amount; + fund.value -= amount; delete _accounts[controller][fundId][accountId]; @@ -242,10 +240,10 @@ abstract contract VaultBase { } function _freezeFund(Controller controller, FundId fundId) internal { - Lock storage lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); + Fund storage fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Locked, VaultFundNotLocked()); - lock.frozenAt = Timestamps.currentTime(); + fund.frozenAt = Timestamps.currentTime(); } function _withdraw( @@ -253,19 +251,19 @@ abstract contract VaultBase { FundId fundId, AccountId accountId ) internal { - Lock memory lock = _locks[controller][fundId]; - require(lock.status() == LockStatus.Withdrawing, VaultFundNotUnlocked()); + Fund memory fund = _funds[controller][fundId]; + require(fund.status() == FundStatus.Withdrawing, VaultFundNotUnlocked()); Account memory account = _accounts[controller][fundId][accountId]; - account.update(lock.flowEnd()); + account.update(fund.flowEnd()); uint128 amount = account.balance.available + account.balance.designated; - lock.value -= amount; + fund.value -= amount; - if (lock.value == 0) { - delete _locks[controller][fundId]; + if (fund.value == 0) { + delete _funds[controller][fundId]; } else { - _locks[controller][fundId] = lock; + _funds[controller][fundId] = fund; } delete _accounts[controller][fundId][accountId]; @@ -274,15 +272,15 @@ abstract contract VaultBase { _token.safeTransfer(owner, amount); } - function _checkLockInvariant(Lock memory lock) private pure { - require(lock.expiry <= lock.maximum, VaultInvalidExpiry()); + function _checkLockInvariant(Fund memory fund) private pure { + require(fund.lockExpiry <= fund.lockMaximum, VaultInvalidExpiry()); } function _checkAccountInvariant( Account memory account, - Lock memory lock + Fund memory fund ) private pure { - require(account.isSolventAt(lock.maximum), VaultInsufficientBalance()); + require(account.isSolventAt(fund.lockMaximum), VaultInsufficientBalance()); } error VaultInsufficientBalance(); diff --git a/test/Vault.tests.js b/test/Vault.tests.js index aa7ef89..574b04f 100644 --- a/test/Vault.tests.js +++ b/test/Vault.tests.js @@ -10,7 +10,7 @@ const { snapshot, revert, } = require("./evm") -const { LockStatus } = require("./vault") +const { FundStatus } = require("./vault") describe("Vault", function () { const fund = randomBytes(32) @@ -69,7 +69,7 @@ describe("Vault", function () { const expiry = (await currentTime()) + 80 const maximum = (await currentTime()) + 100 await vault.lock(fund, expiry, maximum) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Locked) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Locked) expect(await vault.getLockExpiry(fund)).to.equal(expiry) }) @@ -131,7 +131,7 @@ describe("Vault", function () { await token.connect(controller).approve(vault.address, 30) await vault.deposit(fund, account, 30) await vault.burnAccount(fund, account) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Locked) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Locked) expect(await vault.getLockExpiry(fund)).to.not.equal(0) }) }) @@ -606,7 +606,7 @@ describe("Vault", function () { it("can freeze a fund", async function () { await setAutomine(true) await vault.freezeFund(fund) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Frozen) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Frozen) }) it("stops all token flows", async function () { @@ -676,10 +676,10 @@ describe("Vault", function () { it("unlocks the funds", async function () { await mine() - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Locked) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Locked) await expire() await mine() - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Withdrawing) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Withdrawing) }) describe("locking", function () { @@ -706,13 +706,13 @@ describe("Vault", function () { await expire() // some tokens are withdrawn await vault.withdraw(fund, account1) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Withdrawing) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Withdrawing) expect(await vault.getLockExpiry(fund)).not.to.equal(0) // remainder of the tokens are withdrawn by recipient await vault .connect(holder3) .withdrawByRecipient(controller.address, fund, account3) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Inactive) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Inactive) expect(await vault.getLockExpiry(fund)).to.equal(0) }) }) @@ -946,7 +946,7 @@ describe("Vault", function () { it("unlocks when the lock expires", async function () { await advanceTimeTo(expiry) - expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Withdrawing) + expect(await vault.getFundStatus(fund)).to.equal(FundStatus.Withdrawing) }) testFundThatIsNotLocked() diff --git a/test/vault.js b/test/vault.js index dc9885a..753b3a5 100644 --- a/test/vault.js +++ b/test/vault.js @@ -1,8 +1,8 @@ -const LockStatus = { +const FundStatus = { Inactive: 0, Locked: 1, Frozen: 2, Withdrawing: 3, } -module.exports = { LockStatus } +module.exports = { FundStatus }