SIP-366: Asynchronous Delegation
Author | |
---|---|
Status | Approved |
Type | Governance |
Network | Ethereum, Optimism, Base & Arbitrum |
Implementor | TBD |
Release | TBD |
Proposal | Loading status... |
Created | 2024-03-04 |
Simple Summary
This SIP proposes adding functionality such that markets can require liquidity providers to first declare an intent to change the amount of collateral delegated and then process it after a configurable delay.
Abstract
As an improvement over the Minimum Collateral Delegation Duration (per SIP-320), this SIP proposes to replace the concept of a minimum delegation time with a pattern in which liquidity providers call a declareIntentToDelegateCollateral
function, wait a delegateCollateralDelay
or undelegateCollateralDelay
(depending on whether their delegated collateral is increasing or decreasing), and then have anyone call a processIntentToDelegateCollateral
function within a delegateCollateralWindow
or undelegateCollateralWindow
to apply the change.
Motivation
This functionality allows markets to prevent adversarial dynamics between liquidity providers. For example, if a liquidity provider anticipates a large increase or decrease in debt to a pool (without this feature enabled), they could quickly exit or enter the pool such that other liquidity providers assume the change. This is effectively a concern of "just-in-time liquidity."
Note that this functionality would be implemented in addition to the Core System Precautionary Security Features (per SIP-316).
Rationale
It would be possible to have governance or pool owners configure these values rather than having market builders decide on the configuration, but this would complicate liquidity provisioning for all integrators, as it makes delegation non-atomic. Further, because the opportunities of adversarial liquidity provisioning are specific to a market's implementations, it makes sense to configure these values in this way.
Technical Specification
MarketManagerModule
& IMarketManagerModule
Configuration
Replace logic and state associated with the minimum delegation time by omitting minDelegationTime
and its respective setter method (setMarketMinDelegateTime
) with the following logic and state associated with the delegation delay and window functionality:
unit256 undelegateCollateralDelay
and its respective setter methodsetUndelegateCollateralDelay
unit256 undelegateCollateralWindow
and its respective setter methodsetUndelegateCollateralWindow
unit256 delegateCollateralDelay
and its respective setter methodsetDelegateCollateralDelay
unit256 delegateCollateralWindow
and its respective setter methodsetDelegateCollateralWindow
All of these values should be configurable only by the market.
Vault
& IVault
Intents
Define the following object (i.e., struct
) that defines all aspects of an intent to delegate or undelegate collateral. This object should closely resemble the following DelegateCollateralIntent
object:
/**
* Intent Lifecycle:
*
* |<---- Delay ---->|<-- Processing Window -->|
* Time ----|-----------------|-------------------------|---->
* ^ ^ ^
* | | |
* declarationTime processingStartTime processingEndTime
*
* Key:
* - declarationTime: Timestamp at which the intent is declared.
* - processingStartTime: Timestamp from which the intent can start being processed.
* - processingEndTime: Timestamp after which the intent cannot be processed.
*
* The intent can be processed only between processingStartTime and processingEndTime.
*/
struct DelegateCollateralIntent {
/**
* @notice An incrementing nonce to ensure the
* uniqueness of the intent and prevent replay attacks
*/
uint256 nonce;
/**
* @notice The ID of the pool for which the account has an
* outstanding intent to delegate a new amount of collateral to
*/
uint128 poolId;
/**
* @notice The address of the collateral type that the account has an
* outstanding intent to delegate a new amount of
*/
address collateralType;
/**
* @notice The amount of collateral that the account has an
* outstanding intent to delegate a new amount of to the pool,
* denominated with 18 decimals of precision
*/
uint256 collateralAmountD18;
/**
* @notice The intended amount of leverage associated with the new
* amount of collateral that the account has an outstanding intent
* to delegate to the pool
* @dev The system currently only supports 1x leverage
*/
uint256 leverage;
/**
* @notice The timestamp at which the intent was declared
*/
uint256 declarationTime;
/**
* @notice The timestamp before which the intent cannot be processed
* @dev dependent on
* {VaultModule.undelegateCollateralDelay} or
* {VaultModule.delegateCollateralDelay}
*/
uint256 processingStartTime;
/**
* @notice The timestamp after which the intent cannot be processed
* (i.e., intent expiry)
* @dev dependent on
* {VaultModule.undelegateCollateralWindow} or
* {VaultModule.delegateCollateralWindow}
*/
uint256 processingEndTime;
}
Storage
Add the following intentNoncesByAccount
field to the Data
struct
to store an array of all outstanding intent nonces for a given account in a mapping:
mapping(uint128 accountId => uint256[] nonces) intentNoncesByAccount;
Add the following intentByNonce
field to the Data
struct
to map an intent nonce to a DelegateCollateralIntent
object:
mapping(uint256 nonce => DelegateCollateralIntent intent) intentByNonce;
Add the following delegateCollateralCachePerAccountPerPool
and delegateCollateralCachePerAccount
fields to the Data
struct
to store the amount of collateral that is currently declared to be delegated for a given account due to all outstanding DelegateCollateralIntent
intents:
mapping(uint128 accountId => mapping(uint128 poolId => uint256 amount) amountPerPool) delegateCollateralCachePerAccountPerPool;
mapping(uint128 accountId => uint256 amount) delegateCollateralCachePerAccount;
Add the following undelegateCollateralCachePerAccountPerPool
and undelegateCollateralCachePerAccount
fields to the Data
struct
to store the amount of collateral that is currently declared to be undelegated for a given account due to all outstanding DelegateCollateralIntent
intents:
mapping(uint128 accountId => mapping(uint128 poolId => uint256 amount) amountPerPool) undelegateCollateralCachePerAccountPerPool;
mapping(uint128 accountId => uint256 amount) undelegateCollateralCachePerAccount;
The proposed approach of using several fields to store granular intent details per account, rather than a single field mapping of an
accountId
toDelegateCollateralIntent
objects, is more effective. It avoids conflicts that would occur whenDelegateCollateralIntent
objects are updated, as discussed in the article here. Storing intent nonces in an array allows for far less complicated updates to theDelegateCollateralIntent
object. Moreover, detailed accounting is necessary to avoid errors caused by deliberately malicious declarations of intent.
Nonce Management
Implement a rudimentary nonce management system to ensure the uniqueness of each intent declaration. This system should track the current nonce that will be assigned to the next declared intent for a given account. The nonce should be incremented after each declaration. The system should also allow for the invalidation of nonces that can be initiated by the account owner or by the system in the event of account liquidation.
VaultModule
& IVaultModule
Events
Add the following event to emit when an intent to delegate a new amount of collateral is successfully declared:
DelegateCollateralIntentDeclared(
uint128 accountId,
uint256 nonce,
uint128 poolId,
address collateralType,
uint256 collateralAmountD18,
uint256 leverage
);
Add the following event to emit when an intent to delegate a new amount of collateral is successfully processed:
DelegateCollateralIntentProcessed(
uint128 accountId,
uint256 nonce,
uint128 poolId,
address collateralType,
uint256 collateralAmountD18,
uint256 leverage
);
Submitting Intents
Add the following function to declare and record an intent to delegate a new amount of collateral (refer to the NatSpec annotations for further implementation guidance):
/**
* @notice Declare an intent to delegate a new amount of collateral
* @dev The {msg.sender} is not necessarily the account owner but it
* must be authorized to act on behalf of the account
* @param accountId The ID of the account intending to delegate a new
* amount of collateral
* @param poolId The ID of the pool to which the account intends to
* delegate a new amount of collateral
* @param collateralType The address of the collateral type that the
* account intends to delegate
* @param newCollateralAmountD18 The new amount of collateral that
* the account intends to delegate, denominated with 18 decimals of
* precision
* @param leverage The new intended leverage used in the position
*/
function declareIntentToDelegateCollateral(
uint128 accountId,
uint128 poolId,
address collateralType,
uint256 newCollateralAmountD18,
uint256 leverage
) public;
The declareIntentToDelegateCollateral
function should, at minimum, satisfy the following requirements:
- Ensure the caller is authorized to represent the account.
- Verify the account holds enough collateral to execute the intent.
- Check the validity of the collateral amount to be delegated, respecting the caches that track outstanding intents to delegate or undelegate collateral.
- Update the appropriate caches to reflect the collateral amount declared in the intent for the identified account and pool.
- Update the
intentNoncesByAccount
to include the new intent nonce associated with the account. - Update the
intentByNonce
to represent the newly declared intent using the specific nonce. - Emit a
DelegateCollateralIntentDeclared
event.
Processing Intents
Add the following functions to process outstanding intent(s) to delegate a new amount of collateral (refer to the NatSpec annotations for further implementation guidance):
/**
* @notice Attempt to process an outstanding intent to delegate a
* new amount of collateral
* @dev The {msg.sender} can be any address
* @param accountId The ID of the account for which the outstanding
* intent to delegate a new amount of collateral is being processed
* @param poolId The ID of the pool for which the intent of the account
* to delegate a new amount of collateral is being processed
* @param collateralType The address of the collateral type that the
* account has an outstanding intent to delegate a new amount of
*/
function processIntentToDelegateCollateral(
uint128 accountId,
uint128 poolId,
address collateralType
) public;
The processIntentToDelegateCollateral
function should, at minimum, satisfy the following requirements:
- Allow anyone to call it without requiring permissions, enabling the processing of any open intent.
- Verify the existence of the specified intent for the given account, pool, and type of collateral.
- Ensure the intent's submission timestamp is greater than the current timestamp plus the applicable delay.
- Ensure the intent's submission timestamp does not surpass the current timestamp plus both the delay and window period.
- Check that processing the intent won't cause the collateralization ratio to fall below the required issuance ratio.
- Validate that the collateral amount to be processed is within the bounds defined by the appropriate caches that track outstanding intents to delegate or undelegate collateral.
- Update the appropriate caches to reflect the collateral amount processed in the intent for the identified account and pool.
- Delete the intent nonce from
intentNoncesByAccount
for the involved account. - Delete the intent from
intentByNonce
using the specific nonce. - Update the amount of collateral delegated to the pool according to the intent.
- Emit a
DelegateCollateralIntentProcessed
event upon successful processing. - Execute steps 2 to 11 for every intent ready for processing for the mentioned account, pool, and collateral type.
The order by which the intents are processed should not matter so long as the cache mappings are properly maintained.
function processIntentToDelegateCollateral(
uint128 accountId,
uint256[] intentNonces
) public;
The overloaded processIntentToDelegateCollateral
function should, at minimum, satisfy the following requirements:
- Follow the same requirements (steps 1 to 11) originally outlined for the
processIntentToDelegateCollateral
function that processes specific intents queued for an account, pool, and collateral type. - Only process intents that are included in the
intentNonces
array. - Remove intents with nonces included in the
intentNonces
array from theintentNoncesByAccount
mapping if they are expired or processed.
Edge Cases
If an attempt is made to process an intent but constraints such as (but not limited to) issuance ratio restrictions, debt fluctuations, or partial account liquidations prevent the full amount of collateral from being processed, then the maximum permissible amount of collateral should be determined and processed as allowed.
Delegating Collateral Entry Point
The delegateCollateral
function is designed to invoke requestDelegateCollateral
and/or processDelegateCollateral
as needed, ensuring compatibility with the pre-existing delegateCollateral
function. Initially, it aims to declare an intent to delegate a new amount of collateral based on the provided parameters. Subsequently, it seeks to process any existing intents linked to the specified account, pool, and type of collateral.
If an account has a ready-to-process intent but does not wish to declare a new one, it should directly use processDelegateCollateral
.
Conversely, if an account has no pending intents but wishes to declare a new one, requestDelegateCollateral
should be utilized directly.
LiquidationModule
& ILiquidationModule
Account Liquidation
In the event an account is liquidated, all outstanding intents to delegate collateral should be canceled/forcibly expired. This can be achieved by batch invalidating all outstanding intent nonces associated with the account within the liquidate
function.
CollateralModule
& ICollateralModule
Withdrawal Restrictions
The CollateralModule
should be updated to ensure that any collateral balance intended to be delegated is not available for withdrawal until the intent is processed (i.e. executed or expired). This can be achieved by adding a check to the withdraw
function to ensure that the account has sufficient collateral available to withdraw after accounting for any outstanding intents to delegate collateral.
Available Collateral
The CollateralModule
should also be updated to ensure that the available collateral balance for an account is calculated correctly, accounting for any outstanding intents to delegate collateral. This can be achieved by adding logic to the getAccountAvailableCollateral
function to deduct the amount of collateral declared in any outstanding intents to delegate collateral. This information can be obtained from the delegateCollateralCachePerAccount
mapping.
Test Cases
Relevant tests will be developed during implementation.
Configurable Values (Via SCCP)
N/A
Copyright
Copyright and related rights waived via CC0.