mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-10 01:03:12 +00:00
vault: allow for multiple accounts for one account holder
This commit is contained in:
parent
b64c64aff8
commit
95a698c574
@ -11,8 +11,7 @@ import "./vault/VaultBase.sol";
|
||||
///
|
||||
/// 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.
|
||||
/// set of funds. Each fund has a number of accounts.
|
||||
///
|
||||
/// Vault -> Controller -> Fund -> Account
|
||||
///
|
||||
@ -21,6 +20,8 @@ import "./vault/VaultBase.sol";
|
||||
///
|
||||
/// An account has a balance, of which a part can be designated. Designated
|
||||
/// tokens can no longer be transfered to another account.
|
||||
/// 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
|
||||
@ -28,8 +29,8 @@ import "./vault/VaultBase.sol";
|
||||
/// 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
|
||||
/// 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:
|
||||
@ -40,34 +41,49 @@ import "./vault/VaultBase.sol";
|
||||
/// 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
|
||||
/// - 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) {}
|
||||
|
||||
/// The amount of tokens that are currently assigned to a recipient in a fund.
|
||||
/// 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 account
|
||||
) public pure returns (address holder, bytes12 discriminator) {
|
||||
return Accounts.decodeId(account);
|
||||
}
|
||||
|
||||
/// 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(
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
) public view returns (uint128) {
|
||||
function getBalance(Fund fund, AccountId id) public view returns (uint128) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
Balance memory balance = _getBalance(controller, fund, recipient);
|
||||
Balance memory balance = _getBalance(controller, fund, id);
|
||||
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.
|
||||
/// The amount of tokens that are currently designated in an account
|
||||
/// These tokens can no longer be transfered to other accounts.
|
||||
function getDesignatedBalance(
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
AccountId id
|
||||
) public view returns (uint128) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
Balance memory balance = _getBalance(controller, fund, recipient);
|
||||
Balance memory balance = _getBalance(controller, fund, id);
|
||||
return balance.designated;
|
||||
}
|
||||
|
||||
@ -107,45 +123,44 @@ contract Vault is VaultBase, Pausable, Ownable {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// of the account. ERC20 tokens are transfered from the caller to the vault
|
||||
/// contract.
|
||||
/// Only allowed when the fund is locked.
|
||||
function deposit(
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId id,
|
||||
uint128 amount
|
||||
) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_deposit(controller, fund, recipient, amount);
|
||||
_deposit(controller, fund, id, 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.
|
||||
/// 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(
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId id,
|
||||
uint128 amount
|
||||
) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_designate(controller, fund, recipient, amount);
|
||||
_designate(controller, fund, id, amount);
|
||||
}
|
||||
|
||||
/// Transfers an amount of tokens from the acount of one recipient to the
|
||||
/// other.
|
||||
/// Transfers an amount of tokens from one account to the other.
|
||||
/// Only allowed when the fund is locked.
|
||||
function transfer(
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
Recipient to,
|
||||
AccountId from,
|
||||
AccountId to,
|
||||
uint128 amount
|
||||
) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_transfer(controller, fund, from, to, amount);
|
||||
}
|
||||
|
||||
/// Transfers tokens from the account of one recipient to the other over time.
|
||||
/// 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.
|
||||
@ -154,31 +169,31 @@ contract Vault is VaultBase, Pausable, Ownable {
|
||||
/// fund unlocks, even if the lock expiry time is extended to its maximum.
|
||||
function flow(
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
Recipient to,
|
||||
AccountId from,
|
||||
AccountId to,
|
||||
TokensPerSecond rate
|
||||
) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_flow(controller, fund, from, to, rate);
|
||||
}
|
||||
|
||||
/// Burns an amount of designated tokens from the account of the recipient.
|
||||
/// Burns an amount of designated tokens from the account.
|
||||
/// Only allowed when the fund is locked.
|
||||
function burnDesignated(
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId account,
|
||||
uint128 amount
|
||||
) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_burnDesignated(controller, fund, recipient, amount);
|
||||
_burnDesignated(controller, fund, account, amount);
|
||||
}
|
||||
|
||||
/// Burns all tokens from the account of the recipient.
|
||||
/// 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(Fund fund, Recipient recipient) public whenNotPaused {
|
||||
function burnAccount(Fund fund, AccountId account) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_burnAccount(controller, fund, recipient);
|
||||
_burnAccount(controller, fund, account);
|
||||
}
|
||||
|
||||
/// Burns all tokens from all accounts in a fund.
|
||||
@ -188,23 +203,28 @@ contract Vault is VaultBase, Pausable, Ownable {
|
||||
_burnFund(controller, fund);
|
||||
}
|
||||
|
||||
/// Transfers all ERC20 tokens in the recipient's account out of the vault to
|
||||
/// the recipient address.
|
||||
/// Transfers all ERC20 tokens in the account out of the vault to the account
|
||||
/// owner.
|
||||
/// Only allowed when the fund is unlocked.
|
||||
/// ⚠️ The recipient can also withdraw itself, so when designing a smart
|
||||
/// ⚠️ 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(Fund fund, Recipient recipient) public whenNotPaused {
|
||||
function withdraw(Fund fund, AccountId account) public whenNotPaused {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
_withdraw(controller, fund, recipient);
|
||||
_withdraw(controller, fund, account);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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, Fund fund) public {
|
||||
Recipient recipient = Recipient.wrap(msg.sender);
|
||||
_withdraw(controller, fund, recipient);
|
||||
function withdrawByRecipient(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
AccountId account
|
||||
) public {
|
||||
(address holder, ) = Accounts.decodeId(account);
|
||||
_withdraw(controller, fund, account);
|
||||
}
|
||||
|
||||
function pause() public onlyOwner {
|
||||
|
||||
@ -4,6 +4,10 @@ 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;
|
||||
@ -37,6 +41,26 @@ library Accounts {
|
||||
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(
|
||||
|
||||
@ -24,17 +24,17 @@ import "./Locks.sol";
|
||||
/// 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:
|
||||
/// (∀ controller ∈ Controller, fund ∈ Fund, account ∈ AccountId:
|
||||
/// flow.outgoing * (lock.maximum - flow.updated) <= balance.available
|
||||
/// where lock = _locks[controller][fund])
|
||||
/// and flow = _accounts[controller][fund][recipient].flow
|
||||
/// and balance = _accounts[controller][fund][recipient].balance
|
||||
/// and flow = _accounts[controller][fund][account].flow
|
||||
/// and balance = _accounts[controller][fund][account].balance
|
||||
///
|
||||
/// 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)
|
||||
/// (∑ account ∈ AccountId: accounts[account].flow.incoming) =
|
||||
/// (∑ account ∈ AccountId: accounts[account].flow.outgoing)
|
||||
/// where accounts = _accounts[controller][fund])
|
||||
///
|
||||
abstract contract VaultBase {
|
||||
@ -48,13 +48,11 @@ abstract contract VaultBase {
|
||||
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)))
|
||||
/// Each account holder has its own set of accounts in a fund
|
||||
mapping(Controller => mapping(Fund => mapping(AccountId => Account)))
|
||||
private _accounts;
|
||||
|
||||
constructor(IERC20 token) {
|
||||
@ -78,17 +76,17 @@ abstract contract VaultBase {
|
||||
function _getBalance(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
AccountId id
|
||||
) internal view returns (Balance memory) {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
LockStatus lockStatus = lock.status();
|
||||
if (lockStatus == LockStatus.Locked) {
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
Account memory account = _accounts[controller][fund][id];
|
||||
account.update(Timestamps.currentTime());
|
||||
return account.balance;
|
||||
}
|
||||
if (lockStatus == LockStatus.Unlocked) {
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
Account memory account = _accounts[controller][fund][id];
|
||||
account.update(lock.expiry);
|
||||
return account.balance;
|
||||
}
|
||||
@ -125,13 +123,13 @@ abstract contract VaultBase {
|
||||
function _deposit(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId id,
|
||||
uint128 amount
|
||||
) internal {
|
||||
Lock storage lock = _locks[controller][fund];
|
||||
require(lock.status() == LockStatus.Locked, VaultFundNotLocked());
|
||||
|
||||
Account storage account = _accounts[controller][fund][recipient];
|
||||
Account storage account = _accounts[controller][fund][id];
|
||||
|
||||
account.balance.available += amount;
|
||||
lock.value += amount;
|
||||
@ -146,27 +144,27 @@ abstract contract VaultBase {
|
||||
function _designate(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId id,
|
||||
uint128 amount
|
||||
) internal {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
require(lock.status() == LockStatus.Locked, VaultFundNotLocked());
|
||||
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
Account memory account = _accounts[controller][fund][id];
|
||||
require(amount <= account.balance.available, VaultInsufficientBalance());
|
||||
|
||||
account.balance.available -= amount;
|
||||
account.balance.designated += amount;
|
||||
_checkAccountInvariant(account, lock);
|
||||
|
||||
_accounts[controller][fund][recipient] = account;
|
||||
_accounts[controller][fund][id] = account;
|
||||
}
|
||||
|
||||
function _transfer(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
Recipient to,
|
||||
AccountId from,
|
||||
AccountId to,
|
||||
uint128 amount
|
||||
) internal {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
@ -186,8 +184,8 @@ abstract contract VaultBase {
|
||||
function _flow(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient from,
|
||||
Recipient to,
|
||||
AccountId from,
|
||||
AccountId to,
|
||||
TokensPerSecond rate
|
||||
) internal {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
@ -206,13 +204,13 @@ abstract contract VaultBase {
|
||||
function _burnDesignated(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient,
|
||||
AccountId id,
|
||||
uint128 amount
|
||||
) internal {
|
||||
Lock storage lock = _locks[controller][fund];
|
||||
require(lock.status() == LockStatus.Locked, VaultFundNotLocked());
|
||||
|
||||
Account storage account = _accounts[controller][fund][recipient];
|
||||
Account storage account = _accounts[controller][fund][id];
|
||||
require(account.balance.designated >= amount, VaultInsufficientBalance());
|
||||
|
||||
account.balance.designated -= amount;
|
||||
@ -225,18 +223,18 @@ abstract contract VaultBase {
|
||||
function _burnAccount(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
AccountId id
|
||||
) internal {
|
||||
Lock storage lock = _locks[controller][fund];
|
||||
require(lock.status() == LockStatus.Locked, VaultFundNotLocked());
|
||||
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
Account memory account = _accounts[controller][fund][id];
|
||||
require(account.flow.incoming == account.flow.outgoing, VaultFlowNotZero());
|
||||
uint128 amount = account.balance.available + account.balance.designated;
|
||||
|
||||
lock.value -= amount;
|
||||
|
||||
delete _accounts[controller][fund][recipient];
|
||||
delete _accounts[controller][fund][id];
|
||||
|
||||
_token.safeTransfer(address(0xdead), amount);
|
||||
}
|
||||
@ -250,15 +248,11 @@ abstract contract VaultBase {
|
||||
_token.safeTransfer(address(0xdead), lock.value);
|
||||
}
|
||||
|
||||
function _withdraw(
|
||||
Controller controller,
|
||||
Fund fund,
|
||||
Recipient recipient
|
||||
) internal {
|
||||
function _withdraw(Controller controller, Fund fund, AccountId id) internal {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
require(lock.status() == LockStatus.Unlocked, VaultFundNotUnlocked());
|
||||
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
Account memory account = _accounts[controller][fund][id];
|
||||
account.update(lock.expiry);
|
||||
uint128 amount = account.balance.available + account.balance.designated;
|
||||
|
||||
@ -270,9 +264,10 @@ abstract contract VaultBase {
|
||||
_locks[controller][fund] = lock;
|
||||
}
|
||||
|
||||
delete _accounts[controller][fund][recipient];
|
||||
delete _accounts[controller][fund][id];
|
||||
|
||||
_token.safeTransfer(Recipient.unwrap(recipient), amount);
|
||||
(address owner, ) = Accounts.decodeId(id);
|
||||
_token.safeTransfer(owner, amount);
|
||||
}
|
||||
|
||||
function _checkLockInvariant(Lock memory lock) private pure {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user