Accounting Layer | Vault
At the core of this architecture is the Vault, which functions as an immutable accounting layer. It maintains a ledger of tokens that are either deposited or owed, facilitating a secure and efficient settlement process at the end of each transaction.
Lock Mechanism
One of the key mechaism in the vault is the lock mechanism. Caller need to get a lock from the vault before performing any operation (swap, liquidity or donate) with the pool manager.
The high level steps are as follow:
-
Acquire a lock from the vault via
vault.lock()
and the vault will perform a callback to the callerlockAcquired(data)
-
Within
lockAcquired(data)
, caller perform action such as swapclPoolManager.swap(...)
or liquidityclPoolManager.modifyLiquidity(..)
-
The poolManager will inform vault of the balance changes through
vault.accountPoolBalanceDelta(...)
w -
Caller will then need to reconcile the balance with the vault through either of the 4 methods:
take(), settle(), mint() or burn()
.
// An example of the 4 steps process, some codes are removed to keep this simple
contract ExamplePositionManager {
function addLiquidity(AddLiquidity calldata params) external {
// Step 1: Get a lock
vault.lock(..)
}
function lockAcquired(bytes calldata rawData) external {
// Step 2: Perform action with pool manager
CallbackData memory data = abi.decode(rawData, (CallbackData));
(BalanceDelta delta, ) = poolManager.mint(...)
// Step 4: Reconcile balance, an example of token0 below
if (delta.amount0() > 0) {
vault.take(poolKey.currency0, user, uint128(delta.amount0()));
} else {
// transfer token0 to the vault first then call vault.settle
vault.sync(poolKey.currency0);
pay(poolKey.currency0, user, address(vault), uint256(int256(-delta.amount0())));
vault.settle(poolKey.currency0);
}
}
}
Flash accounting
The vault uses SettlementGuard to keep track of the number of unsettled currency and how much is owed/owe for each currency.
Below shows a snippet of code in SettlementGuard.sol
. Whenever an operation happen, pool manager will call Vault.accountDelta to inform vault of the balance change
function accountDelta(address settler, Currency currency, int256 newlyAddedDelta) internal {
if (newlyAddedDelta == 0) return;
/// @dev update the count of non-zero deltas if necessary
int256 currentDelta = getCurrencyDelta(settler, currency);
int256 nextDelta = currentDelta + newlyAddedDelta;
unchecked {
if (nextDelta == 0) {
tstore(UNSETTLED_DELTAS_COUNT, sub(tload(UNSETTLED_DELTAS_COUNT), 1))
} else if (currentDelta == 0) {
tstore(UNSETTLED_DELTAS_COUNT, add(tload(UNSETTLED_DELTAS_COUNT), 1))
}
}
uint256 elementSlot = uint256(keccak256(abi.encode(settler, currency, CURRENCY_DELTA)));
tstore(elementSlot, nextDelta)
}
This technique is also known as flash accounting as ledger (currency owed/owe to vault) is tracked temporarily through transient storage and user only settle them at the end of all operations. An example where gas benefit shines is multi-hop transactions,
- User swap A -> B -> C
- In PCS v3, there will be ERC20 transfer of all three token A, B and C
- In PCS v4, as token B is netted off in the ledger by the end of the multi-hop operation, there will only be ERC20 transfer of token A and C.
Reconciling balance
BalanceDelta is returned whenever you perform some actions with poolManager, eg. swap | modifyLiquidity | donate
. It contains int128 amount0
and int128 amount1
that we need to take or settle with the vault.
Positive balance delta
In this case, vault owes the caller the amount of token.
Caller have 2 choices:
-
vault.take(Currency currency, address to, uint256 amount)
- this will transfer the owed token from vault to the address (specified inaddress to
). -
vault.mint(Currency currency, address to, uint256 amount)
- this will store the surplus token on the vault and credit it toaddress to
.
Negative balance delta
In this case, caller owes the vault the amount of token.
Caller have 2 choices:
-
Call
vault.sync(currency)
, transfer token to the vault and callvault.settle(Currency token)
-
Assuming caller have done
vault.mint
earlier to store surplus token on the vault, caller can callvault.burn(Currency currency, uint256 amount)
now to use those surplus token stored in vault.
Q1. Why are there 3 steps vault.sync(currency), transfer() token to vault and vault.settle() for settlement?
vault.sync()
inform vault to store the the vault's current currency balance in transient storage.- After which token transfer to the vault
vault.settle()
inform vault to compare the balance of token in vault now and (1) to know how many token are sent to the vault.
You may refer to this CurrencySettlement.sol library as a reference implementation on the 3 step settlement.
Q2. When to use take/mint/settle/burn
?
It really depends on your use case, for most use case, take/settle
is sufficient.
However if your developing a contract which does a lot of trades eg. arbitrage bot, you can consider mint/burn
to save on the gas cost by reducing the number of ERC-20 token transfer.