markspanbroek e49abc4104
Vault (#220)
* 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>
2025-04-16 11:57:07 +02:00

111 lines
4.1 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "./TokenFlows.sol";
import "./Timestamps.sol";
/// Used to identify an account. The first 20 bytes consist of the address of
/// the account holder, and the last 12 bytes consist of a discriminator value.
type AccountId is bytes32;
/// 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;
}
library Accounts {
using Accounts for Account;
using TokenFlows for TokensPerSecond;
using Timestamps for Timestamp;
/// Creates an account id from the account holder address and a discriminator.
/// The discriminiator can be used to create different accounts that belong to
/// the same account holder.
function encodeId(
address holder,
bytes12 discriminator
) internal pure returns (AccountId) {
bytes32 left = bytes32(bytes20(holder));
bytes32 right = bytes32(uint256(uint96(discriminator)));
return AccountId.wrap(left | right);
}
/// Extracts the account holder and the discriminator from the the account id
function decodeId(AccountId id) internal pure returns (address, bytes12) {
bytes32 unwrapped = AccountId.unwrap(id);
address holder = address(bytes20(unwrapped));
bytes12 discriminator = bytes12(uint96(uint256(unwrapped)));
return (holder, discriminator);
}
/// 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
) internal pure returns (bool) {
Duration duration = account.flow.updated.until(timestamp);
uint128 outgoing = account.flow.outgoing.accumulate(duration);
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 accumulateFlows(
Account memory account,
Timestamp timestamp
) internal pure {
Duration duration = account.flow.updated.until(timestamp);
account.balance.available -= account.flow.outgoing.accumulate(duration);
account.balance.designated += account.flow.incoming.accumulate(duration);
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.accumulateFlows(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.accumulateFlows(Timestamps.currentTime());
if (rate <= account.flow.incoming) {
account.flow.incoming = account.flow.incoming - rate;
} else {
account.flow.outgoing = account.flow.outgoing + rate;
account.flow.outgoing = account.flow.outgoing - account.flow.incoming;
account.flow.incoming = TokensPerSecond.wrap(0);
}
}
}