mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-05 06:43:07 +00:00
vault: split flow into incoming and outgoing
- no need to deal with signed integers anymore - allows flow to self to designate tokens over time
This commit is contained in:
parent
297ec7f6b3
commit
450e5308d9
@ -13,7 +13,7 @@ contract Vault is VaultBase {
|
||||
) public view returns (uint128) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
Account memory account = _getAccount(controller, fund, recipient);
|
||||
return account.available + account.designated;
|
||||
return account.balance.available + account.balance.designated;
|
||||
}
|
||||
|
||||
function getDesignatedBalance(
|
||||
@ -22,7 +22,7 @@ contract Vault is VaultBase {
|
||||
) public view returns (uint128) {
|
||||
Controller controller = Controller.wrap(msg.sender);
|
||||
Account memory account = _getAccount(controller, fund, recipient);
|
||||
return account.designated;
|
||||
return account.balance.designated;
|
||||
}
|
||||
|
||||
function getLock(Fund fund) public view returns (Lock memory) {
|
||||
|
||||
@ -1,52 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "./TokensPerSecond.sol";
|
||||
import "./TokenFlows.sol";
|
||||
import "./Timestamps.sol";
|
||||
|
||||
struct Account {
|
||||
Balance balance;
|
||||
Flow flow;
|
||||
}
|
||||
|
||||
struct Balance {
|
||||
uint128 available;
|
||||
uint128 designated;
|
||||
TokensPerSecond flow;
|
||||
Timestamp flowUpdated;
|
||||
}
|
||||
|
||||
struct Flow {
|
||||
TokensPerSecond outgoing;
|
||||
TokensPerSecond incoming;
|
||||
Timestamp updated;
|
||||
}
|
||||
|
||||
library Accounts {
|
||||
function isValidAt(
|
||||
using Accounts for Account;
|
||||
using TokenFlows for TokensPerSecond;
|
||||
using Timestamps for Timestamp;
|
||||
|
||||
function isSolventAt(
|
||||
Account memory account,
|
||||
Timestamp timestamp
|
||||
) internal pure returns (bool) {
|
||||
if (account.flow < TokensPerSecond.wrap(0)) {
|
||||
return uint128(-accumulateFlow(account, timestamp)) <= account.available;
|
||||
Duration duration = account.flow.updated.until(timestamp);
|
||||
uint128 outgoing = account.flow.outgoing.accumulate(duration);
|
||||
return outgoing <= account.balance.available;
|
||||
}
|
||||
|
||||
function update(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;
|
||||
}
|
||||
|
||||
function flowIn(Account memory account, TokensPerSecond rate) internal view {
|
||||
account.update(Timestamps.currentTime());
|
||||
account.flow.incoming = account.flow.incoming + rate;
|
||||
}
|
||||
|
||||
function flowOut(Account memory account, TokensPerSecond rate) internal view {
|
||||
account.update(Timestamps.currentTime());
|
||||
if (rate <= account.flow.incoming) {
|
||||
account.flow.incoming = account.flow.incoming - rate;
|
||||
} else {
|
||||
return true;
|
||||
account.flow.outgoing = account.flow.outgoing + rate;
|
||||
account.flow.outgoing = account.flow.outgoing - account.flow.incoming;
|
||||
account.flow.incoming = TokensPerSecond.wrap(0);
|
||||
}
|
||||
}
|
||||
|
||||
function at(
|
||||
Account memory account,
|
||||
Timestamp timestamp
|
||||
) internal pure returns (Account memory) {
|
||||
Account memory result = account;
|
||||
if (result.flow != TokensPerSecond.wrap(0)) {
|
||||
int128 accumulated = accumulateFlow(result, timestamp);
|
||||
if (accumulated >= 0) {
|
||||
result.designated += uint128(accumulated);
|
||||
} else {
|
||||
result.available -= uint128(-accumulated);
|
||||
}
|
||||
}
|
||||
result.flowUpdated = timestamp;
|
||||
return result;
|
||||
}
|
||||
|
||||
function accumulateFlow(
|
||||
Account memory account,
|
||||
Timestamp timestamp
|
||||
) private pure returns (int128) {
|
||||
int128 rate = TokensPerSecond.unwrap(account.flow);
|
||||
Timestamp start = account.flowUpdated;
|
||||
Timestamp end = timestamp;
|
||||
uint64 duration = Timestamp.unwrap(end) - Timestamp.unwrap(start);
|
||||
return rate * int128(uint128(duration));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
type Timestamp is uint64;
|
||||
type Duration is uint64;
|
||||
|
||||
using {_timestampEquals as ==} for Timestamp global;
|
||||
using {_timestampNotEqual as !=} for Timestamp global;
|
||||
@ -39,4 +40,11 @@ library Timestamps {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
function until(
|
||||
Timestamp start,
|
||||
Timestamp end
|
||||
) internal pure returns (Duration) {
|
||||
return Duration.wrap(Timestamp.unwrap(end) - Timestamp.unwrap(start));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,22 +3,16 @@ pragma solidity 0.8.28;
|
||||
|
||||
import "./Timestamps.sol";
|
||||
|
||||
type TokensPerSecond is int128;
|
||||
type TokensPerSecond is uint96;
|
||||
|
||||
using {_tokensPerSecondNegate as -} for TokensPerSecond global;
|
||||
using {_tokensPerSecondMinus as -} for TokensPerSecond global;
|
||||
using {_tokensPerSecondPlus as +} for TokensPerSecond global;
|
||||
using {_tokensPerSecondEquals as ==} for TokensPerSecond global;
|
||||
using {_tokensPerSecondNotEqual as !=} for TokensPerSecond global;
|
||||
using {_tokensPerSecondAtLeast as >=} for TokensPerSecond global;
|
||||
using {_tokensPerSecondAtMost as <=} for TokensPerSecond global;
|
||||
using {_tokensPerSecondLessThan as <} for TokensPerSecond global;
|
||||
|
||||
function _tokensPerSecondNegate(
|
||||
TokensPerSecond rate
|
||||
) pure returns (TokensPerSecond) {
|
||||
return TokensPerSecond.wrap(-TokensPerSecond.unwrap(rate));
|
||||
}
|
||||
|
||||
function _tokensPerSecondMinus(
|
||||
TokensPerSecond a,
|
||||
TokensPerSecond b
|
||||
@ -56,9 +50,25 @@ function _tokensPerSecondAtLeast(
|
||||
return TokensPerSecond.unwrap(a) >= TokensPerSecond.unwrap(b);
|
||||
}
|
||||
|
||||
function _tokensPerSecondAtMost(
|
||||
TokensPerSecond a,
|
||||
TokensPerSecond b
|
||||
) pure returns (bool) {
|
||||
return TokensPerSecond.unwrap(a) <= TokensPerSecond.unwrap(b);
|
||||
}
|
||||
|
||||
function _tokensPerSecondLessThan(
|
||||
TokensPerSecond a,
|
||||
TokensPerSecond b
|
||||
) pure returns (bool) {
|
||||
return TokensPerSecond.unwrap(a) < TokensPerSecond.unwrap(b);
|
||||
}
|
||||
|
||||
library TokenFlows {
|
||||
function accumulate(
|
||||
TokensPerSecond rate,
|
||||
Duration duration
|
||||
) internal pure returns (uint128) {
|
||||
return uint128(TokensPerSecond.unwrap(rate)) * Duration.unwrap(duration);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "./Accounts.sol";
|
||||
import "./Timestamps.sol";
|
||||
import "./TokensPerSecond.sol";
|
||||
import "./TokenFlows.sol";
|
||||
import "./Locks.sol";
|
||||
|
||||
using SafeERC20 for IERC20;
|
||||
@ -46,7 +46,8 @@ abstract contract VaultBase {
|
||||
Timestamps.currentTime(),
|
||||
lock.expiry
|
||||
);
|
||||
return account.at(timestamp);
|
||||
account.update(timestamp);
|
||||
return account;
|
||||
}
|
||||
|
||||
function _lock(
|
||||
@ -88,7 +89,7 @@ abstract contract VaultBase {
|
||||
Recipient recipient = Recipient.wrap(from);
|
||||
Account storage account = _accounts[controller][fund][recipient];
|
||||
|
||||
account.available += amount;
|
||||
account.balance.available += amount;
|
||||
lock.value += amount;
|
||||
|
||||
_token.safeTransferFrom(from, address(this), amount);
|
||||
@ -105,9 +106,9 @@ abstract contract VaultBase {
|
||||
|
||||
Account memory account = _accounts[controller][fund][recipient];
|
||||
|
||||
require(amount <= account.available, InsufficientBalance());
|
||||
account.available -= amount;
|
||||
account.designated += amount;
|
||||
require(amount <= account.balance.available, InsufficientBalance());
|
||||
account.balance.available -= amount;
|
||||
account.balance.designated += amount;
|
||||
|
||||
_checkAccountInvariant(account, lock);
|
||||
|
||||
@ -124,17 +125,17 @@ abstract contract VaultBase {
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
require(lock.isLocked(), LockRequired());
|
||||
|
||||
Account memory senderAccount = _getAccount(controller, fund, from);
|
||||
Account memory receiverAccount = _getAccount(controller, fund, to);
|
||||
Account memory sender = _getAccount(controller, fund, from);
|
||||
Account memory receiver = _getAccount(controller, fund, to);
|
||||
|
||||
require(amount <= senderAccount.available, InsufficientBalance());
|
||||
senderAccount.available -= amount;
|
||||
receiverAccount.available += amount;
|
||||
require(amount <= sender.balance.available, InsufficientBalance());
|
||||
sender.balance.available -= amount;
|
||||
receiver.balance.available += amount;
|
||||
|
||||
_checkAccountInvariant(senderAccount, lock);
|
||||
_checkAccountInvariant(sender, lock);
|
||||
|
||||
_accounts[controller][fund][from] = senderAccount;
|
||||
_accounts[controller][fund][to] = receiverAccount;
|
||||
_accounts[controller][fund][from] = sender;
|
||||
_accounts[controller][fund][to] = receiver;
|
||||
}
|
||||
|
||||
function _flow(
|
||||
@ -144,21 +145,17 @@ abstract contract VaultBase {
|
||||
Recipient to,
|
||||
TokensPerSecond rate
|
||||
) internal {
|
||||
require(rate >= TokensPerSecond.wrap(0), NegativeFlow());
|
||||
|
||||
Lock memory lock = _locks[controller][fund];
|
||||
require(lock.isLocked(), LockRequired());
|
||||
|
||||
Account memory senderAccount = _getAccount(controller, fund, from);
|
||||
Account memory receiverAccount = _getAccount(controller, fund, to);
|
||||
Account memory sender = _accounts[controller][fund][from];
|
||||
sender.flowOut(rate);
|
||||
_checkAccountInvariant(sender, lock);
|
||||
_accounts[controller][fund][from] = sender;
|
||||
|
||||
senderAccount.flow = senderAccount.flow - rate;
|
||||
receiverAccount.flow = receiverAccount.flow + rate;
|
||||
|
||||
_checkAccountInvariant(senderAccount, lock);
|
||||
|
||||
_accounts[controller][fund][from] = senderAccount;
|
||||
_accounts[controller][fund][to] = receiverAccount;
|
||||
Account memory receiver = _accounts[controller][fund][to];
|
||||
receiver.flowIn(rate);
|
||||
_accounts[controller][fund][to] = receiver;
|
||||
}
|
||||
|
||||
function _burn(
|
||||
@ -170,9 +167,12 @@ abstract contract VaultBase {
|
||||
require(lock.isLocked(), LockRequired());
|
||||
|
||||
Account memory account = _getAccount(controller, fund, recipient);
|
||||
require(account.flow == TokensPerSecond.wrap(0), CannotBurnFlowingTokens());
|
||||
require(
|
||||
account.flow.incoming == account.flow.outgoing,
|
||||
CannotBurnFlowingTokens()
|
||||
);
|
||||
|
||||
uint128 amount = account.available + account.designated;
|
||||
uint128 amount = account.balance.available + account.balance.designated;
|
||||
|
||||
lock.value -= amount;
|
||||
|
||||
@ -190,7 +190,7 @@ abstract contract VaultBase {
|
||||
require(!lock.isLocked(), Locked());
|
||||
|
||||
Account memory account = _getAccount(controller, fund, recipient);
|
||||
uint128 amount = account.available + account.designated;
|
||||
uint128 amount = account.balance.available + account.balance.designated;
|
||||
|
||||
lock.value -= amount;
|
||||
|
||||
@ -213,7 +213,7 @@ abstract contract VaultBase {
|
||||
Account memory account,
|
||||
Lock memory lock
|
||||
) private pure {
|
||||
require(account.isValidAt(lock.maximum), InsufficientBalance());
|
||||
require(account.isSolventAt(lock.maximum), InsufficientBalance());
|
||||
}
|
||||
|
||||
error InsufficientBalance();
|
||||
@ -222,6 +222,5 @@ abstract contract VaultBase {
|
||||
error ExpiryPastMaximum();
|
||||
error InvalidExpiry();
|
||||
error LockRequired();
|
||||
error NegativeFlow();
|
||||
error CannotBurnFlowingTokens();
|
||||
}
|
||||
|
||||
@ -420,6 +420,15 @@ describe("Vault", function () {
|
||||
expect(await vault.getDesignatedBalance(fund, address2)).to.equal(21)
|
||||
})
|
||||
|
||||
it("designates tokens that flow back to the sender", async function () {
|
||||
await vault.flow(fund, address1, address1, 3)
|
||||
await mine()
|
||||
const start = await currentTime()
|
||||
await advanceTimeTo(start + 7)
|
||||
expect(await vault.getBalance(fund, address1)).to.equal(deposit)
|
||||
expect(await vault.getDesignatedBalance(fund, address1)).to.equal(21)
|
||||
})
|
||||
|
||||
it("flows longer when lock is extended", async function () {
|
||||
await vault.flow(fund, address1, address2, 2)
|
||||
await mine()
|
||||
@ -435,13 +444,6 @@ describe("Vault", function () {
|
||||
expect(await getBalance(address2)).to.equal(total)
|
||||
})
|
||||
|
||||
it("rejects negative flows", async function () {
|
||||
setAutomine(true)
|
||||
await expect(
|
||||
vault.flow(fund, address1, address2, -1)
|
||||
).to.be.revertedWith("NegativeFlow")
|
||||
})
|
||||
|
||||
it("rejects flow when insufficient available tokens", async function () {
|
||||
setAutomine(true)
|
||||
await expect(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user