mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-05 23:03:12 +00:00
vault: add documentation
This commit is contained in:
parent
439d3772db
commit
43cd44330b
@ -3,9 +3,53 @@ pragma solidity 0.8.28;
|
||||
|
||||
import "./vault/VaultBase.sol";
|
||||
|
||||
/// A vault provides a means for smart contracts to control allocation of ERC20
|
||||
/// tokens without the need to hold the ERC20 tokens themselves, thereby
|
||||
/// decreasing their own attack surface.
|
||||
///
|
||||
/// A vault keeps track of funds for a smart contract. This smart contract is
|
||||
/// called the controller of the funds. Each controller has its own independent
|
||||
/// set of funds. Each fund has a number of recipients, and every recipient has
|
||||
/// its own account within the fund.
|
||||
///
|
||||
/// Vault -> Controller -> Fund -> Account
|
||||
///
|
||||
/// Funds are identified by a unique 32 byte identifier, chosen by the
|
||||
/// controller.
|
||||
///
|
||||
/// An account has a balance, of which a part can be designated. Designated
|
||||
/// tokens can no longer be transfered to another account.
|
||||
///
|
||||
/// A typical flow in which a controller uses the vault to handle funds:
|
||||
/// 1. the controller chooses a unique id for the fund
|
||||
/// 2. the controller locks the fund for an amount of time
|
||||
/// 3. the controller deposits ERC20 tokens into the fund
|
||||
/// 4. the controller transfers tokens between accounts in the fund
|
||||
/// 5. the fund unlocks after a while, freezing the account balances
|
||||
/// 6. the controller withdraws ERC20 tokens from the fund for a recipient, or
|
||||
/// the recipient initiates the withdrawal itself
|
||||
///
|
||||
/// The vault makes it harder for an attacker to extract funds, through several
|
||||
/// mechanisms:
|
||||
/// - tokens in a fund can only be reassigned while the fund is time-locked, and
|
||||
/// only be withdrawn after the lock unlocks, delaying an attacker's attempt
|
||||
/// at extraction of tokens from the vault
|
||||
/// - tokens in a fund can not be reassigned when the lock unlocks, ensuring
|
||||
/// that they can no longer be reassigned to an attacker
|
||||
/// - when storing collateral, it can be designated for the collateral provider,
|
||||
/// ensuring that it cannot be reassigned to an attacker
|
||||
/// - malicious upgrades to a fund controller cannot prevent recipients from
|
||||
/// withdrawing their tokens
|
||||
/// - burning tokens in a fund ensures that these tokens can no longer be
|
||||
/// extracted by an attacker
|
||||
///
|
||||
contract Vault is VaultBase {
|
||||
|
||||
constructor(IERC20 token) VaultBase(token) {}
|
||||
|
||||
/// The amount of tokens that are currently assigned to a recipient in a fund.
|
||||
/// This includes available and designated tokens. Available tokens can be
|
||||
/// transfered to other accounts, but designated tokens cannot.
|
||||
function getBalance(
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
@ -15,6 +59,8 @@ contract Vault is VaultBase {
|
||||
return balance.available + balance.designated;
|
||||
}
|
||||
|
||||
/// The amount of tokens that are currently designated to a recipient in a
|
||||
/// fund. These tokens can no longer be transfered to other accounts.
|
||||
function getDesignatedBalance(
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
@ -24,31 +70,49 @@ contract Vault is VaultBase {
|
||||
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(Fund fund) public view returns (LockStatus) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
return _getLockStatus(controller, fund);
|
||||
}
|
||||
|
||||
/// Returns the expiry time of the lock on the fund. A locked fund unlocks
|
||||
/// automatically at this timestamp.
|
||||
function getLockExpiry(Fund fund) public view returns (Timestamp) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
return _getLockExpiry(controller, fund);
|
||||
}
|
||||
|
||||
/// Locks the fund until the expiry timestamp. The lock expiry can be extended
|
||||
/// later, but no more than the maximum timestamp.
|
||||
function lock(Fund fund, Timestamp expiry, Timestamp maximum) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_lock(controller, fund, expiry, maximum);
|
||||
}
|
||||
|
||||
/// Delays unlocking of a locked fund. The new expiry should be later than
|
||||
/// the existing expiry, but no later than the maximum timestamp that was
|
||||
/// provided when locking the fund.
|
||||
function extendLock(Fund fund, Timestamp expiry) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_extendLock(controller, fund, expiry);
|
||||
}
|
||||
|
||||
/// Deposits an amount of tokens into the vault, and adds them to the balance
|
||||
/// of the recipient. ERC20 tokens are transfered from the caller to the vault
|
||||
/// contract.
|
||||
/// Only allowed when the fund is locked.
|
||||
function deposit(Fund fund, Recipient recipient, uint128 amount) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_deposit(controller, fund, recipient, amount);
|
||||
}
|
||||
|
||||
/// Takes an amount of tokens from the recipient's balance and designates them
|
||||
/// for the recipient. These tokens are no longer available to be transfered
|
||||
/// to other accounts.
|
||||
/// Only allowed when the fund is locked.
|
||||
function designate(
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
@ -58,6 +122,9 @@ contract Vault is VaultBase {
|
||||
_designate(controller, fund, recipient, amount);
|
||||
}
|
||||
|
||||
/// Transfers an amount of tokens from the acount of one recipient to the
|
||||
/// other.
|
||||
/// Only allowed when the fund is locked.
|
||||
function transfer(
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
@ -68,6 +135,12 @@ contract Vault is VaultBase {
|
||||
_transfer(controller, fund, from, to, amount);
|
||||
}
|
||||
|
||||
/// Transfers tokens from the account of one recipient to the other over time.
|
||||
/// Every second a number of tokens are transfered, until the fund is
|
||||
/// unlocked.
|
||||
/// Only allowed when the fund is locked.
|
||||
/// Only allowed when the balance is sufficient to sustain the flow until the
|
||||
/// fund unlocks, even if the lock expiry time is extended to its maximum.
|
||||
function flow(
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
@ -78,26 +151,41 @@ contract Vault is VaultBase {
|
||||
_flow(controller, fund, from, to, rate);
|
||||
}
|
||||
|
||||
/// Burns an amount of designated tokens from the account of the recipient.
|
||||
/// Only allowed when the fund is locked.
|
||||
function burnDesignated(Fund fund, Recipient recipient, uint128 amount) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_burnDesignated(controller, fund, recipient, amount);
|
||||
}
|
||||
|
||||
/// Burns all tokens from the account of the recipient.
|
||||
/// Only allowed when the fund is locked.
|
||||
function burnAccount(Fund fund, Recipient recipient) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_burnAccount(controller, fund, recipient);
|
||||
}
|
||||
|
||||
/// Burns all tokens from all accounts in a fund.
|
||||
/// Only allowed when the fund is locked.
|
||||
function burnFund(Fund fund) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_burnFund(controller, fund);
|
||||
}
|
||||
|
||||
/// Transfers all ERC20 tokens in the recipient's account out of the vault to
|
||||
/// the recipient address.
|
||||
/// Only allowed when the fund is unlocked.
|
||||
/// ⚠️ The recipient can also withdraw itself, so when designing a smart
|
||||
/// contract that controls funds in the vault, don't assume that only this
|
||||
/// smart contract can initiate a withdrawal ⚠️
|
||||
function withdraw(Fund fund, Recipient recipient) public {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_withdraw(controller, fund, recipient);
|
||||
}
|
||||
|
||||
/// Allows a recipient to withdraw its tokens from a fund directly, bypassing
|
||||
/// the need to ask the controller of the fund to initiate the withdrawal.
|
||||
/// Only allowed when the fund is unlocked.
|
||||
function withdrawByRecipient(Controller controller, Fund fund) public {
|
||||
Recipient recipient = Recipient.wrap(msg.sender);
|
||||
_withdraw(controller, fund, recipient);
|
||||
|
||||
@ -4,19 +4,31 @@ pragma solidity 0.8.28;
|
||||
import "./TokenFlows.sol";
|
||||
import "./Timestamps.sol";
|
||||
|
||||
/// Records the token balance and the incoming and outgoing token flows
|
||||
struct Account {
|
||||
Balance balance;
|
||||
Flow flow;
|
||||
}
|
||||
|
||||
/// The account balance. Fits in 32 bytes to minimize storage costs.
|
||||
/// A uint128 is used to record the amount of tokens, which should be more than
|
||||
/// enough. Given a standard 18 decimal places for the ERC20 token, this still
|
||||
/// allows for 10^20 whole coins.
|
||||
struct Balance {
|
||||
/// Available tokens can be transfered
|
||||
uint128 available;
|
||||
/// Designated tokens can no longer be transfered
|
||||
uint128 designated;
|
||||
}
|
||||
|
||||
/// The incoming and outgoing flows of an account. Fits in 32 bytes to minimize
|
||||
/// storage costs.
|
||||
struct Flow {
|
||||
/// Rate of outgoing tokens
|
||||
TokensPerSecond outgoing;
|
||||
/// Rate of incoming tokens
|
||||
TokensPerSecond incoming;
|
||||
/// Last time that the flow was updated
|
||||
Timestamp updated;
|
||||
}
|
||||
|
||||
@ -25,6 +37,8 @@ library Accounts {
|
||||
using TokenFlows for TokensPerSecond;
|
||||
using Timestamps for Timestamp;
|
||||
|
||||
/// Calculates whether the available balance is sufficient to sustain the
|
||||
/// outgoing flow of tokens until the specified timestamp
|
||||
function isSolventAt(
|
||||
Account memory account,
|
||||
Timestamp timestamp
|
||||
@ -34,6 +48,10 @@ library Accounts {
|
||||
return outgoing <= account.balance.available;
|
||||
}
|
||||
|
||||
/// Updates the available and designated balances by accumulating the
|
||||
/// outgoing and incoming flows up until the specified timestamp. Outgoing
|
||||
/// tokens are deducted from the available balance. Incoming tokens are added
|
||||
/// to the designated tokens.
|
||||
function update(Account memory account, Timestamp timestamp) internal pure {
|
||||
Duration duration = account.flow.updated.until(timestamp);
|
||||
account.balance.available -= account.flow.outgoing.accumulate(duration);
|
||||
@ -41,11 +59,17 @@ library Accounts {
|
||||
account.flow.updated = timestamp;
|
||||
}
|
||||
|
||||
/// Starts an incoming flow of tokens at the specified rate. If there already
|
||||
/// is a flow of incoming tokens, then its rate is increased accordingly.
|
||||
function flowIn(Account memory account, TokensPerSecond rate) internal view {
|
||||
account.update(Timestamps.currentTime());
|
||||
account.flow.incoming = account.flow.incoming + rate;
|
||||
}
|
||||
|
||||
/// Starts an outgoing flow of tokens at the specified rate. If there is
|
||||
/// already a flow of incoming tokens, then these are used to pay for the
|
||||
/// outgoing flow. If there are insuffient incoming tokens, then the outgoing
|
||||
/// rate is increased.
|
||||
function flowOut(Account memory account, TokensPerSecond rate) internal view {
|
||||
account.update(Timestamps.currentTime());
|
||||
if (rate <= account.flow.incoming) {
|
||||
|
||||
@ -3,17 +3,35 @@ 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;
|
||||
/// The total amount of tokens locked up in the fund
|
||||
uint128 value;
|
||||
/// Indicates whether the fund was burned
|
||||
bool burned;
|
||||
}
|
||||
|
||||
/// A lock can go through the following states:
|
||||
///
|
||||
/// ----------------------------------------
|
||||
/// | |
|
||||
/// --> NoLock ---> Locked ---> UnLocked --
|
||||
/// \
|
||||
/// ---> Burned
|
||||
///
|
||||
enum LockStatus {
|
||||
/// Indicates that no lock is set. This is the initial state, or the state
|
||||
/// after all tokens have been withdrawn.
|
||||
NoLock,
|
||||
/// Indicates that the funds are locked. Withdrawing tokens is not allowed.
|
||||
Locked,
|
||||
/// Indicates that the lock is unlocked. Withdrawing is allowed.
|
||||
Unlocked,
|
||||
/// Indicates that all tokens in the fund are burned
|
||||
Burned
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
/// Represents a moment in time, represented as unix time (number of seconds
|
||||
/// since 1970). Uses a uint40 to facilitate efficient packing in structs. A
|
||||
/// uint40 allows times to be represented for the coming 30 000 years.
|
||||
type Timestamp is uint40;
|
||||
/// Represents a duration of time in seconds
|
||||
type Duration is uint40;
|
||||
|
||||
using {_timestampEquals as ==} for Timestamp global;
|
||||
@ -21,10 +25,12 @@ function _timestampAtMost(Timestamp a, Timestamp b) pure returns (bool) {
|
||||
}
|
||||
|
||||
library Timestamps {
|
||||
/// Returns the current block timestamp converted to a Timestamp type
|
||||
function currentTime() internal view returns (Timestamp) {
|
||||
return Timestamp.wrap(uint40(block.timestamp));
|
||||
}
|
||||
|
||||
/// Calculates the duration from start until end
|
||||
function until(
|
||||
Timestamp start,
|
||||
Timestamp end
|
||||
|
||||
@ -3,6 +3,9 @@ pragma solidity 0.8.28;
|
||||
|
||||
import "./Timestamps.sol";
|
||||
|
||||
/// Represents a flow of tokens. Uses a uint96 to represent the flow rate, which
|
||||
/// should be more than enough. Given a standard 18 decimal places for the
|
||||
/// ERC20 token, this still allows for a rate of 10^10 whole coins per second.
|
||||
type TokensPerSecond is uint96;
|
||||
|
||||
using {_tokensPerSecondMinus as -} for TokensPerSecond global;
|
||||
@ -41,6 +44,8 @@ function _tokensPerSecondAtMost(
|
||||
}
|
||||
|
||||
library TokenFlows {
|
||||
/// Calculates how many tokens are accumulated when a token flow is maintained
|
||||
/// for a duration of time.
|
||||
function accumulate(
|
||||
TokensPerSecond rate,
|
||||
Duration duration
|
||||
|
||||
@ -6,6 +6,36 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "./Accounts.sol";
|
||||
import "./Locks.sol";
|
||||
|
||||
/// Records account balances and token flows. Accounts are separated into funds.
|
||||
/// Funds are kept separate between controllers.
|
||||
///
|
||||
/// A fund can only be manipulated by a controller when it is locked. Tokens can
|
||||
/// only be withdrawn when a fund is unlocked.
|
||||
///
|
||||
/// The vault maintains a number of invariants to ensure its integrity.
|
||||
///
|
||||
/// The lock invariant ensures that there is a maximum time that a fund can be
|
||||
/// locked:
|
||||
///
|
||||
/// (∀ controller ∈ Controller, fund ∈ Fund:
|
||||
/// lock.expiry <= lock.maximum
|
||||
/// where lock = _locks[controller][fund])
|
||||
///
|
||||
/// 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 ∈ Fund, recipient ∈ Recipient:
|
||||
/// account.isSolventAt(lock.maximum)
|
||||
/// where account = _accounts[controller][fund][recipient]
|
||||
/// and lock = _locks[controller][fund])
|
||||
///
|
||||
/// The flow invariant ensures that incoming and outgoing flow rates match:
|
||||
///
|
||||
/// (∀ controller ∈ Controller, fund ∈ Fund:
|
||||
/// (∑ recipient ∈ Recipient: accounts[recipient].flow.incoming) =
|
||||
/// (∑ recipient ∈ Recipient: accounts[recipient].flow.outgoing)
|
||||
/// where accounts = _accounts[controller][fund])
|
||||
///
|
||||
abstract contract VaultBase {
|
||||
using SafeERC20 for IERC20;
|
||||
using Accounts for Account;
|
||||
@ -13,11 +43,16 @@ abstract contract VaultBase {
|
||||
|
||||
IERC20 internal immutable _token;
|
||||
|
||||
/// Represents a smart contract that can redistribute and burn tokens in funds
|
||||
type Controller is address;
|
||||
/// Unique identifier for a fund, chosen by the controller
|
||||
type Fund is bytes32;
|
||||
/// Receives the balance of an account when withdrawing
|
||||
type Recipient is address;
|
||||
|
||||
/// Each fund has its own time lock
|
||||
mapping(Controller => mapping(Fund => Lock)) private _locks;
|
||||
/// Each recipient has its own account in a fund
|
||||
mapping(Controller => mapping(Fund => mapping(Recipient => Account)))
|
||||
private _accounts;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user