mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-02 21:33:08 +00:00
vault: rename Lock -> Fund
This commit is contained in:
parent
96b4fc0eff
commit
edee270eca
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
const LockStatus = {
|
||||
const FundStatus = {
|
||||
Inactive: 0,
|
||||
Locked: 1,
|
||||
Frozen: 2,
|
||||
Withdrawing: 3,
|
||||
}
|
||||
|
||||
module.exports = { LockStatus }
|
||||
module.exports = { FundStatus }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user