SIP-366: Asynchronous Delegation

Author
StatusApproved
TypeGovernance
NetworkEthereum, Optimism, Base & Arbitrum
ImplementorTBD
ReleaseTBD
ProposalLoading status...
Created2024-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:

  1. unit256 undelegateCollateralDelay and its respective setter method setUndelegateCollateralDelay
  2. unit256 undelegateCollateralWindow and its respective setter method setUndelegateCollateralWindow
  3. unit256 delegateCollateralDelay and its respective setter method setDelegateCollateralDelay
  4. unit256 delegateCollateralWindow and its respective setter method setDelegateCollateralWindow

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 to DelegateCollateralIntent objects, is more effective. It avoids conflicts that would occur when DelegateCollateralIntent objects are updated, as discussed in the article here. Storing intent nonces in an array allows for far less complicated updates to the DelegateCollateralIntent 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:

  1. Ensure the caller is authorized to represent the account.
  2. Verify the account holds enough collateral to execute the intent.
  3. Check the validity of the collateral amount to be delegated, respecting the caches that track outstanding intents to delegate or undelegate collateral.
  4. Update the appropriate caches to reflect the collateral amount declared in the intent for the identified account and pool.
  5. Update the intentNoncesByAccount to include the new intent nonce associated with the account.
  6. Update the intentByNonce to represent the newly declared intent using the specific nonce.
  7. 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:

  1. Allow anyone to call it without requiring permissions, enabling the processing of any open intent.
  2. Verify the existence of the specified intent for the given account, pool, and type of collateral.
  3. Ensure the intent's submission timestamp is greater than the current timestamp plus the applicable delay.
  4. Ensure the intent's submission timestamp does not surpass the current timestamp plus both the delay and window period.
  5. Check that processing the intent won't cause the collateralization ratio to fall below the required issuance ratio.
  6. 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.
  7. Update the appropriate caches to reflect the collateral amount processed in the intent for the identified account and pool.
  8. Delete the intent nonce from intentNoncesByAccount for the involved account.
  9. Delete the intent from intentByNonce using the specific nonce.
  10. Update the amount of collateral delegated to the pool according to the intent.
  11. Emit a DelegateCollateralIntentProcessed event upon successful processing.
  12. 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:

  1. 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.
  2. Only process intents that are included in the intentNonces array.
  3. Remove intents with nonces included in the intentNonces array from the intentNoncesByAccount 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 and related rights waived via CC0.