294 lines
8.8 KiB
Solidity
Raw Normal View History

2025-01-22 11:32:22 +01:00
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./Accounts.sol";
2025-03-03 13:20:58 +01:00
import "./Funds.sol";
2025-01-22 11:32:22 +01:00
2025-02-25 15:53:01 +01:00
/// Unique identifier for a fund, chosen by the controller
type FundId is bytes32;
2025-02-11 13:52:08 +01:00
/// 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:
///
2025-03-03 13:20:58 +01:00
/// (∀ controller ∈ Controller, fundId ∈ FundId:
/// fund.lockExpiry <= fund.lockMaximum
/// where fund = _funds[controller][fundId])
2025-02-11 13:52:08 +01:00
///
/// The account invariant ensures that the outgoing token flow can be sustained
/// for the maximum time that a fund can be locked:
///
2025-03-03 13:20:58 +01:00
/// (∀ 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
2025-02-11 13:52:08 +01:00
///
/// The flow invariant ensures that incoming and outgoing flow rates match:
///
2025-03-03 13:20:58 +01:00
/// (∀ controller ∈ Controller, fundId ∈ FundId:
/// (∑ accountId ∈ AccountId: accounts[accountId].flow.incoming) =
/// (∑ accountId ∈ AccountId: accounts[accountId].flow.outgoing)
/// where accounts = _accounts[controller][fundId])
2025-02-11 13:52:08 +01:00
///
2025-01-22 11:32:22 +01:00
abstract contract VaultBase {
2025-02-10 12:21:02 +01:00
using SafeERC20 for IERC20;
using Accounts for Account;
2025-03-03 13:20:58 +01:00
using Funds for Fund;
2025-02-10 12:21:02 +01:00
2025-01-22 11:32:22 +01:00
IERC20 internal immutable _token;
2025-02-11 13:52:08 +01:00
/// Represents a smart contract that can redistribute and burn tokens in funds
2025-01-22 11:32:22 +01:00
type Controller is address;
2025-03-03 13:20:58 +01:00
/// 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
2025-03-03 10:49:56 +01:00
mapping(Controller => mapping(FundId => mapping(AccountId => Account)))
2025-02-04 13:27:27 +01:00
private _accounts;
2025-01-22 11:32:22 +01:00
constructor(IERC20 token) {
_token = token;
}
2025-03-03 13:20:58 +01:00
function _getFundStatus(
2025-02-04 15:06:10 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId
2025-03-03 13:20:58 +01:00
) internal view returns (FundStatus) {
return _funds[controller][fundId].status();
}
function _getLockExpiry(
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId
) internal view returns (Timestamp) {
2025-03-03 13:20:58 +01:00
return _funds[controller][fundId].lockExpiry;
2025-02-04 15:06:10 +01:00
}
function _getBalance(
2025-01-22 11:32:22 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId accountId
) internal view returns (Balance memory) {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
FundStatus status = fund.status();
if (status == FundStatus.Locked) {
2025-03-03 10:49:56 +01:00
Account memory account = _accounts[controller][fundId][accountId];
account.update(Timestamps.currentTime());
2025-02-06 10:58:21 +01:00
return account.balance;
}
2025-03-03 13:20:58 +01:00
if (status == FundStatus.Withdrawing || status == FundStatus.Frozen) {
2025-03-03 10:49:56 +01:00
Account memory account = _accounts[controller][fundId][accountId];
2025-03-03 13:20:58 +01:00
account.update(fund.flowEnd());
2025-02-06 10:58:21 +01:00
return account.balance;
}
2025-02-06 10:58:21 +01:00
return Balance({available: 0, designated: 0});
}
function _lock(
2025-01-22 11:32:22 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
Timestamp expiry,
Timestamp maximum
2025-01-22 11:32:22 +01:00
) internal {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Inactive, VaultFundAlreadyLocked());
fund.lockExpiry = expiry;
fund.lockMaximum = maximum;
_checkLockInvariant(fund);
_funds[controller][fundId] = fund;
2025-01-22 11:32:22 +01:00
}
function _extendLock(
2025-01-22 11:32:22 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
Timestamp expiry
) internal {
2025-03-03 13:20:58 +01:00
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;
2025-01-22 11:32:22 +01:00
}
function _deposit(
2025-01-22 11:32:22 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId accountId,
uint128 amount
2025-01-22 11:32:22 +01:00
) internal {
2025-03-03 13:20:58 +01:00
Fund storage fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-03-03 10:49:56 +01:00
Account storage account = _accounts[controller][fundId][accountId];
account.balance.available += amount;
2025-03-03 13:20:58 +01:00
fund.value += amount;
_token.safeTransferFrom(
Controller.unwrap(controller),
address(this),
amount
);
2025-01-22 11:32:22 +01:00
}
function _designate(
2025-01-22 11:32:22 +01:00
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId accountId,
uint128 amount
2025-01-22 11:32:22 +01:00
) internal {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-03-03 10:49:56 +01:00
Account memory account = _accounts[controller][fundId][accountId];
2025-02-10 11:14:00 +01:00
require(amount <= account.balance.available, VaultInsufficientBalance());
account.balance.available -= amount;
account.balance.designated += amount;
2025-03-03 13:20:58 +01:00
_checkAccountInvariant(account, fund);
2025-01-28 16:08:11 +01:00
2025-03-03 10:49:56 +01:00
_accounts[controller][fundId][accountId] = account;
2025-01-22 11:32:22 +01:00
}
function _transfer(
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId from,
AccountId to,
uint128 amount
2025-01-22 11:32:22 +01:00
) internal {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-03-03 10:49:56 +01:00
Account memory sender = _accounts[controller][fundId][from];
2025-02-10 11:14:00 +01:00
require(amount <= sender.balance.available, VaultInsufficientBalance());
sender.balance.available -= amount;
2025-03-03 13:20:58 +01:00
_checkAccountInvariant(sender, fund);
2025-03-03 10:49:56 +01:00
_accounts[controller][fundId][from] = sender;
2025-02-05 14:04:46 +01:00
2025-03-03 10:49:56 +01:00
_accounts[controller][fundId][to].balance.available += amount;
2025-01-22 11:32:22 +01:00
}
function _flow(
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId from,
AccountId to,
TokensPerSecond rate
) internal {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-03-03 10:49:56 +01:00
Account memory sender = _accounts[controller][fundId][from];
sender.flowOut(rate);
2025-03-03 13:20:58 +01:00
_checkAccountInvariant(sender, fund);
2025-03-03 10:49:56 +01:00
_accounts[controller][fundId][from] = sender;
2025-03-03 10:49:56 +01:00
Account memory receiver = _accounts[controller][fundId][to];
receiver.flowIn(rate);
2025-03-03 10:49:56 +01:00
_accounts[controller][fundId][to] = receiver;
}
2025-02-10 10:57:29 +01:00
function _burnDesignated(
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId accountId,
2025-02-10 10:57:29 +01:00
uint128 amount
) internal {
2025-03-03 13:20:58 +01:00
Fund storage fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-02-10 10:57:29 +01:00
2025-03-03 10:49:56 +01:00
Account storage account = _accounts[controller][fundId][accountId];
2025-02-10 11:14:00 +01:00
require(account.balance.designated >= amount, VaultInsufficientBalance());
2025-02-10 10:57:29 +01:00
account.balance.designated -= amount;
2025-03-03 13:20:58 +01:00
fund.value -= amount;
2025-02-10 10:57:29 +01:00
_token.safeTransfer(address(0xdead), amount);
}
function _burnAccount(
Controller controller,
2025-03-03 10:49:56 +01:00
FundId fundId,
AccountId accountId
) internal {
2025-03-03 13:20:58 +01:00
Fund storage fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-03-03 10:49:56 +01:00
Account memory account = _accounts[controller][fundId][accountId];
2025-02-10 11:14:00 +01:00
require(account.flow.incoming == account.flow.outgoing, VaultFlowNotZero());
uint128 amount = account.balance.available + account.balance.designated;
2025-03-03 13:20:58 +01:00
fund.value -= amount;
2025-03-03 10:49:56 +01:00
delete _accounts[controller][fundId][accountId];
_token.safeTransfer(address(0xdead), amount);
}
2025-03-03 10:49:56 +01:00
function _freezeFund(Controller controller, FundId fundId) internal {
2025-03-03 13:20:58 +01:00
Fund storage fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Locked, VaultFundNotLocked());
2025-02-06 10:58:21 +01:00
2025-03-03 13:20:58 +01:00
fund.frozenAt = Timestamps.currentTime();
2025-02-06 10:58:21 +01:00
}
2025-03-03 10:49:56 +01:00
function _withdraw(
Controller controller,
FundId fundId,
AccountId accountId
) internal {
2025-03-03 13:20:58 +01:00
Fund memory fund = _funds[controller][fundId];
require(fund.status() == FundStatus.Withdrawing, VaultFundNotUnlocked());
2025-03-03 10:49:56 +01:00
Account memory account = _accounts[controller][fundId][accountId];
2025-03-03 13:20:58 +01:00
account.update(fund.flowEnd());
uint128 amount = account.balance.available + account.balance.designated;
2025-03-03 13:20:58 +01:00
fund.value -= amount;
2025-03-03 13:20:58 +01:00
if (fund.value == 0) {
delete _funds[controller][fundId];
} else {
2025-03-03 13:20:58 +01:00
_funds[controller][fundId] = fund;
}
2025-03-03 10:49:56 +01:00
delete _accounts[controller][fundId][accountId];
2025-02-04 09:53:31 +01:00
2025-03-03 10:49:56 +01:00
(address owner, ) = Accounts.decodeId(accountId);
_token.safeTransfer(owner, amount);
}
2025-03-03 13:20:58 +01:00
function _checkLockInvariant(Fund memory fund) private pure {
require(fund.lockExpiry <= fund.lockMaximum, VaultInvalidExpiry());
}
function _checkAccountInvariant(
2025-02-04 13:27:27 +01:00
Account memory account,
2025-03-03 13:20:58 +01:00
Fund memory fund
2025-01-28 14:58:14 +01:00
) private pure {
2025-03-03 13:20:58 +01:00
require(account.isSolventAt(fund.lockMaximum), VaultInsufficientBalance());
2025-01-28 14:58:14 +01:00
}
2025-02-10 11:14:00 +01:00
error VaultInsufficientBalance();
error VaultInvalidExpiry();
error VaultFundNotLocked();
error VaultFundNotUnlocked();
error VaultFundAlreadyLocked();
error VaultFlowNotZero();
2025-01-22 11:32:22 +01:00
}