diff --git a/content/sips/sip-366.md b/content/sips/sip-366.md index 2032beb94..8310f88d6 100644 --- a/content/sips/sip-366.md +++ b/content/sips/sip-366.md @@ -1,10 +1,10 @@ --- sip: 366 -title: Asyncronous Delegation -network: Ethereum, Optimism & Base +title: Asynchronous Delegation +network: Ethereum, Optimism, Base, Arbitrum status: Approved type: Governance -author: 'Afif (@aband1), Noah Litvin (@noahlitvin)' +author: 'Jared Borders (@jaredborders), kaleb (@kaleb-keny)' created: 2024-03-04 proposal: >- https://snapshot.org/#/snxgov.eth/proposal/0xfff673d535369488fed83333f397725ff36a0e45adcf9c2831e628f9e8bc4595 @@ -16,19 +16,19 @@ proposal: >- -This SIP proposes adding functionality such that markets can require liquidity providers to first request a change in their delegation of collateral and the process it after a configurable delay. +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](https://sips.synthetix.io/sips/sip-320/)), this SIP proposes to replace the concept of a minimum delegation time with a pattern in which liquidity providers call a `requestDelegateCollateral` function, wait a `delegateCollateralDelay` or `undelegateCollateralDelay` (depending on whether their delegated collateral is increasing or decreasing), and then have anyone call a `processDelegateCollateral` function within a `delegateCollateralWindow` or `undelegateCollateralWindow` to apply the change. +As an improvement over the Minimum Collateral Delegation Duration (per [SIP-320](https://sips.synthetix.io/sips/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 adversial dynamics between liquidity providers. For example, if a liquidity provider is able to anticipate a large increase or decrease of 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 a effectively a concern of "just-in-time liquidity". +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](https://sips.synthetix.io/sips/sip-316/)). @@ -36,16 +36,282 @@ Note that this functionality would be implemented in addition to the Core System -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 this 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. +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 -* Replace `setMarketMinDelegateTime` and `minDelegationTime` with relevant getter and setter methods for `unit256 undelegateCollateralDelay`, `unit256 undelegateCollateralWindow`, `unit256 delegateCollateralDelay`, and `unit256 delegateCollateralWindow`. -* `requestDelegateCollateral(uint128 accountId,uint128 poolId,address collateralType,uint256 newCollateralAmountD18,uint256 leverage)` should be added to the `VaultModule`. This should perform similar checks as `delegateCollateral`, but store the request (along with the `block.timestamp`) to a mapping in the `Vault` storage library. -* `processDelegateCollateral(uint128 accountId,uint128 poolId,address collateralType)` should be added to the `VaultModule`. This should confirm that the collateralization ratio of the position will not drop below the issuance ratio if the delegation change is processed. It can be called permissionlessly, such that anyone can process an open request. It should confirm that a corresponding request exists in storage, that the stored timestamp is greater than the current timestamp plus `delegateCollateralDelay` or `undelegateCollateralDelay`, and less than the current time plus the relevant delay value and the relevant window value. -* `delegateCollateral` can call `requestDelegateCollateral` or `processDelegateCollateral` as appropriate if collateral is being reduced and `delegateDelay` or `undelegateDelay` is set. +### `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: + +```solidity +/** + * 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: + +```solidity +mapping(uint128 accountId => uint256[] nonces) intentNoncesByAccount; +``` + +Add the following `intentByNonce` field to the `Data` `struct` to map an intent nonce to a `DelegateCollateralIntent` object: + +```solidity +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: + +```solidity +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: + +```solidity +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](https://medium.com/@0xFluffyBeard/migrating-the-eip-2535-diamond-storage-layout-cb28b9c47e71). 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: + +```solidity +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: + +```solidity +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): + +```solidity +/** + * @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): + +```solidity +/** + * @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. + +```solidity +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