mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-02 21:33:08 +00:00
* vault: deposit and withdraw * vault: change data structure to be recipient oriented * vault: burning funds * vault: transfer tokens from one recipient to the other * vault: designate tokens for a single recipient * vault: lock up tokens until expiry time * vault: lock is deleted upon withdrawal * vault: simplify test setup * vault: remove duplication in tests * vault: further test for locks * vault: allow recipient to withdraw * vault: flow tokens from one recipient to the other * vault: designate tokens that flow * vault: move flow accumulation calculation into VaultBase * vault: use custom operators to improve readability * vault: stop flowing when lock expires * vault: reject flow when insufficient tokens available * vault: do not allow flow when lock already expired * vault: allow automine to be disabled in time sensitive tests * vault: improve naming of public functions * vault: flow to multiple recipients - changes balance from uint256 -> uint128 so that entire Balance can be read or written with a single operation - moves Lock to library - simplifies lock checks * vault: reject negative flows * vault: make tests a bit more robust * vault: change flows over time * vault: check Lock invariant before writing * vault: allow flows to be diverted to others * vault: simplify example flow rates in test * vault: disallow transfer of flowing tokens * vault: cannot burn flowing tokens * vault: delete flow when burning or withdrawing * vault: fix flaky time sensitive tests Ensures that setting of lock and starting of flow happen in the same block. Therefore hardhat cannot occasionally increase the timestamp between the two operations. This makes predicting the balances over time much easier. * vault: disallow designating of flowing tokens * vault: document setAutomine() * vault: delete lock all tokens are withdrawn or burned * vault: cleanup * vault: reorder tests * vault: only allow deposit, transfer, etc when locked * vault: reorder functions in roughly chronological order * vault: rename context -> fund * vault: rename balance -> account * vault: combine account and flow mappings * vault: _getAccount updates to the latest timestamp * vault: simplify _getAccount() * vault: reordering * vault: formatting * vault: do not delete lock when burning * vault: combine Account and Flow structs * vault: cleanup * vault: split flow into incoming and outgoing - no need to deal with signed integers anymore - allows flow to self to designate tokens over time * vault: fix transfer to self * vault: remove _getAccount() - no longer calculate flow updates when not needed - use account.update(timestamp) where needed - use _getBalance() to view current balance * vault: rename error * vault: reduce size of timestamp further * vault: prevent approval hijacking - transfer ERC20 funds into the vault from the controller, not from the user - prevents an attacker from hijacking a user's ERC20 approval to move tokens into a part of the vault that is controlled by the attacker * vault: extract common tests for unlocked funds * vault: burn entire fund * vault: transfer tokens to 0xdead when fund is burned * vault: do not expose Lock internals on public api * vault: formatting * vault: test lock state transitions * vault: clean up errors * vault: rename burn -> burnAccount, burnAll -> burnFund * vault: burn part of designated tokens * vault: burn designated/fund allowed when flowing * vault: prefix errors with 'Vault' * vault: cleanup * vault: remove dead code * vault: add documentation * vault: fix accounting of locked value when burning designated tokens * vault: update documentation * update openzeppelin contracts to 5.2.0 * vault: format all solidity files * vault: cleanup tests * vault: pausing and unpausing * vault: rename account->holder in tests * vault: allow for multiple accounts for one account holder * vault: only allow account holder to withdraw for itself * vault: freezeFund() instead of burnFund() * vault: rename Fund -> FundId * vault: rename lock states - NoLock -> Inactive - Unlocked -> Withdrawing * vault: rename Lock -> Fund * vault: clarification Co-Authored-by: Adam Uhlíř <adam@uhlir.dev> * vault: rename update() -> accumulateFlows() Reason: update() is too generic, and can easily be interpreted as changing the on-chain state, whereas it actually updates the in-memory struct. Co-Authored-By: Eric <5089238+emizzle@users.noreply.github.com> Co-Authored-By: Adam Uhlíř <adam@uhlir.dev> * vault: rephrase Co-Authored-By: Adam Uhlíř <adam@uhlir.dev> --------- Co-authored-by: Adam Uhlíř <adam@uhlir.dev> Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com>
249 lines
9.5 KiB
Solidity
249 lines
9.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.28;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/utils/Pausable.sol";
|
|
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 accounts.
|
|
///
|
|
/// 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, although they can be
|
|
/// burned.
|
|
/// Accounts are identified by the address of the account holder, and an id that
|
|
/// can be used to create different accounts for the same holder.
|
|
///
|
|
/// 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 an account holder,
|
|
/// or the account holder 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 account holders
|
|
/// 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, Pausable, Ownable {
|
|
constructor(IERC20 token) VaultBase(token) Ownable(msg.sender) {}
|
|
|
|
/// Creates an account id that encodes the address of the account holder, and
|
|
/// a discriminator. The discriminator can be used to create different
|
|
/// accounts within a fund that all belong to the same account holder.
|
|
function encodeAccountId(
|
|
address holder,
|
|
bytes12 discriminator
|
|
) public pure returns (AccountId) {
|
|
return Accounts.encodeId(holder, discriminator);
|
|
}
|
|
|
|
/// Extracts the address of the account holder and the discriminator from the
|
|
/// account id.
|
|
function decodeAccountId(
|
|
AccountId id
|
|
) public pure returns (address holder, bytes12 discriminator) {
|
|
return Accounts.decodeId(id);
|
|
}
|
|
|
|
/// The amount of tokens that are currently in an account.
|
|
/// This includes available and designated tokens. Available tokens can be
|
|
/// transfered to other accounts, but designated tokens cannot.
|
|
function getBalance(
|
|
FundId fundId,
|
|
AccountId accountId
|
|
) public view returns (uint128) {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
Balance memory balance = _getBalance(controller, fundId, accountId);
|
|
return balance.available + balance.designated;
|
|
}
|
|
|
|
/// The amount of tokens that are currently designated in an account
|
|
/// These tokens can no longer be transfered to other accounts.
|
|
function getDesignatedBalance(
|
|
FundId fundId,
|
|
AccountId accountId
|
|
) public view returns (uint128) {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
Balance memory balance = _getBalance(controller, fundId, accountId);
|
|
return balance.designated;
|
|
}
|
|
|
|
/// 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 _getFundStatus(controller, fundId);
|
|
}
|
|
|
|
/// Returns the expiry time of the lock on the fund. A locked fund unlocks
|
|
/// automatically at this timestamp.
|
|
function getLockExpiry(FundId fundId) public view returns (Timestamp) {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
return _getLockExpiry(controller, fundId);
|
|
}
|
|
|
|
/// Locks the fund until the expiry timestamp. The lock expiry can be extended
|
|
/// later, but no more than the maximum timestamp.
|
|
function lock(
|
|
FundId fundId,
|
|
Timestamp expiry,
|
|
Timestamp maximum
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_lock(controller, fundId, 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.
|
|
/// Only allowed when the lock has not unlocked yet.
|
|
function extendLock(FundId fundId, Timestamp expiry) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_extendLock(controller, fundId, expiry);
|
|
}
|
|
|
|
/// Deposits an amount of tokens into the vault, and adds them to the balance
|
|
/// of the account. ERC20 tokens are transfered from the caller to the vault
|
|
/// contract.
|
|
/// Only allowed when the fund is locked.
|
|
function deposit(
|
|
FundId fundId,
|
|
AccountId accountId,
|
|
uint128 amount
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_deposit(controller, fundId, accountId, amount);
|
|
}
|
|
|
|
/// Takes an amount of tokens from the account balance and designates them
|
|
/// for the account holder. These tokens are no longer available to be
|
|
/// transfered to other accounts.
|
|
/// Only allowed when the fund is locked.
|
|
function designate(
|
|
FundId fundId,
|
|
AccountId accountId,
|
|
uint128 amount
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_designate(controller, fundId, accountId, amount);
|
|
}
|
|
|
|
/// Transfers an amount of tokens from one account to the other.
|
|
/// Only allowed when the fund is locked.
|
|
function transfer(
|
|
FundId fundId,
|
|
AccountId from,
|
|
AccountId to,
|
|
uint128 amount
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_transfer(controller, fundId, from, to, amount);
|
|
}
|
|
|
|
/// Transfers tokens from one account the other over time.
|
|
/// Every second a number of tokens are transfered, until the fund is
|
|
/// unlocked. After flowing into an account, these tokens become designated
|
|
/// tokens, so they cannot be transfered again.
|
|
/// 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(
|
|
FundId fundId,
|
|
AccountId from,
|
|
AccountId to,
|
|
TokensPerSecond rate
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_flow(controller, fundId, from, to, rate);
|
|
}
|
|
|
|
/// Burns an amount of designated tokens from the account.
|
|
/// Only allowed when the fund is locked.
|
|
function burnDesignated(
|
|
FundId fundId,
|
|
AccountId accountId,
|
|
uint128 amount
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_burnDesignated(controller, fundId, accountId, amount);
|
|
}
|
|
|
|
/// Burns all tokens from the account.
|
|
/// Only allowed when the fund is locked.
|
|
/// Only allowed when no funds are flowing into or out of the account.
|
|
function burnAccount(
|
|
FundId fundId,
|
|
AccountId accountId
|
|
) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_burnAccount(controller, fundId, accountId);
|
|
}
|
|
|
|
/// Freezes a fund. Stops all tokens flows and disallows any operations on the
|
|
/// fund until it unlocks.
|
|
/// Only allowed when the fund is locked.
|
|
function freezeFund(FundId fundId) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_freezeFund(controller, fundId);
|
|
}
|
|
|
|
/// Transfers all ERC20 tokens in the account out of the vault to the account
|
|
/// owner.
|
|
/// Only allowed when the fund is unlocked.
|
|
/// ⚠️ The account holder 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(FundId fund, AccountId accountId) public whenNotPaused {
|
|
Controller controller = Controller.wrap(msg.sender);
|
|
_withdraw(controller, fund, accountId);
|
|
}
|
|
|
|
/// Allows an account holder 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,
|
|
FundId fund,
|
|
AccountId accountId
|
|
) public {
|
|
(address holder, ) = Accounts.decodeId(accountId);
|
|
require(msg.sender == holder, VaultOnlyAccountHolder());
|
|
_withdraw(controller, fund, accountId);
|
|
}
|
|
|
|
function pause() public onlyOwner {
|
|
_pause();
|
|
}
|
|
|
|
function unpause() public onlyOwner {
|
|
_unpause();
|
|
}
|
|
|
|
error VaultOnlyAccountHolder();
|
|
}
|