From 2209136eaee0232664cb480b0a13cbed6f41aca1 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 8 May 2024 14:42:54 -0300 Subject: [PATCH 01/50] wip sip366 --- .../interfaces/IMarketManagerModule.sol | 89 +++- .../contracts/interfaces/IVaultModule.sol | 58 ++- .../synthetix/contracts/mocks/MockMarket.sol | 25 +- .../modules/core/MarketManagerModule.sol | 102 ++++- .../contracts/modules/core/PoolModule.sol | 4 +- .../contracts/modules/core/VaultModule.sol | 391 +++++++++++++++--- .../storage/AccountDelegationIntents.sol | 121 ++++++ .../contracts/storage/DelegationIntent.sol | 124 ++++++ .../synthetix/contracts/storage/Market.sol | 11 +- protocol/synthetix/contracts/storage/Pool.sol | 98 +++-- .../contracts/storage/VaultEpoch.sol | 7 +- 11 files changed, 891 insertions(+), 139 deletions(-) create mode 100644 protocol/synthetix/contracts/storage/AccountDelegationIntents.sol create mode 100644 protocol/synthetix/contracts/storage/DelegationIntent.sol diff --git a/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol b/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol index a69f633699..2c015ce282 100644 --- a/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol +++ b/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol @@ -73,11 +73,35 @@ interface IMarketManagerModule { event MarketSystemFeePaid(uint128 indexed marketId, uint256 feeAmount); /** - * @notice Emitted when a market sets an updated minimum delegation time + * @notice Emitted when a market sets an updated undelegate collateral delay * @param marketId The id of the market that the setting is applied to - * @param minDelegateTime The minimum amount of time between delegation changes + * @param undelegateCollateralDelay The minimum amount of time to undelegate collateral */ - event SetMinDelegateTime(uint128 indexed marketId, uint32 minDelegateTime); + event SetUndelegateCollateralDelay(uint128 indexed marketId, uint32 undelegateCollateralDelay); + + /** + * @notice Emitted when a market sets an updated undelegate collateral window + * @param marketId The id of the market that the setting is applied to + * @param undelegateCollateralWindow The maximum window of time to undelegate collateral + */ + event SetUndelegateCollateralWindow( + uint128 indexed marketId, + uint32 undelegateCollateralWindow + ); + + /** + * @notice Emitted when a market sets an updated delegate collateral delay + * @param marketId The id of the market that the setting is applied to + * @param delegateCollateralDelay The minimum amount of time to delegate collateral + */ + event SetDelegateCollateralDelay(uint128 indexed marketId, uint32 delegateCollateralDelay); + + /** + * @notice Emitted when a market sets an updated delegate collateral window + * @param marketId The id of the market that the setting is applied to + * @param delegateCollateralWindow The maximum window of time to delegate collateral + */ + event SetDelegateCollateralWindow(uint128 indexed marketId, uint32 delegateCollateralWindow); /** * @notice Emitted when a market-specific minimum liquidity ratio is set @@ -221,18 +245,65 @@ interface IMarketManagerModule { ) external returns (bool finishedDistributing); /** - * @notice allows for a market to set its minimum delegation time. This is useful for preventing stakers from frontrunning rewards or losses - * by limiting the frequency of `delegateCollateral` (or `setPoolConfiguration`) calls. By default, there is no minimum delegation time. - * @param marketId the id of the market that wants to set delegation time. - * @param minDelegateTime the minimum number of seconds between delegation calls. Note: this value must be less than the globally defined maximum minDelegateTime + * @notice allows for a market to set its un-delegation delay time. (See SIP-366). By default, there is no delay for undelegation. + * @param marketId the id of the market that wants to set un-delegation delay time. + * @param undelegateCollateralDelay the minimum number of delay seconds to un-delegation + */ + function setUndelegateCollateralDelay( + uint128 marketId, + uint32 undelegateCollateralDelay + ) external; + + /** + * @notice Retrieve the un-delegation delay time of a market + * @param marketId the id of the market + */ + function getUndelegateCollateralDelay(uint128 marketId) external view returns (uint32); + + /** + * @notice allows for a market to set its un-delegation window time. (See SIP-366). By default, (or if it's set to zero) there no window limit for undelegation. + * @param marketId the id of the market that wants to set un-delegation window time. + * @param undelegateCollateralWindow the maximum number of seconds that an undelegation can be executed after the delay. + */ + function setUndelegateCollateralWindow( + uint128 marketId, + uint32 undelegateCollateralWindow + ) external; + + /** + * @notice Retrieve the un-delegation window of a market + * @param marketId the id of the market */ - function setMarketMinDelegateTime(uint128 marketId, uint32 minDelegateTime) external; + function getUndelegateCollateralWindow(uint128 marketId) external view returns (uint32); + + /** + * @notice allows for a market to set its delegation delay time. (See SIP-366). By default, there is no delay for undelegation. + * @param marketId the id of the market that wants to set delegation delay time. + * @param delegateCollateralDelay the minimum number of delay seconds to delegation + */ + function setDelegateCollateralDelay(uint128 marketId, uint32 delegateCollateralDelay) external; /** * @notice Retrieve the minimum delegation time of a market * @param marketId the id of the market */ - function getMarketMinDelegateTime(uint128 marketId) external view returns (uint32); + function getDelegateCollateralDelay(uint128 marketId) external view returns (uint32); + + /** + * @notice allows for a market to set its delegation window time. (See SIP-366). By default, (or if it's set to zero) there no window limit for delegation. + * @param marketId the id of the market that wants to set delegation window time. + * @param delegateCollateralWindow the maximum number of seconds that an delegation can be executed after the delay. + */ + function setDelegateCollateralWindow( + uint128 marketId, + uint32 delegateCollateralWindow + ) external; + + /** + * @notice Retrieve the delegation window of a market + * @param marketId the id of the market + */ + function getDelegateCollateralWindow(uint128 marketId) external view returns (uint32); /** * @notice Allows the system owner (not the pool owner) to set a market-specific minimum liquidity ratio. diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index e5f73cc775..2f92678f33 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -22,6 +22,11 @@ interface IVaultModule { */ error InvalidCollateralAmount(); + /** + * @notice Thrown when the specified intent is not related to the account id. + */ + error InvalidDelegationIntent(); + /** * @notice Emitted when {sender} updates the delegation of collateral in the specified liquidity position. * @param accountId The id of the account whose position was updated. @@ -40,9 +45,33 @@ interface IVaultModule { address indexed sender ); + // /** + // * @notice Updates an account's delegated collateral amount for the specified pool and collateral type pair. + // * @param accountId The id of the account associated with the position that will be updated. + // * @param poolId The id of the pool associated with the position. + // * @param collateralType The address of the collateral used in the position. + // * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. + // * @param leverage The new leverage amount used in the position, denominated with 18 decimals of precision. + // * + // * Requirements: + // * + // * - `ERC2771Context._msgSender()` must be the owner of the account, have the `ADMIN` permission, or have the `DELEGATE` permission. + // * - If increasing the amount delegated, it must not exceed the available collateral (`getAccountAvailableCollateral`) associated with the account. + // * - If decreasing the amount delegated, the liquidity position must have a collateralization ratio greater than the target collateralization ratio for the corresponding collateral type. + // * + // * Emits a {DelegationUpdated} event. + // */ + // function delegateCollateral( + // uint128 accountId, + // uint128 poolId, + // address collateralType, + // uint256 amount, + // uint256 leverage + // ) external; + /** - * @notice Updates an account's delegated collateral amount for the specified pool and collateral type pair. - * @param accountId The id of the account associated with the position that will be updated. + * @notice Declare an intent to update the delegated amount for the specified pool and collateral type pair. + * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param poolId The id of the pool associated with the position. * @param collateralType The address of the collateral used in the position. * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. @@ -56,7 +85,7 @@ interface IVaultModule { * * Emits a {DelegationUpdated} event. */ - function delegateCollateral( + function declareIntentToDelegateCollateral( uint128 accountId, uint128 poolId, address collateralType, @@ -64,6 +93,29 @@ interface IVaultModule { uint256 leverage ) external; + /** + * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by intent ids. + * @param accountId The id of the account associated with the position that intends to update the collateral amount. + * @param intentIds An array of intents to attempt to process. + * Requirements: + * + * Emits a {DelegationUpdated} event. + */ + function processIntentToDelegateCollateralByIntents( + uint128 accountId, + uint256[] calldata intentIds + ) external; + + /** + * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by pool/accountID pair. + * @param accountId The id of the account associated with the position that intends to update the collateral amount. + * @param poolId The ID of the pool for which the intent of the account to delegate a new amount of collateral is being processed + * Requirements: + * + * Emits a {DelegationUpdated} event. + */ + function processIntentToDelegateCollateralByPair(uint128 accountId, uint128 poolId) external; + /** * @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0. * @dev Call this function using `callStatic` to treat it as a view function. diff --git a/protocol/synthetix/contracts/mocks/MockMarket.sol b/protocol/synthetix/contracts/mocks/MockMarket.sol index 063e5a76bc..cb07608d0f 100644 --- a/protocol/synthetix/contracts/mocks/MockMarket.sol +++ b/protocol/synthetix/contracts/mocks/MockMarket.sol @@ -102,8 +102,29 @@ contract MockMarket is IMarket { _price = newPrice; } - function setMinDelegationTime(uint32 minDelegationTime) external { - IMarketManagerModule(_proxy).setMarketMinDelegateTime(_marketId, minDelegationTime); + function setUndelegateCollateralDelay(uint32 undelegateCollateralDelay) external { + IMarketManagerModule(_proxy).setUndelegateCollateralDelay( + _marketId, + undelegateCollateralDelay + ); + } + + function setUndelegateCollateralWindow(uint32 undelegateCollateralWindow) external { + IMarketManagerModule(_proxy).setUndelegateCollateralWindow( + _marketId, + undelegateCollateralWindow + ); + } + + function setDelegateCollateralDelay(uint32 delegateCollateralDelay) external { + IMarketManagerModule(_proxy).setDelegateCollateralDelay(_marketId, delegateCollateralDelay); + } + + function setDelegateCollateralWindow(uint32 delegateCollateralWindow) external { + IMarketManagerModule(_proxy).setDelegateCollateralWindow( + _marketId, + delegateCollateralWindow + ); } function price() external view returns (uint256) { diff --git a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol index f7733648c1..d16c7f5279 100644 --- a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol +++ b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol @@ -43,7 +43,6 @@ contract MarketManagerModule is IMarketManagerModule { bytes32 private constant _DEPOSIT_MARKET_FEATURE_FLAG = "depositMarketUsd"; bytes32 private constant _WITHDRAW_MARKET_FEATURE_FLAG = "withdrawMarketUsd"; - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_RATIO = "depositMarketUsd_feeRatio"; bytes32 private constant _CONFIG_WITHDRAW_MARKET_USD_FEE_RATIO = "withdrawMarketUsd_feeRatio"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_ADDRESS = "depositMarketUsd_feeAddress"; @@ -329,38 +328,101 @@ contract MarketManagerModule is IMarketManagerModule { /** * @inheritdoc IMarketManagerModule */ - function setMarketMinDelegateTime(uint128 marketId, uint32 minDelegateTime) external override { + function setUndelegateCollateralDelay( + uint128 marketId, + uint32 undelegateCollateralDelay + ) external override { Market.Data storage market = Market.load(marketId); if (ERC2771Context._msgSender() != market.marketAddress) revert AccessError.Unauthorized(ERC2771Context._msgSender()); - // min delegate time should not be unreasonably long - uint256 maxMinDelegateTime = Config.readUint( - _CONFIG_SET_MARKET_MIN_DELEGATE_MAX, - 86400 * 30 - ); + market.undelegateCollateralDelay = undelegateCollateralDelay; - if (minDelegateTime > maxMinDelegateTime) { - revert ParameterError.InvalidParameter("minDelegateTime", "must not be too large"); - } + emit SetUndelegateCollateralDelay(marketId, undelegateCollateralDelay); + } + + /** + * @inheritdoc IMarketManagerModule + */ + function getUndelegateCollateralDelay( + uint128 marketId + ) external view override returns (uint32) { + return Market.load(marketId).undelegateCollateralDelay; + } + + /** + * @inheritdoc IMarketManagerModule + */ + function setUndelegateCollateralWindow( + uint128 marketId, + uint32 undelegateCollateralWindow + ) external override { + Market.Data storage market = Market.load(marketId); - market.minDelegateTime = minDelegateTime; + if (ERC2771Context._msgSender() != market.marketAddress) + revert AccessError.Unauthorized(ERC2771Context._msgSender()); - emit SetMinDelegateTime(marketId, minDelegateTime); + market.undelegateCollateralWindow = undelegateCollateralWindow; + + emit SetUndelegateCollateralWindow(marketId, undelegateCollateralWindow); } /** * @inheritdoc IMarketManagerModule */ - function getMarketMinDelegateTime(uint128 marketId) external view override returns (uint32) { - // solhint-disable-next-line numcast/safe-cast - uint32 maxMinDelegateTime = uint32( - Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) - ); - uint32 marketMinDelegateTime = Market.load(marketId).minDelegateTime; - return - maxMinDelegateTime < marketMinDelegateTime ? maxMinDelegateTime : marketMinDelegateTime; + function getUndelegateCollateralWindow( + uint128 marketId + ) external view override returns (uint32) { + return Market.load(marketId).undelegateCollateralWindow; + } + + /** + * @inheritdoc IMarketManagerModule + */ + function setDelegateCollateralDelay( + uint128 marketId, + uint32 delegateCollateralDelay + ) external override { + Market.Data storage market = Market.load(marketId); + + if (ERC2771Context._msgSender() != market.marketAddress) + revert AccessError.Unauthorized(ERC2771Context._msgSender()); + + market.delegateCollateralDelay = delegateCollateralDelay; + + emit SetDelegateCollateralDelay(marketId, delegateCollateralDelay); + } + + /** + * @inheritdoc IMarketManagerModule + */ + function getDelegateCollateralDelay(uint128 marketId) external view override returns (uint32) { + return Market.load(marketId).delegateCollateralDelay; + } + + /** + * @inheritdoc IMarketManagerModule + */ + function setDelegateCollateralWindow( + uint128 marketId, + uint32 delegateCollateralWindow + ) external override { + Market.Data storage market = Market.load(marketId); + + if (ERC2771Context._msgSender() != market.marketAddress) + revert AccessError.Unauthorized(ERC2771Context._msgSender()); + + market.delegateCollateralWindow = delegateCollateralWindow; + + emit SetUndelegateCollateralWindow(marketId, delegateCollateralWindow); + } + + /** + * @inheritdoc IMarketManagerModule + */ + function getDelegateCollateralWindow(uint128 marketId) external view override returns (uint32) { + return Market.load(marketId).delegateCollateralWindow; } /** diff --git a/protocol/synthetix/contracts/modules/core/PoolModule.sol b/protocol/synthetix/contracts/modules/core/PoolModule.sol index f032b17f34..032b618403 100644 --- a/protocol/synthetix/contracts/modules/core/PoolModule.sol +++ b/protocol/synthetix/contracts/modules/core/PoolModule.sol @@ -148,7 +148,9 @@ contract PoolModule is IPoolModule { ) external override { Pool.Data storage pool = Pool.loadExisting(poolId); Pool.onlyPoolOwner(poolId, ERC2771Context._msgSender()); - pool.requireMinDelegationTimeElapsed(pool.lastConfigurationTime); + // TODO LJM + + // pool.requireMinDelegationTimeElapsed(pool.lastConfigurationTime); // Update each market's pro-rata liquidity and collect accumulated debt into the pool's debt distribution. // Note: This follows the same pattern as Pool.recalculateVaultCollateral(), diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index eaab10bd2f..89d2f30902 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -7,6 +7,7 @@ import "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; import "../../storage/Account.sol"; import "../../storage/Pool.sol"; +import "../../storage/AccountDelegationIntents.sol"; import "@synthetixio/core-modules/contracts/storage/FeatureFlag.sol"; @@ -34,13 +35,138 @@ contract VaultModule is IVaultModule { using SafeCastU256 for uint256; using SafeCastI128 for int128; using SafeCastI256 for int256; + using AccountDelegationIntents for AccountDelegationIntents.Data; + using DelegationIntent for DelegationIntent.Data; bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; + // function delegateCollateral( + // uint128 accountId, + // uint128 poolId, + // address collateralType, + // uint256 newCollateralAmountD18, + // uint256 leverage + // ) external override { + // FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); + // Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + + // // Each collateral type may specify a minimum collateral amount that can be delegated. + // // See CollateralConfiguration.minDelegationD18. + // if (newCollateralAmountD18 > 0) { + // CollateralConfiguration.requireSufficientDelegation( + // collateralType, + // newCollateralAmountD18 + // ); + // } + + // // System only supports leverage of 1.0 for now. + // if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); + + // // Identify the vault that corresponds to this collateral type and pool id. + // Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; + + // // Use account interaction to update its rewards. + // uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; + // uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( + // accountId.toBytes32() + // ); + + // uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + + // // Conditions for collateral amount + + // // Ensure current collateral amount differs from the new collateral amount. + // if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); + // // If increasing delegated collateral amount, + // // Check that the account has sufficient collateral. + // else if (newCollateralAmountD18 > currentCollateralAmount) { + // // Check if the collateral is enabled here because we still want to allow reducing delegation for disabled collaterals. + // CollateralConfiguration.collateralEnabled(collateralType); + + // Account.requireSufficientCollateral( + // accountId, + // collateralType, + // newCollateralAmountD18 - currentCollateralAmount + // ); + + // Pool.loadExisting(poolId).checkPoolCollateralLimit( + // collateralType, + // newCollateralAmountD18 - currentCollateralAmount + // ); + + // // if decreasing delegation amount, ensure min time has elapsed + // } else { + // // TODO LJM + // // Pool.loadExisting(poolId).requireMinDelegationTimeElapsed( + // // vault.currentEpoch().lastDelegationTime[accountId] + // // ); + // } + + // // Update the account's position for the given pool and collateral type, + // // Note: This will trigger an update in the entire debt distribution chain. + // uint256 collateralPrice = _updatePosition( + // accountId, + // poolId, + // collateralType, + // newCollateralAmountD18, + // currentCollateralAmount, + // leverage + // ); + + // _updateAccountCollateralPools( + // accountId, + // poolId, + // collateralType, + // newCollateralAmountD18 > 0 + // ); + + // // If decreasing the delegated collateral amount, + // // check the account's collateralization ratio. + // // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. + // if (newCollateralAmountD18 < currentCollateralAmount) { + // int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; + + // uint256 minIssuanceRatioD18 = Pool + // .loadExisting(poolId) + // .collateralConfigurations[collateralType] + // .issuanceRatioD18; + + // // Minimum collateralization ratios are configured in the system per collateral type.abi + // // Ensure that the account's updated position satisfies this requirement. + // CollateralConfiguration.load(collateralType).verifyIssuanceRatio( + // debt < 0 ? 0 : debt.toUint(), + // newCollateralAmountD18.mulDecimal(collateralPrice), + // minIssuanceRatioD18 + // ); + + // // Accounts cannot reduce collateral if any of the pool's + // // connected market has its capacity locked. + // _verifyNotCapacityLocked(poolId); + // } + + // // solhint-disable-next-line numcast/safe-cast + // vault.currentEpoch().lastDelegationTime[accountId] = uint64(block.timestamp); + + // emit DelegationUpdated( + // accountId, + // poolId, + // collateralType, + // newCollateralAmountD18, + // leverage, + // ERC2771Context._msgSender() + // ); + + // vault.updateRewards( + // Vault.PositionSelector(accountId, poolId, collateralType), + // totalSharesD18, + // actorSharesD18 + // ); + // } + /** * @inheritdoc IVaultModule */ - function delegateCollateral( + function declareIntentToDelegateCollateral( uint128 accountId, uint128 poolId, address collateralType, @@ -66,15 +192,14 @@ contract VaultModule is IVaultModule { Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; // Use account interaction to update its rewards. - uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; - uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( - accountId.toBytes32() - ); + // TODO LJM Do we need to call this in the intent or just at execution? + // uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; + // uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( + // accountId.toBytes32() + // ); uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); - // Conditions for collateral amount - // Ensure current collateral amount differs from the new collateral amount. if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); // If increasing delegated collateral amount, @@ -93,72 +218,103 @@ contract VaultModule is IVaultModule { collateralType, newCollateralAmountD18 - currentCollateralAmount ); - - // if decreasing delegation amount, ensure min time has elapsed - } else { - Pool.loadExisting(poolId).requireMinDelegationTimeElapsed( - vault.currentEpoch().lastDelegationTime[accountId] - ); } + // TODO LJM more checks from original code + // // If decreasing the delegated collateral amount, + // // check the account's collateralization ratio. + // // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. + // if (newCollateralAmountD18 < currentCollateralAmount) { + // int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; + + // uint256 minIssuanceRatioD18 = Pool + // .loadExisting(poolId) + // .collateralConfigurations[collateralType] + // .issuanceRatioD18; + + // // Minimum collateralization ratios are configured in the system per collateral type.abi + // // Ensure that the account's updated position satisfies this requirement. + // CollateralConfiguration.load(collateralType).verifyIssuanceRatio( + // debt < 0 ? 0 : debt.toUint(), + // newCollateralAmountD18.mulDecimal(collateralPrice), + // minIssuanceRatioD18 + // ); + + // // Accounts cannot reduce collateral if any of the pool's + // // connected market has its capacity locked. + // _verifyNotCapacityLocked(poolId); + // } + + // Prepare data for storing the new intent. + int256 collateralDeltaAmountD18 = newCollateralAmountD18 > currentCollateralAmount + ? (newCollateralAmountD18 - currentCollateralAmount).toInt() + : (currentCollateralAmount - newCollateralAmountD18).toInt(); + + (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool + .loadExisting(poolId) + .getRequiredDelegationDelayAndWindow(collateralDeltaAmountD18 > 0); + + // Create a new delegation intent. + uint256 intentId = DelegationIntent.nextId(); + DelegationIntent.Data storage intent = DelegationIntent.load(intentId); + intent.id = intentId; + intent.accountId = accountId; + intent.poolId = poolId; + intent.collateralType = collateralType; + intent.collateralDeltaAmountD18 = collateralDeltaAmountD18; + intent.leverage = leverage; + intent.declarationTime = block.timestamp.to32(); + intent.processingStartTime = intent.declarationTime + requiredDelayTime; + intent.processingEndTime = intent.processingStartTime + requiredWindowTime; + + // Add intent to the account's delegation intents. + AccountDelegationIntents.loadValid(accountId).addIntent(intent); + + // TODO LJM emit an event + } - // Update the account's position for the given pool and collateral type, - // Note: This will trigger an update in the entire debt distribution chain. - uint256 collateralPrice = _updatePosition( - accountId, - poolId, - collateralType, - newCollateralAmountD18, - currentCollateralAmount, - leverage - ); - - _updateAccountCollateralPools( - accountId, - poolId, - collateralType, - newCollateralAmountD18 > 0 - ); + /** + * @inheritdoc IVaultModule + */ + function processIntentToDelegateCollateralByIntents( + uint128 accountId, + uint256[] memory intentIds + ) public override { + FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - // If decreasing the delegated collateral amount, - // check the account's collateralization ratio. - // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. - if (newCollateralAmountD18 < currentCollateralAmount) { - int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; + // Ensure the intent is valid. + if (intent.accountId != accountId) revert InvalidDelegationIntent(); - uint256 minIssuanceRatioD18 = Pool - .loadExisting(poolId) - .collateralConfigurations[collateralType] - .issuanceRatioD18; + // Ensure the intent is within the processing window. + intent.checkIsExecutable(); - // Minimum collateralization ratios are configured in the system per collateral type.abi - // Ensure that the account's updated position satisfies this requirement. - CollateralConfiguration.load(collateralType).verifyIssuanceRatio( - debt < 0 ? 0 : debt.toUint(), - newCollateralAmountD18.mulDecimal(collateralPrice), - minIssuanceRatioD18 + // Process the intent. + _delegateCollateral( + accountId, + intent.poolId, + intent.collateralType, + intent.collateralDeltaAmountD18, + intent.leverage ); - // Accounts cannot reduce collateral if any of the pool's - // connected market has its capacity locked. - _verifyNotCapacityLocked(poolId); - } + // Remove the intent. + AccountDelegationIntents.loadValid(accountId).removeIntent(intent); - // solhint-disable-next-line numcast/safe-cast - vault.currentEpoch().lastDelegationTime[accountId] = uint64(block.timestamp); + // TODO LJM emit an event + } + } - emit DelegationUpdated( + /** + * @inheritdoc IVaultModule + */ + function processIntentToDelegateCollateralByPair( + uint128 accountId, + uint128 poolId + ) external override { + processIntentToDelegateCollateralByIntents( accountId, - poolId, - collateralType, - newCollateralAmountD18, - leverage, - ERC2771Context._msgSender() - ); - - vault.updateRewards( - Vault.PositionSelector(accountId, poolId, collateralType), - totalSharesD18, - actorSharesD18 + AccountDelegationIntents.loadValid(accountId).intentIdsByPair(poolId, accountId) ); } @@ -252,6 +408,115 @@ contract VaultModule is IVaultModule { return Pool.loadExisting(poolId).currentVaultDebt(collateralType); } + function _delegateCollateral( + uint128 accountId, + uint128 poolId, + address collateralType, + int256 deltaCollateralAmountD18, + uint256 leverage + ) internal { + // Identify the vault that corresponds to this collateral type and pool id. + Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; + + // Use account interaction to update its rewards. + uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; + uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( + accountId.toBytes32() + ); + + uint256 newCollateralAmountD18 = deltaCollateralAmountD18 > 0 + ? vault.currentAccountCollateral(accountId) + deltaCollateralAmountD18.toUint() + : vault.currentAccountCollateral(accountId) - deltaCollateralAmountD18.toUint(); + + // Each collateral type may specify a minimum collateral amount that can be delegated. + // See CollateralConfiguration.minDelegationD18. + if (newCollateralAmountD18 > 0) { + CollateralConfiguration.requireSufficientDelegation( + collateralType, + newCollateralAmountD18 + ); + } + + uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + + // Conditions for collateral amount + + // If increasing delegated collateral amount, + // Check that the account has sufficient collateral. + if (deltaCollateralAmountD18 > 0) { + // Check if the collateral is enabled here because we still want to allow reducing delegation for disabled collaterals. + CollateralConfiguration.collateralEnabled(collateralType); + + Account.requireSufficientCollateral( + accountId, + collateralType, + deltaCollateralAmountD18.toUint() + ); + + Pool.loadExisting(poolId).checkPoolCollateralLimit( + collateralType, + deltaCollateralAmountD18.toUint() + ); + } + + // Update the account's position for the given pool and collateral type, + // Note: This will trigger an update in the entire debt distribution chain. + uint256 collateralPrice = _updatePosition( + accountId, + poolId, + collateralType, + newCollateralAmountD18, + currentCollateralAmount, + leverage + ); + + _updateAccountCollateralPools( + accountId, + poolId, + collateralType, + newCollateralAmountD18 > 0 + ); + + // If decreasing the delegated collateral amount, + // check the account's collateralization ratio. + // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. + if (deltaCollateralAmountD18 < 0) { + int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; + + uint256 minIssuanceRatioD18 = Pool + .loadExisting(poolId) + .collateralConfigurations[collateralType] + .issuanceRatioD18; + + // Minimum collateralization ratios are configured in the system per collateral type.abi + // Ensure that the account's updated position satisfies this requirement. + CollateralConfiguration.load(collateralType).verifyIssuanceRatio( + debt < 0 ? 0 : debt.toUint(), + newCollateralAmountD18.mulDecimal(collateralPrice), + minIssuanceRatioD18 + ); + + // Accounts cannot reduce collateral if any of the pool's + // connected market has its capacity locked. + _verifyNotCapacityLocked(poolId); + } + + emit DelegationUpdated( + accountId, + poolId, + collateralType, + newCollateralAmountD18, + leverage, + ERC2771Context._msgSender() // TODO LJM this is the executor address, not the account owner or authorized (the one that posted the intent) + ); + + vault.updateRewards( + Vault.PositionSelector(accountId, poolId, collateralType), + totalSharesD18, + actorSharesD18 + ); + } + /** * @dev Updates the given account's position regarding the given pool and collateral type, * with the new amount of delegated collateral. diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol new file mode 100644 index 0000000000..658c25965a --- /dev/null +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -0,0 +1,121 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "./DelegationIntent.sol"; +import "@synthetixio/core-contracts/contracts/utils/SetUtil.sol"; +import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; + +/** + * @title Represents a delegation (or undelegation) intent. + */ +library AccountDelegationIntents { + using SafeCastI256 for int256; + using SafeCastU128 for uint128; + using SetUtil for SetUtil.UintSet; + + error InvalidAccountDelegationIntents(); + + struct Data { + uint128 accountId; + SetUtil.UintSet intentsId; + mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralId => intentIds[] + // accounting for the intents collateral delegated + mapping(uint128 => uint256) delegatedCollateralAmountPerPool; // poolId => delegatedCollateralAmount + uint256 delegateCollateralCache; + mapping(uint128 => uint256) undelegatedCollateralAmountPerPool; // poolId => undelegatedCollateralAmount + uint256 undelegateCollateralCachePerAccount; + } + + /** + * @dev Returns the account delegation intents stored at the specified account id. + */ + function load(uint128 id) internal pure returns (Data storage accountDelegationIntents) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", id)); + assembly { + accountDelegationIntents.slot := s + } + } + + /** + * @dev Returns the account delegation intents stored at the specified account id. Checks if it's valid + */ + function loadValid(uint128 id) internal view returns (Data storage accountDelegationIntents) { + accountDelegationIntents = load(id); + + if (accountDelegationIntents.accountId != id) { + revert InvalidAccountDelegationIntents(); + } + } + + function addIntent(Data storage self, DelegationIntent.Data storage delegationIntent) internal { + self.intentsId.add(delegationIntent.id); + self + .intentsByPair[ + keccak256( + abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) + ) + ] + .add(delegationIntent.id); + + if (delegationIntent.collateralDeltaAmountD18 >= 0) { + self.delegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent + .collateralDeltaAmountD18 + .toUint(); + self.delegateCollateralCache += delegationIntent.collateralDeltaAmountD18.toUint(); + } else { + self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] += (delegationIntent + .collateralDeltaAmountD18 * -1).toUint(); + self.undelegateCollateralCachePerAccount += (delegationIntent.collateralDeltaAmountD18 * + -1).toUint(); + } + } + + function removeIntent( + Data storage self, + DelegationIntent.Data storage delegationIntent + ) internal { + if (!self.intentsId.contains(delegationIntent.id)) { + return; + } + + self.intentsId.remove(delegationIntent.id); + self + .intentsByPair[ + keccak256( + abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) + ) + ] + .remove(delegationIntent.id); + + if (delegationIntent.collateralDeltaAmountD18 >= 0) { + self.delegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent + .collateralDeltaAmountD18 + .toUint(); + self.delegateCollateralCache -= delegationIntent.collateralDeltaAmountD18.toUint(); + } else { + self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] -= (delegationIntent + .collateralDeltaAmountD18 * -1).toUint(); + self.undelegateCollateralCachePerAccount -= (delegationIntent.collateralDeltaAmountD18 * + -1).toUint(); + } + } + + /** + * @dev Returns the delegation intent stored at the specified nonce id. + */ + function intentIdsByPair( + Data storage self, + uint128 poolId, + uint128 accountId + ) internal view returns (uint256[] memory intentIds) { + return self.intentsByPair[keccak256(abi.encodePacked(poolId, accountId))].values(); + } + + function cleanAllIntents(Data storage self) internal { + uint256[] memory intentIds = self.intentsId.values(); + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + removeIntent(self, intent); + } + } +} diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol new file mode 100644 index 0000000000..7f171b8e90 --- /dev/null +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -0,0 +1,124 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "./Config.sol"; + +/** + * @title Represents a delegation (or undelegation) intent. + */ +library DelegationIntent { + error InvalidDelegationIntentId(); + error DelegationIntentNotReady(uint32 declarationTime, uint32 processingStartTime); + error DelegationIntentExpired(uint32 declarationTime, uint32 processingEndTime); + + bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; + + /** + * 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 Data { + /** + * @notice An incrementing id (nonce) to ensure the uniqueness of the intent and prevent replay attacks + */ + uint256 id; + /** + * @notice The ID of the account that has an outstanding intent to delegate a new amount of collateral to + */ + uint128 accountId; + /** + * @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 delta amount of collateral that the account has an + * outstanding intent to delegate/undelegate to the pool, + * denominated with 18 decimals of precision + */ + int256 collateralDeltaAmountD18; + /** + * @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 + */ + uint32 declarationTime; + /** + * @notice The timestamp before which the intent cannot be processed + * @dev dependent on + * {MarketManagerModule.undelegateCollateralDelay} or + * {MarketManagerModule.delegateCollateralDelay} + */ + uint32 processingStartTime; + /** + * @notice The timestamp after which the intent cannot be processed + * (i.e., intent expiry) + * @dev dependent on + * {MarketManagerModule.undelegateCollateralWindow} or + * {MarketManagerModule.delegateCollateralWindow} + */ + uint32 processingEndTime; + } + + /** + * @dev Returns the delegation intent stored at the specified nonce id. + */ + function load(uint256 id) internal pure returns (Data storage delegationIntent) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); + assembly { + delegationIntent.slot := s + } + } + + /** + * @dev Returns the delegation intent stored at the specified nonce id. Checks if it's valid + */ + function loadValid(uint256 id) internal view returns (Data storage delegationIntent) { + delegationIntent = load(id); + + if (delegationIntent.id != id) { + revert InvalidDelegationIntentId(); + } + } + + function latestId() internal view returns (uint256) { + return Config.readUint(_ATOMIC_VALUE_LATEST_ID, 0); + } + + function nextId() internal returns (uint256 id) { + id = Config.readUint(_ATOMIC_VALUE_LATEST_ID, 0) + 1; + Config.put(_ATOMIC_VALUE_LATEST_ID, bytes32(id)); + } + + function checkIsExecutable(Data storage self) internal view { + if (block.timestamp < self.processingStartTime) + revert DelegationIntentNotReady(self.declarationTime, self.processingStartTime); + if (block.timestamp >= self.processingEndTime) + revert DelegationIntentExpired(self.declarationTime, self.processingEndTime); + } + + function isExecutable(Data storage self) internal view returns (bool) { + return + block.timestamp >= self.processingStartTime && block.timestamp < self.processingEndTime; + } +} diff --git a/protocol/synthetix/contracts/storage/Market.sol b/protocol/synthetix/contracts/storage/Market.sol index bb64f0bf66..7e31f43baa 100644 --- a/protocol/synthetix/contracts/storage/Market.sol +++ b/protocol/synthetix/contracts/storage/Market.sol @@ -138,11 +138,16 @@ library Market { * @dev The maximum amount of market provided collateral, per type, that this market can deposit. */ mapping(address => uint256) maximumDepositableD18; - uint32 minDelegateTime; + /** + * @dev Delegation/Undelegation frontrunning protection. + */ + uint32 __unusedLegacyStorageSlot; + uint32 undelegateCollateralDelay; + uint32 undelegateCollateralWindow; + uint32 delegateCollateralDelay; + uint32 delegateCollateralWindow; uint32 __reservedForLater1; uint64 __reservedForLater2; - uint64 __reservedForLater3; - uint64 __reservedForLater4; /** * @dev Market-specific override of the minimum liquidity ratio */ diff --git a/protocol/synthetix/contracts/storage/Pool.sol b/protocol/synthetix/contracts/storage/Pool.sol index 4ad752b8d7..868fbf26b4 100644 --- a/protocol/synthetix/contracts/storage/Pool.sol +++ b/protocol/synthetix/contracts/storage/Pool.sol @@ -61,8 +61,6 @@ library Pool { uint256 maxCollateral ); - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; - struct Data { /** * @dev Numeric identifier for the pool. Must be unique. @@ -420,29 +418,64 @@ library Pool { return Market.load(0); } - function getRequiredMinDelegationTime( - Data storage self - ) internal view returns (uint32 requiredMinDelegateTime) { + function getRequiredDelegationDelayAndWindow( + Data storage self, + bool isUndelegation + ) internal view returns (uint32 requiredDelayTime, uint32 requiredWindowTime) { for (uint256 i = 0; i < self.marketConfigurations.length; i++) { - uint32 marketMinDelegateTime = Market - .load(self.marketConfigurations[i].marketId) - .minDelegateTime; - - if (marketMinDelegateTime > requiredMinDelegateTime) { - requiredMinDelegateTime = marketMinDelegateTime; + Market.Data storage market = Market.load(self.marketConfigurations[i].marketId); + uint32 marketDelayTime = isUndelegation + ? market.undelegateCollateralDelay + : market.delegateCollateralDelay; + + if (marketDelayTime > requiredDelayTime) { + requiredDelayTime = marketDelayTime; + // Also get the window from the more restrictive market + requiredWindowTime = isUndelegation + ? market.undelegateCollateralWindow + : market.delegateCollateralWindow; } } - // solhint-disable-next-line numcast/safe-cast - uint32 maxMinDelegateTime = uint32( - Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) - ); - return - maxMinDelegateTime < requiredMinDelegateTime - ? maxMinDelegateTime - : requiredMinDelegateTime; + if (requiredWindowTime == 0) { + requiredWindowTime = 86400 * 360; // 1 year + } + + // TODO use global max delay and window ?? + // // solhint-disable-next-line numcast/safe-cast + // uint32 maxMinDelegateTime = uint32( + // Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) + // ); + // return + // maxMinDelegateTime < requiredMinDelegateTime + // ? maxMinDelegateTime + // : requiredMinDelegateTime; } + // TODO LJM + // function getRequiredMinDelegationTime( + // Data storage self + // ) internal view returns (uint32 requiredMinDelegateTime) { + // for (uint256 i = 0; i < self.marketConfigurations.length; i++) { + // uint32 marketMinDelegateTime = Market + // .load(self.marketConfigurations[i].marketId) + // .minDelegateTime; + + // if (marketMinDelegateTime > requiredMinDelegateTime) { + // requiredMinDelegateTime = marketMinDelegateTime; + // } + // } + + // // solhint-disable-next-line numcast/safe-cast + // uint32 maxMinDelegateTime = uint32( + // Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) + // ); + // return + // maxMinDelegateTime < requiredMinDelegateTime + // ? maxMinDelegateTime + // : requiredMinDelegateTime; + // } + /** * @dev Returns the debt of the vault that tracks the given collateral type. * @@ -520,19 +553,20 @@ library Pool { } } - function requireMinDelegationTimeElapsed( - Data storage self, - uint64 lastDelegationTime - ) internal view { - uint32 requiredMinDelegationTime = getRequiredMinDelegationTime(self); - if (block.timestamp < lastDelegationTime + requiredMinDelegationTime) { - revert MinDelegationTimeoutPending( - self.id, - // solhint-disable-next-line numcast/safe-cast - uint32(lastDelegationTime + requiredMinDelegationTime - block.timestamp) - ); - } - } + // TODO LJM + // function requireMinDelegationTimeElapsed( + // Data storage self, + // uint64 lastDelegationTime + // ) internal view { + // uint32 requiredMinDelegationTime = getRequiredMinDelegationTime(self); + // if (block.timestamp < lastDelegationTime + requiredMinDelegationTime) { + // revert MinDelegationTimeoutPending( + // self.id, + // // solhint-disable-next-line numcast/safe-cast + // uint32(lastDelegationTime + requiredMinDelegationTime - block.timestamp) + // ); + // } + // } function checkPoolCollateralLimit( Data storage self, diff --git a/protocol/synthetix/contracts/storage/VaultEpoch.sol b/protocol/synthetix/contracts/storage/VaultEpoch.sol index 69d5c84121..ef82be07ad 100644 --- a/protocol/synthetix/contracts/storage/VaultEpoch.sol +++ b/protocol/synthetix/contracts/storage/VaultEpoch.sol @@ -64,12 +64,7 @@ library VaultEpoch { * and directly when users mint or burn USD, or repay debt. */ mapping(uint256 => int256) consolidatedDebtAmountsD18; - /** - * @dev Tracks last time a user delegated to this vault. - * - * Needed to validate min delegation time compliance to prevent small scale debt pool frontrunning - */ - mapping(uint128 => uint64) lastDelegationTime; + mapping(uint128 => uint64) __unused_legacy_slot; } /** From 45dfd4195661a19db6ca014686924ee984e11d1a Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 9 May 2024 12:49:41 -0300 Subject: [PATCH 02/50] wip 366 --- .../contracts/interfaces/IVaultModule.sol | 2 + .../contracts/modules/core/VaultModule.sol | 31 ++++++++++--- .../storage/AccountDelegationIntents.sol | 45 ++++++++++++++++--- protocol/synthetix/test/common/stakers.ts | 8 +++- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 2f92678f33..9a5e310210 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -97,6 +97,7 @@ interface IVaultModule { * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by intent ids. * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param intentIds An array of intents to attempt to process. + * @dev The intents that are not executable at this time will be ignored and am event will be emitted to show that. * Requirements: * * Emits a {DelegationUpdated} event. @@ -110,6 +111,7 @@ interface IVaultModule { * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by pool/accountID pair. * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param poolId The ID of the pool for which the intent of the account to delegate a new amount of collateral is being processed + * @dev The intents that are not executable at this time will be ignored and am event will be emitted to show that. * Requirements: * * Emits a {DelegationUpdated} event. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 89d2f30902..b3577851ae 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -173,9 +173,21 @@ contract VaultModule is IVaultModule { uint256 newCollateralAmountD18, uint256 leverage ) external override { + // 1- Ensure the caller is authorized to represent the account. FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + // 1.1- Input checks + // System only supports leverage of 1.0 for now. + if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); + + // 2- Verify the account holds enough collateral to execute the intent. + // Get previous intents cache + AccountDelegationIntents.Data storage accountIntents = AccountDelegationIntents.getValid( + accountId + ); + // TODO LJM Continue from here... + // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. if (newCollateralAmountD18 > 0) { @@ -185,9 +197,6 @@ contract VaultModule is IVaultModule { ); } - // System only supports leverage of 1.0 for now. - if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); - // Identify the vault that corresponds to this collateral type and pool id. Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; @@ -200,6 +209,12 @@ contract VaultModule is IVaultModule { uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + // 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. + // Ensure current collateral amount differs from the new collateral amount. if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); // If increasing delegated collateral amount, @@ -267,7 +282,7 @@ contract VaultModule is IVaultModule { intent.processingEndTime = intent.processingStartTime + requiredWindowTime; // Add intent to the account's delegation intents. - AccountDelegationIntents.loadValid(accountId).addIntent(intent); + AccountDelegationIntents.getValid(accountId).addIntent(intent); // TODO LJM emit an event } @@ -282,6 +297,10 @@ contract VaultModule is IVaultModule { FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (!intent.isExecutable()) { + // TODO LJM emit an event + continue; + } // Ensure the intent is valid. if (intent.accountId != accountId) revert InvalidDelegationIntent(); @@ -299,7 +318,7 @@ contract VaultModule is IVaultModule { ); // Remove the intent. - AccountDelegationIntents.loadValid(accountId).removeIntent(intent); + AccountDelegationIntents.getValid(accountId).removeIntent(intent); // TODO LJM emit an event } @@ -314,7 +333,7 @@ contract VaultModule is IVaultModule { ) external override { processIntentToDelegateCollateralByIntents( accountId, - AccountDelegationIntents.loadValid(accountId).intentIdsByPair(poolId, accountId) + AccountDelegationIntents.getValid(accountId).intentIdsByPair(poolId, accountId) ); } diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 658c25965a..b03accff7a 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -11,6 +11,7 @@ import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; library AccountDelegationIntents { using SafeCastI256 for int256; using SafeCastU128 for uint128; + using SafeCastU256 for uint256; using SetUtil for SetUtil.UintSet; error InvalidAccountDelegationIntents(); @@ -20,10 +21,12 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralId => intentIds[] // accounting for the intents collateral delegated + SetUtil.UintSet delegatedPools; mapping(uint128 => uint256) delegatedCollateralAmountPerPool; // poolId => delegatedCollateralAmount - uint256 delegateCollateralCache; + uint256 delegateAcountCachedCollateral; mapping(uint128 => uint256) undelegatedCollateralAmountPerPool; // poolId => undelegatedCollateralAmount - uint256 undelegateCollateralCachePerAccount; + uint256 undelegateAcountCachedCollateral; + int256 netAcountCachedDelegatedCollateral; } /** @@ -39,8 +42,12 @@ library AccountDelegationIntents { /** * @dev Returns the account delegation intents stored at the specified account id. Checks if it's valid */ - function loadValid(uint128 id) internal view returns (Data storage accountDelegationIntents) { + function getValid(uint128 id) internal returns (Data storage accountDelegationIntents) { accountDelegationIntents = load(id); + if (accountDelegationIntents.accountId == 0) { + // Uninitialized storage will have a 0 accountId + accountDelegationIntents.accountId = id; + } if (accountDelegationIntents.accountId != id) { revert InvalidAccountDelegationIntents(); @@ -61,13 +68,19 @@ library AccountDelegationIntents { self.delegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent .collateralDeltaAmountD18 .toUint(); - self.delegateCollateralCache += delegationIntent.collateralDeltaAmountD18.toUint(); + self.delegateAcountCachedCollateral += delegationIntent + .collateralDeltaAmountD18 + .toUint(); } else { self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] += (delegationIntent .collateralDeltaAmountD18 * -1).toUint(); - self.undelegateCollateralCachePerAccount += (delegationIntent.collateralDeltaAmountD18 * + self.undelegateAcountCachedCollateral += (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); } + if (!self.delegatedPools.contains(delegationIntent.poolId)) { + self.delegatedPools.add(delegationIntent.poolId); + } + self.netAcountCachedDelegatedCollateral += delegationIntent.collateralDeltaAmountD18; } function removeIntent( @@ -91,13 +104,16 @@ library AccountDelegationIntents { self.delegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent .collateralDeltaAmountD18 .toUint(); - self.delegateCollateralCache -= delegationIntent.collateralDeltaAmountD18.toUint(); + self.delegateAcountCachedCollateral -= delegationIntent + .collateralDeltaAmountD18 + .toUint(); } else { self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] -= (delegationIntent .collateralDeltaAmountD18 * -1).toUint(); - self.undelegateCollateralCachePerAccount -= (delegationIntent.collateralDeltaAmountD18 * + self.undelegateAcountCachedCollateral -= (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); } + self.netAcountCachedDelegatedCollateral += delegationIntent.collateralDeltaAmountD18; } /** @@ -111,11 +127,26 @@ library AccountDelegationIntents { return self.intentsByPair[keccak256(abi.encodePacked(poolId, accountId))].values(); } + /** + * @dev Cleans all intents related to the account. This should be called upon liquidation. + */ function cleanAllIntents(Data storage self) internal { uint256[] memory intentIds = self.intentsId.values(); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); removeIntent(self, intent); } + + self.netAcountCachedDelegatedCollateral = 0; + self.delegateAcountCachedCollateral = 0; + self.undelegateAcountCachedCollateral = 0; + + // Clear the cached collateral per pool + uint256[] memory pools = self.delegatedPools.values(); + for (uint256 i = 0; i < pools.length; i++) { + self.delegatedCollateralAmountPerPool[pools[i].to128()] = 0; + self.undelegatedCollateralAmountPerPool[pools[i].to128()] = 0; + self.delegatedPools.remove(pools[i]); + } } } diff --git a/protocol/synthetix/test/common/stakers.ts b/protocol/synthetix/test/common/stakers.ts index e9ef9d3c68..a0a270fdbf 100644 --- a/protocol/synthetix/test/common/stakers.ts +++ b/protocol/synthetix/test/common/stakers.ts @@ -74,7 +74,7 @@ export const stake = async ( await Core.connect(user).deposit(accountId, CollateralMock.address, delegateAmount.mul(300)); // invest in the pool - await Core.connect(user).delegateCollateral( + await Core.connect(user).declareIntentToDelegateCollateral( accountId, poolId, CollateralMock.address, @@ -82,12 +82,16 @@ export const stake = async ( ethers.utils.parseEther('1') ); + await Core.connect(user).processIntentToDelegateCollateralByPair(accountId, poolId); + // also for convenience invest in the 0 pool - await Core.connect(user).delegateCollateral( + await Core.connect(user).declareIntentToDelegateCollateral( accountId, 0, CollateralMock.address, delegateAmount, ethers.utils.parseEther('1') ); + + await Core.connect(user).processIntentToDelegateCollateralByPair(accountId, 0); }; From cebc9c4716e69ce27d26b862b37264c0a6d0a63e Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 9 May 2024 12:50:16 -0300 Subject: [PATCH 03/50] wip 366 start fixing tests --- .../integration/modules/core/AssociateDebtModule.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts b/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts index 8968431223..d0c799fabe 100644 --- a/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts @@ -119,13 +119,17 @@ describe('AssociateDebtModule', function () { .Core.connect(user2) .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); - await systems().Core.connect(user2).delegateCollateral( + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount, // user1 50%, user2 50% ethers.utils.parseEther('1') ); + + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByPair(user2AccountId, poolId); }); describe('when the market reported debt is 100', function () { From f41d109e253da158b6108110003ef4237a0f3878 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 9 May 2024 14:12:57 -0300 Subject: [PATCH 04/50] fix, wrong params for pair --- .../contracts/storage/AccountDelegationIntents.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index b03accff7a..d84b9e9f41 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -58,9 +58,7 @@ library AccountDelegationIntents { self.intentsId.add(delegationIntent.id); self .intentsByPair[ - keccak256( - abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) - ) + keccak256(abi.encodePacked(delegationIntent.poolId, delegationIntent.accountId)) ] .add(delegationIntent.id); @@ -94,9 +92,7 @@ library AccountDelegationIntents { self.intentsId.remove(delegationIntent.id); self .intentsByPair[ - keccak256( - abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) - ) + keccak256(abi.encodePacked(delegationIntent.poolId, delegationIntent.accountId)) ] .remove(delegationIntent.id); From e3ab470f4684c4d3597fd08a3241ed8a47bfaae0 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 11:23:46 -0300 Subject: [PATCH 05/50] wip: add per collateral accounting --- .../storage/AccountDelegationIntents.sol | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index d84b9e9f41..f1ff9f9d88 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -13,6 +13,7 @@ library AccountDelegationIntents { using SafeCastU128 for uint128; using SafeCastU256 for uint256; using SetUtil for SetUtil.UintSet; + using SetUtil for SetUtil.AddressSet; error InvalidAccountDelegationIntents(); @@ -21,11 +22,17 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralId => intentIds[] // accounting for the intents collateral delegated + // Per Pool SetUtil.UintSet delegatedPools; mapping(uint128 => uint256) delegatedCollateralAmountPerPool; // poolId => delegatedCollateralAmount uint256 delegateAcountCachedCollateral; mapping(uint128 => uint256) undelegatedCollateralAmountPerPool; // poolId => undelegatedCollateralAmount uint256 undelegateAcountCachedCollateral; + // Per Collateral + SetUtil.AddressSet delegatedCollaterals; + mapping(address => uint256) delegatedAmountPerCollateral; // collateralType => delegatedCollateralAmount + mapping(address => uint256) undelegatedAmountPerCollateral; // collateralType => undelegatedCollateralAmount + // Global int256 netAcountCachedDelegatedCollateral; } @@ -63,6 +70,9 @@ library AccountDelegationIntents { .add(delegationIntent.id); if (delegationIntent.collateralDeltaAmountD18 >= 0) { + self.delegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent + .collateralDeltaAmountD18 + .toUint(); self.delegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent .collateralDeltaAmountD18 .toUint(); @@ -70,14 +80,23 @@ library AccountDelegationIntents { .collateralDeltaAmountD18 .toUint(); } else { + self.undelegatedAmountPerCollateral[ + delegationIntent.collateralType + ] += (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] += (delegationIntent .collateralDeltaAmountD18 * -1).toUint(); self.undelegateAcountCachedCollateral += (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); } + if (!self.delegatedPools.contains(delegationIntent.poolId)) { self.delegatedPools.add(delegationIntent.poolId); } + + if (!self.delegatedCollaterals.contains(delegationIntent.collateralType)) { + self.delegatedCollaterals.add(delegationIntent.collateralType); + } + self.netAcountCachedDelegatedCollateral += delegationIntent.collateralDeltaAmountD18; } @@ -97,6 +116,9 @@ library AccountDelegationIntents { .remove(delegationIntent.id); if (delegationIntent.collateralDeltaAmountD18 >= 0) { + self.delegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent + .collateralDeltaAmountD18 + .toUint(); self.delegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent .collateralDeltaAmountD18 .toUint(); @@ -104,6 +126,9 @@ library AccountDelegationIntents { .collateralDeltaAmountD18 .toUint(); } else { + self.undelegatedAmountPerCollateral[ + delegationIntent.collateralType + ] -= (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] -= (delegationIntent .collateralDeltaAmountD18 * -1).toUint(); self.undelegateAcountCachedCollateral -= (delegationIntent.collateralDeltaAmountD18 * @@ -144,5 +169,13 @@ library AccountDelegationIntents { self.undelegatedCollateralAmountPerPool[pools[i].to128()] = 0; self.delegatedPools.remove(pools[i]); } + + // Clear the cached collateral per collateral + address[] memory addresses = self.delegatedCollaterals.values(); + for (uint256 i = 0; i < addresses.length; i++) { + self.delegatedAmountPerCollateral[addresses[i]] = 0; + self.undelegatedAmountPerCollateral[addresses[i]] = 0; + self.delegatedCollaterals.remove(addresses[i]); + } } } From 18de031436ad0e9c22778a34aa2ad5e8e9eb7b19 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 11:36:50 -0300 Subject: [PATCH 06/50] Test MarketManagerModule --- .../modules/core/MarketManagerModule.sol | 2 +- .../modules/core/MarketManagerModule.test.ts | 109 ++++++++++++++++-- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol index d16c7f5279..a5b1b57501 100644 --- a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol +++ b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol @@ -415,7 +415,7 @@ contract MarketManagerModule is IMarketManagerModule { market.delegateCollateralWindow = delegateCollateralWindow; - emit SetUndelegateCollateralWindow(marketId, delegateCollateralWindow); + emit SetDelegateCollateralWindow(marketId, delegateCollateralWindow); } /** diff --git a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts index 3e2da67efd..22ba17af22 100644 --- a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts @@ -487,23 +487,29 @@ describe('MarketManagerModule', function () { // delegate await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId + 1, collateralAddress(), depositAmount, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, poolId + 1); await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId + 2, collateralAddress(), depositAmount, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, poolId + 2); }); before('accumulate debt', async () => { @@ -546,21 +552,40 @@ describe('MarketManagerModule', function () { }); }); - describe('setMarketMinDelegateTime()', () => { + describe('setDelegateCollateralDelay()', () => { before(restore); it('only works for market', async () => { await assertRevert( - systems().Core.setMarketMinDelegateTime(marketId(), 86400), + systems().Core.setDelegateCollateralDelay(marketId(), 86400), 'Unauthorized', systems().Core ); }); - it('fails when min delegation time is unreasonably large', async () => { + describe('success', () => { + let tx: ethers.providers.TransactionResponse; + before('exec', async () => { + tx = await MockMarket().setDelegateCollateralDelay(60); + }); + + it('sets the value', async () => { + assertBn.equal(await systems().Core.getDelegateCollateralDelay(marketId()), 60); + }); + + it('emits', async () => { + await assertEvent(tx, `SetDelegateCollateralDelay(${marketId()}, 60)`, systems().Core); + }); + }); + }); + + describe('setDelegateCollateralWindow()', () => { + before(restore); + + it('only works for market', async () => { await assertRevert( - MockMarket().setMinDelegationTime(100000000), - 'InvalidParameter("minDelegateTime"', + systems().Core.setDelegateCollateralWindow(marketId(), 86400), + 'Unauthorized', systems().Core ); }); @@ -568,15 +593,69 @@ describe('MarketManagerModule', function () { describe('success', () => { let tx: ethers.providers.TransactionResponse; before('exec', async () => { - tx = await MockMarket().setMinDelegationTime(86400); + tx = await MockMarket().setDelegateCollateralWindow(70); }); it('sets the value', async () => { - assertBn.equal(await systems().Core.getMarketMinDelegateTime(marketId()), 86400); + assertBn.equal(await systems().Core.getDelegateCollateralWindow(marketId()), 70); }); it('emits', async () => { - await assertEvent(tx, `SetMinDelegateTime(${marketId()}, 86400)`, systems().Core); + await assertEvent(tx, `SetDelegateCollateralWindow(${marketId()}, 70)`, systems().Core); + }); + }); + }); + + describe('setUndelegateCollateralDelay()', () => { + before(restore); + + it('only works for market', async () => { + await assertRevert( + systems().Core.setUndelegateCollateralDelay(marketId(), 86400), + 'Unauthorized', + systems().Core + ); + }); + + describe('success', () => { + let tx: ethers.providers.TransactionResponse; + before('exec', async () => { + tx = await MockMarket().setUndelegateCollateralDelay(80); + }); + + it('sets the value', async () => { + assertBn.equal(await systems().Core.getUndelegateCollateralDelay(marketId()), 80); + }); + + it('emits', async () => { + await assertEvent(tx, `SetUndelegateCollateralDelay(${marketId()}, 80)`, systems().Core); + }); + }); + }); + + describe('setUndelegateCollateralWindow()', () => { + before(restore); + + it('only works for market', async () => { + await assertRevert( + systems().Core.setUndelegateCollateralWindow(marketId(), 86400), + 'Unauthorized', + systems().Core + ); + }); + + describe('success', () => { + let tx: ethers.providers.TransactionResponse; + before('exec', async () => { + tx = await MockMarket().setUndelegateCollateralWindow(90); + }); + + it('sets the value', async () => { + assertBn.equal(await systems().Core.getUndelegateCollateralWindow(marketId()), 90); + }); + + it('emits', async () => { + await assertEvent(tx, `SetUndelegateCollateralWindow(${marketId()}, 90)`, systems().Core); }); }); }); @@ -711,23 +790,29 @@ describe('MarketManagerModule', function () { // delegate await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId + 1, collateralAddress(), depositAmount, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, poolId + 1); await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId + 2, collateralAddress(), depositAmount, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, poolId + 2); }); it('inRangePools and outRangePools are returned correctly', async () => { From de561f0a451d26d6df4106813900b1fed1569a13 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 12:12:06 -0300 Subject: [PATCH 07/50] Fix LiquidationModule test --- .../modules/core/LiquidationModule.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts b/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts index 2714662f54..5645911578 100644 --- a/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts @@ -111,13 +111,16 @@ describe('LiquidationModule', function () { // use the zero pool to get minted USD await systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId2, poolId, collateralAddress(), depositAmount.mul(10), ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByPair(accountId2, poolId); }); let txn: ethers.providers.TransactionResponse; @@ -276,13 +279,17 @@ describe('LiquidationModule', function () { .deposit(liquidatorAccountId, collateralAddress(), depositAmount.mul(50)); await systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( liquidatorAccountId, 0, collateralAddress(), depositAmount.mul(50), ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByPair(liquidatorAccountId, 0); + await systems() .Core.connect(user2) .mintUsd(liquidatorAccountId, 0, collateralAddress(), liquidatorAccountStartingBalance); From 1f49264a431e9e9b9c63e3d6f868794ce8f427a1 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 14:21:29 -0300 Subject: [PATCH 08/50] wip: fix tests --- .../modules/core/PoolModuleFundAdmin.test.ts | 45 ++----------------- 1 file changed, 4 insertions(+), 41 deletions(-) diff --git a/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts b/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts index e05fb940ff..0ffe754009 100644 --- a/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts +++ b/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts @@ -6,7 +6,6 @@ import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot import { ethers } from 'ethers'; import hre from 'hardhat'; import { bn, bootstrapWithMockMarketAndPool } from '../../bootstrap'; -import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; describe('PoolModule Admin', function () { const { @@ -167,45 +166,6 @@ describe('PoolModule Admin', function () { ); }); - describe('if one of the markets has a min delegation time', () => { - const restore = snapshotCheckpoint(provider); - - before('set market min delegation time to something high', async () => { - await MockMarket().setMinDelegationTime(86400); - }); - - it('fails when min delegation timeout not elapsed', async () => { - await assertRevert( - systems() - .Core.connect(owner) - .setPoolConfiguration(poolId, [ - { marketId: marketId(), weightD18: 1, maxDebtShareValueD18: One }, - { marketId: marketId2, weightD18: 3, maxDebtShareValueD18: One }, - ]), - `MinDelegationTimeoutPending("${poolId}",`, - systems().Core - ); - }); - - describe('after time passes', () => { - before('fast forward', async () => { - // for some reason `fastForward` doesn't seem to work with anvil - await fastForwardTo((await getTime(provider())) + 86400, provider()); - }); - - it('works', async () => { - await systems() - .Core.connect(owner) - .setPoolConfiguration(poolId, [ - { marketId: marketId(), weightD18: 1, maxDebtShareValueD18: One }, - { marketId: marketId2, weightD18: 3, maxDebtShareValueD18: One }, - ]); - }); - }); - - after(restore); - }); - describe('pool changes staking position to add another market', async () => { before('set pool position', async () => { await systems() @@ -421,7 +381,7 @@ describe('PoolModule Admin', function () { // the second pool is here to test the calculation weighted average // and to test pool entering/joining after debt shifts before('set second pool position position', async () => { - await systems().Core.connect(user1).delegateCollateral( + await systems().Core.connect(user1).declareIntentToDelegateCollateral( accountId, secondPoolId, collateralAddress(), @@ -430,6 +390,9 @@ describe('PoolModule Admin', function () { depositAmount, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, secondPoolId); await systems() .Core.connect(user1) From 5a28067575781753eaef44bb496dc6bc230ee453 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 15:57:49 -0300 Subject: [PATCH 09/50] checkpoint: fixing vault module test (wip) --- .../contracts/interfaces/IVaultModule.sol | 4 +- .../contracts/modules/core/VaultModule.sol | 4 +- .../modules/core/VaultModule.test.ts | 335 +++++++++++++----- 3 files changed, 244 insertions(+), 99 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 9a5e310210..786f8551ae 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -76,7 +76,7 @@ interface IVaultModule { * @param collateralType The address of the collateral used in the position. * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. * @param leverage The new leverage amount used in the position, denominated with 18 decimals of precision. - * + * @return intentId The id of the new intent to update the delegated amount. * Requirements: * * - `ERC2771Context._msgSender()` must be the owner of the account, have the `ADMIN` permission, or have the `DELEGATE` permission. @@ -91,7 +91,7 @@ interface IVaultModule { address collateralType, uint256 amount, uint256 leverage - ) external; + ) external returns (uint256 intentId); /** * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by intent ids. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index b3577851ae..bfef450693 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -172,7 +172,7 @@ contract VaultModule is IVaultModule { address collateralType, uint256 newCollateralAmountD18, uint256 leverage - ) external override { + ) external override returns (uint256 intentId) { // 1- Ensure the caller is authorized to represent the account. FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); @@ -269,7 +269,7 @@ contract VaultModule is IVaultModule { .getRequiredDelegationDelayAndWindow(collateralDeltaAmountD18 > 0); // Create a new delegation intent. - uint256 intentId = DelegationIntent.nextId(); + intentId = DelegationIntent.nextId(); DelegationIntent.Data storage intent = DelegationIntent.load(intentId); intent.id = intentId; intent.accountId = accountId; diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index d9924dea39..0358e30fa9 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -7,7 +7,7 @@ import hre from 'hardhat'; import { bn, bootstrapWithStakedPool } from '../../bootstrap'; import Permissions from '../../mixins/AccountRBACMixin.permissions'; import { verifyUsesFeatureFlag } from '../../verifications'; -import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; +// import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import { wei } from '@synthetixio/wei'; describe('VaultModule', function () { @@ -200,7 +200,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), @@ -217,7 +217,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), @@ -231,7 +231,7 @@ describe('VaultModule', function () { it('fails when trying to delegate less than minDelegation amount', async () => { await assertRevert( - systems().Core.connect(user1).delegateCollateral( + systems().Core.connect(user1).declareIntentToDelegateCollateral( accountId, 0, // 0 pool is just easy way to test another pool collateralAddress(), @@ -247,7 +247,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), @@ -263,7 +263,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, 42, collateralAddress(), @@ -281,7 +281,7 @@ describe('VaultModule', function () { () => systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, 42, collateralAddress(), @@ -313,7 +313,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, fakeVaultId, collateralAddress(), @@ -360,7 +360,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, fakeVaultId, collateralAddress(), @@ -384,15 +384,27 @@ describe('VaultModule', function () { }); it('the delegation works as expected with the enabled collateral', async () => { + const intentId = await systems() + .Core.connect(user1) + .callStatic.declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, fakeVaultId, collateralAddress(), depositAmount.div(50), ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); }); }); @@ -410,7 +422,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), @@ -484,13 +496,25 @@ describe('VaultModule', function () { .Core.connect(user2) .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 75%, user2 25% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 75%, user2 25% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); await systems().Core.connect(user2).mintUsd( user2AccountId, @@ -526,76 +550,88 @@ describe('VaultModule', function () { verifyAccountState(user2AccountId, poolId, depositAmount.div(3), depositAmount.div(100)) ); - describe('if one of the markets has a min delegation time', () => { - const restore = snapshotCheckpoint(provider); - - before('set market min delegation time to something high', async () => { - await MockMarket.setMinDelegationTime(86400); - }); - - describe('without time passing', async () => { - it('fails when min delegation timeout not elapsed', async () => { - await assertRevert( - systems().Core.connect(user2).delegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(4), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ), - `MinDelegationTimeoutPending("${poolId}",`, - systems().Core - ); - }); - - it('can increase delegation without waiting', async () => { - await systems() - .Core.connect(user2) - .delegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.mul(2), - ethers.utils.parseEther('1') - ); - }); - - after(restore); - }); - - describe('after time passes', () => { - before('fast forward', async () => { - // for some reason `fastForward` doesn't seem to work with anvil - await fastForwardTo((await getTime(provider())) + 86400, provider()); - }); - - it('works', async () => { - await systems() - .Core.connect(user2) - .delegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(2), - ethers.utils.parseEther('1') - ); - }); - }); - - after(restore); - }); + // describe('if one of the markets has a min delegation time', () => { + // const restore = snapshotCheckpoint(provider); + + // before('set market min delegation time to something high', async () => { + // await MockMarket.setMinDelegationTime(86400); + // }); + + // describe('without time passing', async () => { + // it('fails when min delegation timeout not elapsed', async () => { + // await assertRevert( + // systems().Core.connect(user2).delegateCollateral( + // user2AccountId, + // poolId, + // collateralAddress(), + // depositAmount.div(4), // user1 50%, user2 50% + // ethers.utils.parseEther('1') + // ), + // `MinDelegationTimeoutPending("${poolId}",`, + // systems().Core + // ); + // }); + + // it('can increase delegation without waiting', async () => { + // await systems() + // .Core.connect(user2) + // .delegateCollateral( + // user2AccountId, + // poolId, + // collateralAddress(), + // depositAmount.mul(2), + // ethers.utils.parseEther('1') + // ); + // }); + + // after(restore); + // }); + + // describe('after time passes', () => { + // before('fast forward', async () => { + // // for some reason `fastForward` doesn't seem to work with anvil + // await fastForwardTo((await getTime(provider())) + 86400, provider()); + // }); + + // it('works', async () => { + // await systems() + // .Core.connect(user2) + // .delegateCollateral( + // user2AccountId, + // poolId, + // collateralAddress(), + // depositAmount.div(2), + // ethers.utils.parseEther('1') + // ); + // }); + // }); + + // after(restore); + // }); // these exposure tests should be enabled when exposures other // than 1 are allowed (which might be something we want to do) describe.skip('increase exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -610,13 +646,25 @@ describe('VaultModule', function () { describe.skip('reduce exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).callStatic.declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -631,13 +679,25 @@ describe('VaultModule', function () { describe('remove exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); }); @@ -649,7 +709,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), @@ -676,7 +736,7 @@ describe('VaultModule', function () { it('fails when trying to open delegation position with disabled collateral', async () => { await assertRevert( - systems().Core.connect(user2).delegateCollateral( + systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, 0, collateralAddress(), @@ -691,13 +751,25 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount, // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount, // user1 50%, user2 50% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -719,16 +791,29 @@ describe('VaultModule', function () { const deposit = depositAmount.div(50); const debt = depositAmount.div(100); + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); + await assertRevert( systems() .Core.connect(user2) - .delegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(50), - ethers.utils.parseEther('1') - ), + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), `InsufficientCollateralRatio("${deposit}", "${debt}", "${deposit .mul(price) .div(debt)}", "${issuanceRatioD18}")`, @@ -740,7 +825,7 @@ describe('VaultModule', function () { await assertRevert( systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), @@ -758,16 +843,28 @@ describe('VaultModule', function () { !(await systems().Core.connect(user2).callStatic.isMarketCapacityLocked(marketId)) ); + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); await assertRevert( systems() .Core.connect(user2) - .delegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(10), - ethers.utils.parseEther('1') - ), + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), `CapacityLocked("${marketId}")`, systems().Core ); @@ -791,15 +888,27 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); await systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(10), ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -835,15 +944,27 @@ describe('VaultModule', function () { }); before('delegate', async () => { + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + 0, + ethers.utils.parseEther('1') + ); await systems() .Core.connect(user2) - .delegateCollateral( + .declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), 0, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -853,13 +974,25 @@ describe('VaultModule', function () { it('user2 position is closed', verifyAccountState(user2AccountId, poolId, 0, 0)); it('lets user2 re-stake again', async () => { - await systems().Core.connect(user2).delegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 75%, user2 25% + ethers.utils.parseEther('1') + ); + await systems().Core.connect(user2).declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 75%, user2 25% ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); }); }); @@ -872,15 +1005,27 @@ describe('VaultModule', function () { }); before('undelegate', async () => { + const intentId = await systems() + .Core.connect(user1) + .callStatic.declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress(), + 0, + ethers.utils.parseEther('1') + ); await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), 0, ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); }); // now the pool is empty From 13a512073babfad391675944dbf55d41288d3b04 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 16:52:54 -0300 Subject: [PATCH 10/50] checkpoint: use delta for intent declarations --- .../contracts/interfaces/IVaultModule.sol | 4 +- .../contracts/modules/core/VaultModule.sol | 40 +++++++++++++------ .../storage/AccountDelegationIntents.sol | 17 +++++++- .../test/integration/storage/Pool.test.ts | 5 ++- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 786f8551ae..32a7782f8f 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -74,7 +74,7 @@ interface IVaultModule { * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param poolId The id of the pool associated with the position. * @param collateralType The address of the collateral used in the position. - * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. + * @param deltaAmountD18 The delta amount of collateral delegated in the position, denominated with 18 decimals of precision. * @param leverage The new leverage amount used in the position, denominated with 18 decimals of precision. * @return intentId The id of the new intent to update the delegated amount. * Requirements: @@ -89,7 +89,7 @@ interface IVaultModule { uint128 accountId, uint128 poolId, address collateralType, - uint256 amount, + int256 deltaAmountD18, uint256 leverage ) external returns (uint256 intentId); diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index bfef450693..f6d5046413 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -170,7 +170,7 @@ contract VaultModule is IVaultModule { uint128 accountId, uint128 poolId, address collateralType, - uint256 newCollateralAmountD18, + int256 deltaCollateralAmountD18, uint256 leverage ) external override returns (uint256 intentId) { // 1- Ensure the caller is authorized to represent the account. @@ -188,15 +188,6 @@ contract VaultModule is IVaultModule { ); // TODO LJM Continue from here... - // Each collateral type may specify a minimum collateral amount that can be delegated. - // See CollateralConfiguration.minDelegationD18. - if (newCollateralAmountD18 > 0) { - CollateralConfiguration.requireSufficientDelegation( - collateralType, - newCollateralAmountD18 - ); - } - // Identify the vault that corresponds to this collateral type and pool id. Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; @@ -209,6 +200,25 @@ contract VaultModule is IVaultModule { uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + uint256 newCollateralAmountD18 = deltaCollateralAmountD18 + + accountIntents.netDelegatedAmountPerCollateral[collateralType] > + 0 + ? currentCollateralAmount + + (deltaCollateralAmountD18 + + accountIntents.netDelegatedAmountPerCollateral[collateralType]).toUint() + : currentCollateralAmount - + (deltaCollateralAmountD18 + + accountIntents.netDelegatedAmountPerCollateral[collateralType]).toUint(); + + // Each collateral type may specify a minimum collateral amount that can be delegated. + // See CollateralConfiguration.minDelegationD18. + if (newCollateralAmountD18 > 0) { + CollateralConfiguration.requireSufficientDelegation( + collateralType, + newCollateralAmountD18 + ); + } + // 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. @@ -216,7 +226,7 @@ contract VaultModule is IVaultModule { // 7- Emit a DelegateCollateralIntentDeclared event. // Ensure current collateral amount differs from the new collateral amount. - if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); + if (deltaCollateralAmountD18 == 0) revert InvalidCollateralAmount(); // If increasing delegated collateral amount, // Check that the account has sufficient collateral. else if (newCollateralAmountD18 > currentCollateralAmount) { @@ -266,7 +276,11 @@ contract VaultModule is IVaultModule { (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(poolId) - .getRequiredDelegationDelayAndWindow(collateralDeltaAmountD18 > 0); + .getRequiredDelegationDelayAndWindow( + collateralDeltaAmountD18 + + accountIntents.netDelegatedCollateralAmountPerPool[poolId] > + 0 + ); // Create a new delegation intent. intentId = DelegationIntent.nextId(); @@ -282,7 +296,7 @@ contract VaultModule is IVaultModule { intent.processingEndTime = intent.processingStartTime + requiredWindowTime; // Add intent to the account's delegation intents. - AccountDelegationIntents.getValid(accountId).addIntent(intent); + AccountDelegationIntents.getValid(intent.accountId).addIntent(intent); // TODO LJM emit an event } diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index f1ff9f9d88..d379cad7ad 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -24,12 +24,14 @@ library AccountDelegationIntents { // accounting for the intents collateral delegated // Per Pool SetUtil.UintSet delegatedPools; + mapping(uint128 => int256) netDelegatedCollateralAmountPerPool; // poolId => net delegatedCollateralAmount mapping(uint128 => uint256) delegatedCollateralAmountPerPool; // poolId => delegatedCollateralAmount uint256 delegateAcountCachedCollateral; mapping(uint128 => uint256) undelegatedCollateralAmountPerPool; // poolId => undelegatedCollateralAmount uint256 undelegateAcountCachedCollateral; // Per Collateral SetUtil.AddressSet delegatedCollaterals; + mapping(address => int256) netDelegatedAmountPerCollateral; // poolId => net delegatedCollateralAmount mapping(address => uint256) delegatedAmountPerCollateral; // collateralType => delegatedCollateralAmount mapping(address => uint256) undelegatedAmountPerCollateral; // collateralType => undelegatedCollateralAmount // Global @@ -88,6 +90,10 @@ library AccountDelegationIntents { self.undelegateAcountCachedCollateral += (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); } + self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent + .collateralDeltaAmountD18; + self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent + .collateralDeltaAmountD18; if (!self.delegatedPools.contains(delegationIntent.poolId)) { self.delegatedPools.add(delegationIntent.poolId); @@ -134,7 +140,12 @@ library AccountDelegationIntents { self.undelegateAcountCachedCollateral -= (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); } - self.netAcountCachedDelegatedCollateral += delegationIntent.collateralDeltaAmountD18; + self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent + .collateralDeltaAmountD18; + self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent + .collateralDeltaAmountD18; + + self.netAcountCachedDelegatedCollateral -= delegationIntent.collateralDeltaAmountD18; } /** @@ -167,6 +178,8 @@ library AccountDelegationIntents { for (uint256 i = 0; i < pools.length; i++) { self.delegatedCollateralAmountPerPool[pools[i].to128()] = 0; self.undelegatedCollateralAmountPerPool[pools[i].to128()] = 0; + self.netDelegatedCollateralAmountPerPool[pools[i].to128()] = 0; + self.delegatedPools.remove(pools[i]); } @@ -175,6 +188,8 @@ library AccountDelegationIntents { for (uint256 i = 0; i < addresses.length; i++) { self.delegatedAmountPerCollateral[addresses[i]] = 0; self.undelegatedAmountPerCollateral[addresses[i]] = 0; + self.netDelegatedAmountPerCollateral[addresses[i]] = 0; + self.delegatedCollaterals.remove(addresses[i]); } } diff --git a/protocol/synthetix/test/integration/storage/Pool.test.ts b/protocol/synthetix/test/integration/storage/Pool.test.ts index 64c5c825ea..e98ea0ecac 100644 --- a/protocol/synthetix/test/integration/storage/Pool.test.ts +++ b/protocol/synthetix/test/integration/storage/Pool.test.ts @@ -105,13 +105,16 @@ describe('Pool', function () { before('increase collateral', async () => { await systems() .Core.connect(user1) - .delegateCollateral( + .declareIntentToDelegateCollateral( accountId, poolId, collateralAddress(), depositAmount.mul(2), ethers.utils.parseEther('1') ); + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByPair(accountId, poolId); }); it('the ultimate capacity of the pool ends up higher', async () => { From b2cb01c251b89fa2da2a0b79f36e9cdac74fb277 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 17:42:11 -0300 Subject: [PATCH 11/50] checkpoint: added some events --- .../contracts/interfaces/IVaultModule.sol | 68 +++++++++++++ .../contracts/modules/core/VaultModule.sol | 99 ++++++++++--------- .../contracts/storage/DelegationIntent.sol | 8 ++ 3 files changed, 128 insertions(+), 47 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 32a7782f8f..7782225910 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -45,6 +45,74 @@ interface IVaultModule { address indexed sender ); + /** + * @notice Emitted when {sender} updates the delegation of collateral in the specified liquidity position. + * @param accountId The id of the account whose position was updated. + * @param poolId The id of the pool in which the position was updated. + * @param collateralType The address of the collateral associated to the position. + * @param collateralDeltaAmount The new amount of the position, denominated with 18 decimals of precision. + * @param leverage The new leverage value of the position, denominated with 18 decimals of precision. + * @param intentId The id of the intent to update the position. + * @param declarationTime The time at which the intent was declared. + * @param processingStartTime The time at which the intent can be processed. + * @param processingEndTime The time at which the intent will no longer be able to be processed. + * @param sender The address that triggered the update of the position. + */ + event DelegationIntentDeclared( + uint128 indexed accountId, + uint128 indexed poolId, + address collateralType, + int256 collateralDeltaAmount, + uint256 leverage, + uint256 intentId, + uint32 declarationTime, + uint32 processingStartTime, + uint32 processingEndTime, + address indexed sender + ); + + /** + * @notice Emitted when an intent is removed (due to succesful execution or expiration). + * @param intentId The id of the intent to update the position. + * @param accountId The id of the account whose position was updated. + * @param poolId The id of the pool in which the position was updated. + * @param collateralType The address of the collateral associated to the position. + */ + event DelegationIntentRemoved( + uint256 intentId, + uint128 indexed accountId, + uint128 indexed poolId, + address collateralType + ); + + /** + * @notice Emitted when an intent is skipped due to the intent not being executable at that time. + * @param intentId The id of the intent to update the position. + * @param accountId The id of the account whose position was updated. + * @param poolId The id of the pool in which the position was updated. + * @param collateralType The address of the collateral associated to the position. + */ + event DelegationIntentSkipped( + uint256 intentId, + uint128 indexed accountId, + uint128 indexed poolId, + address collateralType + ); + + /** + * @notice Emitted when an intent is processed. + * @param intentId The id of the intent to update the position. + * @param accountId The id of the account whose position was updated. + * @param poolId The id of the pool in which the position was updated. + * @param collateralType The address of the collateral associated to the position. + */ + event DelegationIntentProcessed( + uint256 intentId, + uint128 indexed accountId, + uint128 indexed poolId, + address collateralType + ); + // /** // * @notice Updates an account's delegated collateral amount for the specified pool and collateral type pair. // * @param accountId The id of the account associated with the position that will be updated. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index f6d5046413..05583dd477 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -173,31 +173,25 @@ contract VaultModule is IVaultModule { int256 deltaCollateralAmountD18, uint256 leverage ) external override returns (uint256 intentId) { - // 1- Ensure the caller is authorized to represent the account. + // Ensure the caller is authorized to represent the account. FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); - // 1.1- Input checks + // Input checks // System only supports leverage of 1.0 for now. if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); + // Ensure current collateral amount differs from the new collateral amount. + if (deltaCollateralAmountD18 == 0) revert InvalidCollateralAmount(); - // 2- Verify the account holds enough collateral to execute the intent. + // Verify the account holds enough collateral to execute the intent. // Get previous intents cache AccountDelegationIntents.Data storage accountIntents = AccountDelegationIntents.getValid( accountId ); - // TODO LJM Continue from here... // Identify the vault that corresponds to this collateral type and pool id. Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; - // Use account interaction to update its rewards. - // TODO LJM Do we need to call this in the intent or just at execution? - // uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; - // uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( - // accountId.toBytes32() - // ); - uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); uint256 newCollateralAmountD18 = deltaCollateralAmountD18 + @@ -218,15 +212,7 @@ contract VaultModule is IVaultModule { newCollateralAmountD18 ); } - - // 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. - - // Ensure current collateral amount differs from the new collateral amount. - if (deltaCollateralAmountD18 == 0) revert InvalidCollateralAmount(); + // Check the validity of the collateral amount to be delegated, respecting the caches that track outstanding intents to delegate or undelegate collateral. // If increasing delegated collateral amount, // Check that the account has sufficient collateral. else if (newCollateralAmountD18 > currentCollateralAmount) { @@ -244,30 +230,6 @@ contract VaultModule is IVaultModule { newCollateralAmountD18 - currentCollateralAmount ); } - // TODO LJM more checks from original code - // // If decreasing the delegated collateral amount, - // // check the account's collateralization ratio. - // // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. - // if (newCollateralAmountD18 < currentCollateralAmount) { - // int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; - - // uint256 minIssuanceRatioD18 = Pool - // .loadExisting(poolId) - // .collateralConfigurations[collateralType] - // .issuanceRatioD18; - - // // Minimum collateralization ratios are configured in the system per collateral type.abi - // // Ensure that the account's updated position satisfies this requirement. - // CollateralConfiguration.load(collateralType).verifyIssuanceRatio( - // debt < 0 ? 0 : debt.toUint(), - // newCollateralAmountD18.mulDecimal(collateralPrice), - // minIssuanceRatioD18 - // ); - - // // Accounts cannot reduce collateral if any of the pool's - // // connected market has its capacity locked. - // _verifyNotCapacityLocked(poolId); - // } // Prepare data for storing the new intent. int256 collateralDeltaAmountD18 = newCollateralAmountD18 > currentCollateralAmount @@ -298,7 +260,19 @@ contract VaultModule is IVaultModule { // Add intent to the account's delegation intents. AccountDelegationIntents.getValid(intent.accountId).addIntent(intent); - // TODO LJM emit an event + // emit an event + emit DelegationIntentDeclared( + accountId, + poolId, + collateralType, + collateralDeltaAmountD18, + leverage, + intentId, + intent.declarationTime, + intent.processingStartTime, + intent.processingEndTime, + ERC2771Context._msgSender() + ); } /** @@ -312,7 +286,26 @@ contract VaultModule is IVaultModule { for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (!intent.isExecutable()) { - // TODO LJM emit an event + // Remove the intent. + if (intent.windowIsClosed()) { + AccountDelegationIntents.getValid(accountId).removeIntent(intent); + emit DelegationIntentRemoved( + intent.id, + accountId, + intent.poolId, + intent.collateralType + ); + } + + // emit an event + emit DelegationIntentSkipped( + intent.id, + accountId, + intent.poolId, + intent.collateralType + ); + + // skip to the next intent continue; } @@ -333,8 +326,20 @@ contract VaultModule is IVaultModule { // Remove the intent. AccountDelegationIntents.getValid(accountId).removeIntent(intent); + emit DelegationIntentRemoved( + intent.id, + accountId, + intent.poolId, + intent.collateralType + ); - // TODO LJM emit an event + // emit an event + emit DelegationIntentProcessed( + intent.id, + accountId, + intent.poolId, + intent.collateralType + ); } } diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 7f171b8e90..9dc006d565 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -121,4 +121,12 @@ library DelegationIntent { return block.timestamp >= self.processingStartTime && block.timestamp < self.processingEndTime; } + + function windowIsOpened(Data storage self) internal view returns (bool) { + return block.timestamp >= self.processingStartTime; + } + + function windowIsClosed(Data storage self) internal view returns (bool) { + return block.timestamp < self.processingEndTime; + } } From 7280c5802e2fb4eaad4eac6b36cb0855b8380d75 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 13 May 2024 18:30:14 -0300 Subject: [PATCH 12/50] Link to liquidation --- .../modules/core/LiquidationModule.sol | 6 + .../contracts/modules/core/VaultModule.sol | 123 ------------------ 2 files changed, 6 insertions(+), 123 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/LiquidationModule.sol b/protocol/synthetix/contracts/modules/core/LiquidationModule.sol index 97c0ae48a6..eaba2260e5 100644 --- a/protocol/synthetix/contracts/modules/core/LiquidationModule.sol +++ b/protocol/synthetix/contracts/modules/core/LiquidationModule.sol @@ -14,6 +14,8 @@ import "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; import "@synthetixio/core-modules/contracts/storage/FeatureFlag.sol"; +import "../../storage/AccountDelegationIntents.sol"; + /** * @title Module for liquidated positions and vaults that are below the liquidation ratio. * @dev See ILiquidationModule. @@ -33,6 +35,7 @@ contract LiquidationModule is ILiquidationModule { using VaultEpoch for VaultEpoch.Data; using Distribution for Distribution.Data; using ScalableMapping for ScalableMapping.Data; + using AccountDelegationIntents for AccountDelegationIntents.Data; bytes32 private constant _USD_TOKEN = "USDToken"; @@ -114,6 +117,9 @@ contract LiquidationModule is ILiquidationModule { liquidationData.amountRewarded ); + // Clean any outstanding intents to delegate collateral + AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + emit Liquidation( accountId, poolId, diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 05583dd477..47bfffd95d 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -40,129 +40,6 @@ contract VaultModule is IVaultModule { bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; - // function delegateCollateral( - // uint128 accountId, - // uint128 poolId, - // address collateralType, - // uint256 newCollateralAmountD18, - // uint256 leverage - // ) external override { - // FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); - // Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); - - // // Each collateral type may specify a minimum collateral amount that can be delegated. - // // See CollateralConfiguration.minDelegationD18. - // if (newCollateralAmountD18 > 0) { - // CollateralConfiguration.requireSufficientDelegation( - // collateralType, - // newCollateralAmountD18 - // ); - // } - - // // System only supports leverage of 1.0 for now. - // if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); - - // // Identify the vault that corresponds to this collateral type and pool id. - // Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; - - // // Use account interaction to update its rewards. - // uint256 totalSharesD18 = vault.currentEpoch().accountsDebtDistribution.totalSharesD18; - // uint256 actorSharesD18 = vault.currentEpoch().accountsDebtDistribution.getActorShares( - // accountId.toBytes32() - // ); - - // uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); - - // // Conditions for collateral amount - - // // Ensure current collateral amount differs from the new collateral amount. - // if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); - // // If increasing delegated collateral amount, - // // Check that the account has sufficient collateral. - // else if (newCollateralAmountD18 > currentCollateralAmount) { - // // Check if the collateral is enabled here because we still want to allow reducing delegation for disabled collaterals. - // CollateralConfiguration.collateralEnabled(collateralType); - - // Account.requireSufficientCollateral( - // accountId, - // collateralType, - // newCollateralAmountD18 - currentCollateralAmount - // ); - - // Pool.loadExisting(poolId).checkPoolCollateralLimit( - // collateralType, - // newCollateralAmountD18 - currentCollateralAmount - // ); - - // // if decreasing delegation amount, ensure min time has elapsed - // } else { - // // TODO LJM - // // Pool.loadExisting(poolId).requireMinDelegationTimeElapsed( - // // vault.currentEpoch().lastDelegationTime[accountId] - // // ); - // } - - // // Update the account's position for the given pool and collateral type, - // // Note: This will trigger an update in the entire debt distribution chain. - // uint256 collateralPrice = _updatePosition( - // accountId, - // poolId, - // collateralType, - // newCollateralAmountD18, - // currentCollateralAmount, - // leverage - // ); - - // _updateAccountCollateralPools( - // accountId, - // poolId, - // collateralType, - // newCollateralAmountD18 > 0 - // ); - - // // If decreasing the delegated collateral amount, - // // check the account's collateralization ratio. - // // Note: This is the best time to do so since the user's debt and the collateral's price have both been updated. - // if (newCollateralAmountD18 < currentCollateralAmount) { - // int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; - - // uint256 minIssuanceRatioD18 = Pool - // .loadExisting(poolId) - // .collateralConfigurations[collateralType] - // .issuanceRatioD18; - - // // Minimum collateralization ratios are configured in the system per collateral type.abi - // // Ensure that the account's updated position satisfies this requirement. - // CollateralConfiguration.load(collateralType).verifyIssuanceRatio( - // debt < 0 ? 0 : debt.toUint(), - // newCollateralAmountD18.mulDecimal(collateralPrice), - // minIssuanceRatioD18 - // ); - - // // Accounts cannot reduce collateral if any of the pool's - // // connected market has its capacity locked. - // _verifyNotCapacityLocked(poolId); - // } - - // // solhint-disable-next-line numcast/safe-cast - // vault.currentEpoch().lastDelegationTime[accountId] = uint64(block.timestamp); - - // emit DelegationUpdated( - // accountId, - // poolId, - // collateralType, - // newCollateralAmountD18, - // leverage, - // ERC2771Context._msgSender() - // ); - - // vault.updateRewards( - // Vault.PositionSelector(accountId, poolId, collateralType), - // totalSharesD18, - // actorSharesD18 - // ); - // } - /** * @inheritdoc IVaultModule */ From 64030e3d7dcadc2e26a30831ec886ac4f0ed2a21 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 14 May 2024 16:59:50 -0300 Subject: [PATCH 13/50] checkpoint: more tests fixed and bugs smashed --- .../contracts/modules/core/VaultModule.sol | 17 +- .../modules/core/VaultModule.test.ts | 483 ++++++++++-------- 2 files changed, 278 insertions(+), 222 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 47bfffd95d..a8c0be883a 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -78,8 +78,9 @@ contract VaultModule is IVaultModule { (deltaCollateralAmountD18 + accountIntents.netDelegatedAmountPerCollateral[collateralType]).toUint() : currentCollateralAmount - - (deltaCollateralAmountD18 + - accountIntents.netDelegatedAmountPerCollateral[collateralType]).toUint(); + (-1 * + (deltaCollateralAmountD18 + + accountIntents.netDelegatedAmountPerCollateral[collateralType])).toUint(); // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. @@ -109,14 +110,10 @@ contract VaultModule is IVaultModule { } // Prepare data for storing the new intent. - int256 collateralDeltaAmountD18 = newCollateralAmountD18 > currentCollateralAmount - ? (newCollateralAmountD18 - currentCollateralAmount).toInt() - : (currentCollateralAmount - newCollateralAmountD18).toInt(); - (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(poolId) .getRequiredDelegationDelayAndWindow( - collateralDeltaAmountD18 + + deltaCollateralAmountD18 + accountIntents.netDelegatedCollateralAmountPerPool[poolId] > 0 ); @@ -128,7 +125,7 @@ contract VaultModule is IVaultModule { intent.accountId = accountId; intent.poolId = poolId; intent.collateralType = collateralType; - intent.collateralDeltaAmountD18 = collateralDeltaAmountD18; + intent.collateralDeltaAmountD18 = deltaCollateralAmountD18; intent.leverage = leverage; intent.declarationTime = block.timestamp.to32(); intent.processingStartTime = intent.declarationTime + requiredDelayTime; @@ -142,7 +139,7 @@ contract VaultModule is IVaultModule { accountId, poolId, collateralType, - collateralDeltaAmountD18, + deltaCollateralAmountD18, leverage, intentId, intent.declarationTime, @@ -341,7 +338,7 @@ contract VaultModule is IVaultModule { uint256 newCollateralAmountD18 = deltaCollateralAmountD18 > 0 ? vault.currentAccountCollateral(accountId) + deltaCollateralAmountD18.toUint() - : vault.currentAccountCollateral(accountId) - deltaCollateralAmountD18.toUint(); + : vault.currentAccountCollateral(accountId) - (deltaCollateralAmountD18 * -1).toUint(); // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index 0358e30fa9..d6f58c82d1 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -141,6 +141,52 @@ describe('VaultModule', function () { }; } + async function fixedToDeltaAmount( + accountId: number, + poolId: number, + collateralAddress: string, + fixedDepositAmount: BigNumber + ): Promise { + const currentPositionCollateral = await systems().Core.getPositionCollateral( + accountId, + poolId, + collateralAddress + ); + const deltaCollateral = fixedDepositAmount.sub(currentPositionCollateral); + return deltaCollateral; + } + + async function delegateCollateral( + signer: ethers.Signer, + accountId: number, + poolId: number, + collateralAddress: string, + fixedDepositAmount: BigNumber, + leverage: BigNumber + ): Promise { + const intentId = await systems() + .Core.connect(signer) + .callStatic.declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress, + await fixedToDeltaAmount(accountId, poolId, collateralAddress, fixedDepositAmount), + leverage + ); + await systems() + .Core.connect(signer) + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress, + await fixedToDeltaAmount(accountId, poolId, collateralAddress, fixedDepositAmount), + leverage + ); + await systems() + .Core.connect(signer) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + } + describe('fresh vault', async () => { const fakeFreshVaultId = 209372; @@ -231,13 +277,15 @@ describe('VaultModule', function () { it('fails when trying to delegate less than minDelegation amount', async () => { await assertRevert( - systems().Core.connect(user1).declareIntentToDelegateCollateral( - accountId, - 0, // 0 pool is just easy way to test another pool - collateralAddress(), - depositAmount.div(51), - ethers.utils.parseEther('1') - ), + systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + 0, // 0 pool is just easy way to test another pool + collateralAddress(), + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(51)), + ethers.utils.parseEther('1') + ), 'InsufficientDelegation("20000000000000000000")', systems().Core ); @@ -251,7 +299,7 @@ describe('VaultModule', function () { accountId, poolId, collateralAddress(), - depositAmount, + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount), ethers.utils.parseEther('1') ), 'InvalidCollateralAmount()', @@ -267,7 +315,7 @@ describe('VaultModule', function () { accountId, 42, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(50)), ethers.utils.parseEther('1') ), 'PoolNotFound("42")', @@ -278,14 +326,14 @@ describe('VaultModule', function () { verifyUsesFeatureFlag( () => systems().Core, 'delegateCollateral', - () => + async () => systems() .Core.connect(user1) .declareIntentToDelegateCollateral( accountId, 42, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(50)), ethers.utils.parseEther('1') ) ); @@ -310,16 +358,38 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - await assertRevert( - systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( + const intentId = await systems() + .Core.connect(user1) + .callStatic.declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await fixedToDeltaAmount( accountId, fakeVaultId, collateralAddress(), - depositAmount.div(50), - ethers.utils.parseEther('1') + depositAmount.div(50) ), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await fixedToDeltaAmount( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50) + ), + ethers.utils.parseEther('1') + ); + await assertRevert( + systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), `CollateralDepositDisabled("${collateralAddress()}")`, systems().Core ); @@ -357,16 +427,39 @@ describe('VaultModule', function () { // fails when collateral is disabled for the pool by pool owner it('fails when trying to open delegation position with disabled collateral', async () => { - await assertRevert( - systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( + const intentId = await systems() + .Core.connect(user1) + .callStatic.declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await fixedToDeltaAmount( accountId, fakeVaultId, collateralAddress(), - depositAmount.div(50), - ethers.utils.parseEther('1') + depositAmount.div(50) ), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await fixedToDeltaAmount( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50) + ), + ethers.utils.parseEther('1') + ); + + await assertRevert( + systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), `PoolCollateralLimitExceeded("${fakeVaultId}", "${collateralAddress()}", "${depositAmount .div(50) .toString()}", "${bn(10).toString()}")`, @@ -384,51 +477,51 @@ describe('VaultModule', function () { }); it('the delegation works as expected with the enabled collateral', async () => { + await delegateCollateral( + user1, + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); + }); + }); + + describe('when pool has limited collateral deposit', async () => { + before('set pool limit', async () => { + await systems() + .Core.connect(owner) + .setPoolCollateralConfiguration(poolId, collateralAddress(), { + collateralLimitD18: depositAmount.div(2), + issuanceRatioD18: bn(0), + }); + }); + + it('fails when pool does not allow sufficient deposit amount', async () => { const intentId = await systems() .Core.connect(user1) .callStatic.declareIntentToDelegateCollateral( accountId, - fakeVaultId, + poolId, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.mul(2)), ethers.utils.parseEther('1') ); await systems() .Core.connect(user1) .declareIntentToDelegateCollateral( accountId, - fakeVaultId, + poolId, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.mul(2)), ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user1) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]); - }); - }); - - describe('when pool has limited collateral deposit', async () => { - before('set pool limit', async () => { - await systems() - .Core.connect(owner) - .setPoolCollateralConfiguration(poolId, collateralAddress(), { - collateralLimitD18: depositAmount.div(2), - issuanceRatioD18: bn(0), - }); - }); - it('fails when pool does not allow sufficient deposit amount', async () => { await assertRevert( systems() .Core.connect(user1) - .declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress(), - depositAmount.mul(2), - ethers.utils.parseEther('1') - ), + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), `PoolCollateralLimitExceeded("${poolId}", "${collateralAddress()}", "${depositAmount .mul(2) .toString()}", "${depositAmount.div(2).toString()}")`, @@ -496,25 +589,14 @@ describe('VaultModule', function () { .Core.connect(user2) .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(3), // user1 75%, user2 25% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 75%, user2 25% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); await systems().Core.connect(user2).mintUsd( user2AccountId, @@ -613,25 +695,14 @@ describe('VaultModule', function () { // than 1 are allowed (which might be something we want to do) describe.skip('increase exposure', async () => { before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(3), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -646,25 +717,14 @@ describe('VaultModule', function () { describe.skip('reduce exposure', async () => { before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(3), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).callStatic.declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -679,25 +739,14 @@ describe('VaultModule', function () { describe('remove exposure', async () => { before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(3), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 50%, user2 50% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); }); @@ -706,16 +755,29 @@ describe('VaultModule', function () { const wanted = depositAmount.mul(3); const missing = wanted.sub(depositAmount.div(3)); + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await fixedToDeltaAmount(user2AccountId, poolId, collateralAddress(), wanted), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await fixedToDeltaAmount(user2AccountId, poolId, collateralAddress(), wanted), + ethers.utils.parseEther('1') + ); + await assertRevert( systems() .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - wanted, - ethers.utils.parseEther('1') - ), + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), `InsufficientAccountCollateral("${missing}")`, systems().Core ); @@ -735,14 +797,29 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - await assertRevert( - systems().Core.connect(user2).declareIntentToDelegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( user2AccountId, 0, collateralAddress(), - depositAmount, // user1 50%, user2 50% + await fixedToDeltaAmount(user2AccountId, 0, collateralAddress(), depositAmount), // user1 50%, user2 50% ethers.utils.parseEther('1') - ), + ); + await systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( + user2AccountId, + 0, + collateralAddress(), + await fixedToDeltaAmount(user2AccountId, 0, collateralAddress(), depositAmount), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), `CollateralDepositDisabled("${collateralAddress()}")`, systems().Core ); @@ -751,25 +828,14 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount, // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount, // user1 50%, user2 50% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); it( @@ -797,7 +863,12 @@ describe('VaultModule', function () { user2AccountId, poolId, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(50) + ), ethers.utils.parseEther('1') ); await systems() @@ -806,7 +877,12 @@ describe('VaultModule', function () { user2AccountId, poolId, collateralAddress(), - depositAmount.div(50), + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(50) + ), ethers.utils.parseEther('1') ); @@ -822,16 +898,39 @@ describe('VaultModule', function () { }); it('fails when reducing to below minDelegation amount', async () => { - await assertRevert( - systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( + const intentId = await systems() + .Core.connect(user2) + .callStatic.declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await fixedToDeltaAmount( user2AccountId, poolId, collateralAddress(), - depositAmount.div(51), - ethers.utils.parseEther('1') + depositAmount.div(51) + ), + ethers.utils.parseEther('1') + ); + await systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(51) ), + ethers.utils.parseEther('1') + ); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), 'InsufficientDelegation("20000000000000000000")', systems().Core ); @@ -849,7 +948,12 @@ describe('VaultModule', function () { user2AccountId, poolId, collateralAddress(), - depositAmount.div(10), + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10) + ), ethers.utils.parseEther('1') ); await systems() @@ -858,7 +962,12 @@ describe('VaultModule', function () { user2AccountId, poolId, collateralAddress(), - depositAmount.div(10), + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10) + ), ethers.utils.parseEther('1') ); await assertRevert( @@ -888,27 +997,14 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(10), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(10), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); + await delegateCollateral( + user2, + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); }); it( @@ -944,27 +1040,14 @@ describe('VaultModule', function () { }); before('delegate', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - 0, - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - 0, - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); + await delegateCollateral( + user2, + user2AccountId, + poolId, + collateralAddress(), + BigNumber.from(0), + ethers.utils.parseEther('1') + ); }); it( @@ -974,25 +1057,14 @@ describe('VaultModule', function () { it('user2 position is closed', verifyAccountState(user2AccountId, poolId, 0, 0)); it('lets user2 re-stake again', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(3), // user1 75%, user2 25% - ethers.utils.parseEther('1') - ); - await systems().Core.connect(user2).declareIntentToDelegateCollateral( + await delegateCollateral( + user2, user2AccountId, poolId, collateralAddress(), depositAmount.div(3), // user1 75%, user2 25% ethers.utils.parseEther('1') ); - await systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]); }); }); }); @@ -1005,27 +1077,14 @@ describe('VaultModule', function () { }); before('undelegate', async () => { - const intentId = await systems() - .Core.connect(user1) - .callStatic.declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress(), - 0, - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress(), - 0, - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user1) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + await delegateCollateral( + user1, + accountId, + poolId, + collateralAddress(), + BigNumber.from(0), + ethers.utils.parseEther('1') + ); }); // now the pool is empty From 3f68d81b851b617b551faf3a47ca861ef3d77534 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 15 May 2024 14:08:16 -0300 Subject: [PATCH 14/50] Fix tests --- .../contracts/interfaces/IVaultModule.sol | 12 +++++ .../contracts/modules/core/VaultModule.sol | 28 ++++++---- .../modules/core/VaultModule.test.ts | 52 ++++++++----------- 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 7782225910..eaae6d1f71 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -27,6 +27,16 @@ interface IVaultModule { */ error InvalidDelegationIntent(); + /** + * @notice Thrown when the specified intent is not executable due to pending intents. + */ + error ExceedingUndelegateAmount( + int256 deltaCollateralAmountD18, + int256 cachedDeltaCollateralAmountD18, + int256 totalDeltaCollateralAmountD18, + uint256 currentCollateralAmount + ); + /** * @notice Emitted when {sender} updates the delegation of collateral in the specified liquidity position. * @param accountId The id of the account whose position was updated. @@ -186,6 +196,8 @@ interface IVaultModule { */ function processIntentToDelegateCollateralByPair(uint128 accountId, uint128 poolId) external; + function deleteAllIntents(uint128 accountId) external; + /** * @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0. * @dev Call this function using `callStatic` to treat it as a view function. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index a8c0be883a..ece0746a15 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -70,17 +70,20 @@ contract VaultModule is IVaultModule { Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + int256 accumulatedDelta = deltaCollateralAmountD18 + + accountIntents.netDelegatedAmountPerCollateral[collateralType]; + if (accumulatedDelta < 0 && currentCollateralAmount < (-1 * accumulatedDelta).toUint()) { + revert ExceedingUndelegateAmount( + deltaCollateralAmountD18, + accountIntents.netDelegatedAmountPerCollateral[collateralType], + accumulatedDelta, + currentCollateralAmount + ); + } - uint256 newCollateralAmountD18 = deltaCollateralAmountD18 + - accountIntents.netDelegatedAmountPerCollateral[collateralType] > - 0 - ? currentCollateralAmount + - (deltaCollateralAmountD18 + - accountIntents.netDelegatedAmountPerCollateral[collateralType]).toUint() - : currentCollateralAmount - - (-1 * - (deltaCollateralAmountD18 + - accountIntents.netDelegatedAmountPerCollateral[collateralType])).toUint(); + uint256 newCollateralAmountD18 = accumulatedDelta > 0 + ? currentCollateralAmount + (accumulatedDelta).toUint() + : currentCollateralAmount - (-1 * accumulatedDelta).toUint(); // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. @@ -230,6 +233,11 @@ contract VaultModule is IVaultModule { ); } + function deleteAllIntents(uint128 accountId) external override { + Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + } + /** * @inheritdoc IVaultModule */ diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index d6f58c82d1..d7983833bc 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -477,6 +477,7 @@ describe('VaultModule', function () { }); it('the delegation works as expected with the enabled collateral', async () => { + await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( user1, accountId, @@ -589,6 +590,7 @@ describe('VaultModule', function () { .Core.connect(user2) .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -695,6 +697,7 @@ describe('VaultModule', function () { // than 1 are allowed (which might be something we want to do) describe.skip('increase exposure', async () => { before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -717,6 +720,7 @@ describe('VaultModule', function () { describe.skip('reduce exposure', async () => { before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -739,6 +743,7 @@ describe('VaultModule', function () { describe('remove exposure', async () => { before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -828,6 +833,7 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -898,39 +904,22 @@ describe('VaultModule', function () { }); it('fails when reducing to below minDelegation amount', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(51) - ), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); + await assertRevert( + systems() + .Core.connect(user2) + .declareIntentToDelegateCollateral( user2AccountId, poolId, collateralAddress(), - depositAmount.div(51) + await fixedToDeltaAmount( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(51) + ), + ethers.utils.parseEther('1') ), - ethers.utils.parseEther('1') - ); - - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), 'InsufficientDelegation("20000000000000000000")', systems().Core ); @@ -938,6 +927,7 @@ describe('VaultModule', function () { it('fails when market becomes capacity locked', async () => { // sanity + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); assert.ok( !(await systems().Core.connect(user2).callStatic.isMarketCapacityLocked(marketId)) ); @@ -997,6 +987,7 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -1040,6 +1031,7 @@ describe('VaultModule', function () { }); before('delegate', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -1057,6 +1049,7 @@ describe('VaultModule', function () { it('user2 position is closed', verifyAccountState(user2AccountId, poolId, 0, 0)); it('lets user2 re-stake again', async () => { + await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( user2, user2AccountId, @@ -1077,6 +1070,7 @@ describe('VaultModule', function () { }); before('undelegate', async () => { + await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( user1, accountId, From c60b1c4a8bd49e1fe78a67a34a995dbe752b76cc Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 16 May 2024 12:04:31 -0300 Subject: [PATCH 15/50] fix other project tests --- .../LegacyMarket.iosiroInfiniteMoney.ts | 35 +- .../test/integration/LegacyMarket.ts | 21 +- .../test/integration/Insolvent.test.ts | 19 +- .../integration/Position/InterestRate.test.ts | 19 +- .../test/common/delegateCollateral.ts | 93 +++++ protocol/synthetix/test/common/index.ts | 1 + .../modules/core/VaultModule.test.ts | 326 ++++++------------ 7 files changed, 258 insertions(+), 256 deletions(-) create mode 100644 protocol/synthetix/test/common/delegateCollateral.ts diff --git a/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts b/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts index 7ff2f1a3c2..3c56c17640 100644 --- a/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts +++ b/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts @@ -6,6 +6,7 @@ import { wei } from '@synthetixio/wei'; import { snapshotCheckpoint } from '../utils'; import { fastForward } from '@synthetixio/core-utils/utils/hardhat/rpc'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import { delegateCollateral } from '@synthetixio/main/test/common'; async function getImpersonatedSigner( provider: ethers.providers.JsonRpcProvider, @@ -274,15 +275,17 @@ describe('LegacyMarket (iosiro)', function () { await v3System .connect(whaleAccount) .deposit(whaleAccountId, collateralType, whaleDelegationAmount); - await v3System - .connect(whaleAccount) - .delegateCollateral( - whaleAccountId, - otherPoolId, - collateralType, - whaleDelegationAmount, - wei(1).toBN() - ); + await delegateCollateral( + () => ({ + Core: v3System, + }), + whaleAccount, + whaleAccountId, + otherPoolId, + collateralType, + whaleDelegationAmount, + wei(1).toBN() + ); await v3System .connect(whaleAccount) .mintUsd(whaleAccountId, otherPoolId, collateralType, wei(3333).toBN()); @@ -325,9 +328,17 @@ describe('LegacyMarket (iosiro)', function () { const amountToWithdraw = accountCollateralDetails.totalDeposited; await fastForward(605000, cannonProvider); - await v3System - .connect(attacker) - .delegateCollateral(accountID, poolId, collateralType, 0, wei(1).toBN()); + await delegateCollateral( + () => ({ + Core: v3System, + }), + attacker, + accountID, + poolId, + collateralType, + wei(0).toBN(), + wei(1).toBN() + ); await v3System.connect(attacker).withdraw(accountID, collateralType, amountToWithdraw); // attacker restakes in v2 await snxV2.connect(attacker).issueMaxSynths(); diff --git a/markets/legacy-market/test/integration/LegacyMarket.ts b/markets/legacy-market/test/integration/LegacyMarket.ts index 8590e1021e..6c4c345a0a 100644 --- a/markets/legacy-market/test/integration/LegacyMarket.ts +++ b/markets/legacy-market/test/integration/LegacyMarket.ts @@ -11,6 +11,7 @@ import { LegacyMarket } from '../../typechain-types/contracts/LegacyMarket'; import Wei, { wei } from '@synthetixio/wei'; import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; import { snapshotCheckpoint } from '../utils'; +import { delegateCollateral } from '@synthetixio/main/test/common'; async function getImpersonatedSigner( provider: ethers.providers.JsonRpcProvider, @@ -347,15 +348,17 @@ describe('LegacyMarket', function () { await v3System.connect(owner).deposit(accountId, snxToken.address, delegateAmount); // invest in the pool - await v3System - .connect(owner) - .delegateCollateral( - accountId, - await v3System.getPreferredPool(), - snxToken.address, - delegateAmount, - ethers.utils.parseEther('1') - ); + await delegateCollateral( + () => ({ + Core: v3System, + }), + owner, + accountId, + await v3System.getPreferredPool(), + snxToken.address, + delegateAmount, + ethers.utils.parseEther('1') + ); // sanity assertBn.equal( diff --git a/markets/perps-market/test/integration/Insolvent.test.ts b/markets/perps-market/test/integration/Insolvent.test.ts index 40da1ada2a..ad7352bd64 100644 --- a/markets/perps-market/test/integration/Insolvent.test.ts +++ b/markets/perps-market/test/integration/Insolvent.test.ts @@ -4,6 +4,7 @@ import Wei, { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; +import { delegateCollateral } from '@synthetixio/main/test/common'; const _SECONDS_IN_DAY = 24 * 60 * 60; @@ -83,15 +84,15 @@ describe('Position - interest rates', () => { systems().CollateralMock.address ); // very low amount to make market insolvent - await systems() - .Core.connect(staker()) - .delegateCollateral( - 1, - 1, - systems().CollateralMock.address, - wei(currentCollateralAmount).mul(wei(0.1)).toBN(), - ethers.utils.parseEther('1') - ); + await delegateCollateral( + systems, + staker(), + 1, + 1, + systems().CollateralMock.address, + wei(currentCollateralAmount).mul(wei(0.1)).toBN(), + ethers.utils.parseEther('1') + ); }); // trader 1 diff --git a/markets/perps-market/test/integration/Position/InterestRate.test.ts b/markets/perps-market/test/integration/Position/InterestRate.test.ts index d12a1a162e..51a81b3746 100644 --- a/markets/perps-market/test/integration/Position/InterestRate.test.ts +++ b/markets/perps-market/test/integration/Position/InterestRate.test.ts @@ -5,6 +5,7 @@ import { ethers } from 'ethers'; import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; +import { delegateCollateral } from '@synthetixio/main/test/common'; const _SECONDS_IN_DAY = 24 * 60 * 60; const _SECONDS_IN_YEAR = 31557600; @@ -230,15 +231,15 @@ describe('Position - interest rates', () => { systems().CollateralMock.address ); // current assumption = 1000 collateral at $2000 price == $2M delegated collateral value - await systems() - .Core.connect(staker()) - .delegateCollateral( - 1, - 1, - systems().CollateralMock.address, - wei(currentCollateralAmount).mul(wei(0.9)).toBN(), - ethers.utils.parseEther('1') - ); + await delegateCollateral( + systems, + staker(), + 1, + 1, + systems().CollateralMock.address, + wei(currentCollateralAmount).mul(wei(0.9)).toBN(), + ethers.utils.parseEther('1') + ); }); let updateTxn: ethers.providers.TransactionResponse; diff --git a/protocol/synthetix/test/common/delegateCollateral.ts b/protocol/synthetix/test/common/delegateCollateral.ts new file mode 100644 index 0000000000..7fb549d2c4 --- /dev/null +++ b/protocol/synthetix/test/common/delegateCollateral.ts @@ -0,0 +1,93 @@ +import type { CoreProxy } from '../generated/typechain'; +import { BigNumber, ethers, Contract } from 'ethers'; + +type SystemArgs = { + Core: CoreProxy | Contract; +}; + +export async function expectedToDeltaDelegatedCollateral( + systems: () => SystemArgs, + accountId: number, + poolId: number, + collateralAddress: string, + fixedDepositAmount: BigNumber +): Promise { + const currentPositionCollateral = await systems().Core.getPositionCollateral( + accountId, + poolId, + collateralAddress + ); + const deltaCollateral = fixedDepositAmount.sub(currentPositionCollateral); + return deltaCollateral; +} + +export async function declareDelegateIntent( + systems: () => SystemArgs, + signer: ethers.Signer, + accountId: number, + poolId: number, + collateralAddress: string, + fixedDepositAmount: BigNumber, + leverage: BigNumber, + shouldCleanBefore: boolean = true +): Promise { + if (shouldCleanBefore) { + await systems().Core.connect(signer).deleteAllIntents(accountId); + } + const intentId = await systems() + .Core.connect(signer) + .callStatic.declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress, + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress, + fixedDepositAmount + ), + leverage + ); + await systems() + .Core.connect(signer) + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress, + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress, + fixedDepositAmount + ), + leverage + ); + return intentId; +} + +export async function delegateCollateral( + systems: () => SystemArgs, + signer: ethers.Signer, + accountId: number, + poolId: number, + collateralAddress: string, + fixedDepositAmount: BigNumber, + leverage: BigNumber, + shouldCleanBefore: boolean = true +): Promise { + const intentId = await declareDelegateIntent( + systems, + signer, + accountId, + poolId, + collateralAddress, + fixedDepositAmount, + leverage, + shouldCleanBefore + ); + await systems() + .Core.connect(signer) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); +} diff --git a/protocol/synthetix/test/common/index.ts b/protocol/synthetix/test/common/index.ts index 9abf62d221..916a7d3053 100644 --- a/protocol/synthetix/test/common/index.ts +++ b/protocol/synthetix/test/common/index.ts @@ -1,2 +1,3 @@ export * from './stakers'; export * from './stakedPool'; +export * from './delegateCollateral'; diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index d7983833bc..ac086435f3 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -7,6 +7,11 @@ import hre from 'hardhat'; import { bn, bootstrapWithStakedPool } from '../../bootstrap'; import Permissions from '../../mixins/AccountRBACMixin.permissions'; import { verifyUsesFeatureFlag } from '../../verifications'; +import { + delegateCollateral, + declareDelegateIntent, + expectedToDeltaDelegatedCollateral, +} from '../../../common'; // import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import { wei } from '@synthetixio/wei'; @@ -141,52 +146,6 @@ describe('VaultModule', function () { }; } - async function fixedToDeltaAmount( - accountId: number, - poolId: number, - collateralAddress: string, - fixedDepositAmount: BigNumber - ): Promise { - const currentPositionCollateral = await systems().Core.getPositionCollateral( - accountId, - poolId, - collateralAddress - ); - const deltaCollateral = fixedDepositAmount.sub(currentPositionCollateral); - return deltaCollateral; - } - - async function delegateCollateral( - signer: ethers.Signer, - accountId: number, - poolId: number, - collateralAddress: string, - fixedDepositAmount: BigNumber, - leverage: BigNumber - ): Promise { - const intentId = await systems() - .Core.connect(signer) - .callStatic.declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress, - await fixedToDeltaAmount(accountId, poolId, collateralAddress, fixedDepositAmount), - leverage - ); - await systems() - .Core.connect(signer) - .declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress, - await fixedToDeltaAmount(accountId, poolId, collateralAddress, fixedDepositAmount), - leverage - ); - await systems() - .Core.connect(signer) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]); - } - describe('fresh vault', async () => { const fakeFreshVaultId = 209372; @@ -283,7 +242,13 @@ describe('VaultModule', function () { accountId, 0, // 0 pool is just easy way to test another pool collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(51)), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.div(51) + ), ethers.utils.parseEther('1') ), 'InsufficientDelegation("20000000000000000000")', @@ -299,7 +264,13 @@ describe('VaultModule', function () { accountId, poolId, collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount + ), ethers.utils.parseEther('1') ), 'InvalidCollateralAmount()', @@ -315,7 +286,13 @@ describe('VaultModule', function () { accountId, 42, collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(50)), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.div(50) + ), ethers.utils.parseEther('1') ), 'PoolNotFound("42")', @@ -333,7 +310,13 @@ describe('VaultModule', function () { accountId, 42, collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.div(50)), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.div(50) + ), ethers.utils.parseEther('1') ) ); @@ -358,34 +341,15 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await systems() - .Core.connect(user1) - .callStatic.declareIntentToDelegateCollateral( - accountId, - fakeVaultId, - collateralAddress(), - await fixedToDeltaAmount( - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( - accountId, - fakeVaultId, - collateralAddress(), - await fixedToDeltaAmount( - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user1, + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); await assertRevert( systems() .Core.connect(user1) @@ -427,34 +391,15 @@ describe('VaultModule', function () { // fails when collateral is disabled for the pool by pool owner it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await systems() - .Core.connect(user1) - .callStatic.declareIntentToDelegateCollateral( - accountId, - fakeVaultId, - collateralAddress(), - await fixedToDeltaAmount( - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( - accountId, - fakeVaultId, - collateralAddress(), - await fixedToDeltaAmount( - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user1, + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); await assertRevert( systems() @@ -479,6 +424,7 @@ describe('VaultModule', function () { it('the delegation works as expected with the enabled collateral', async () => { await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( + systems, user1, accountId, fakeVaultId, @@ -500,24 +446,15 @@ describe('VaultModule', function () { }); it('fails when pool does not allow sufficient deposit amount', async () => { - const intentId = await systems() - .Core.connect(user1) - .callStatic.declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.mul(2)), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user1) - .declareIntentToDelegateCollateral( - accountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount(accountId, poolId, collateralAddress(), depositAmount.mul(2)), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); await assertRevert( systems() @@ -592,6 +529,7 @@ describe('VaultModule', function () { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -699,6 +637,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -722,6 +661,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -745,6 +685,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -760,24 +701,15 @@ describe('VaultModule', function () { const wanted = depositAmount.mul(3); const missing = wanted.sub(depositAmount.div(3)); - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount(user2AccountId, poolId, collateralAddress(), wanted), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount(user2AccountId, poolId, collateralAddress(), wanted), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user2, + user2AccountId, + poolId, + collateralAddress(), + wanted, + ethers.utils.parseEther('1') + ); await assertRevert( systems() @@ -802,24 +734,15 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - 0, - collateralAddress(), - await fixedToDeltaAmount(user2AccountId, 0, collateralAddress(), depositAmount), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - 0, - collateralAddress(), - await fixedToDeltaAmount(user2AccountId, 0, collateralAddress(), depositAmount), // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user2, + user2AccountId, + poolId, + collateralAddress(), + depositAmount, // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); await assertRevert( systems() @@ -835,6 +758,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -863,34 +787,15 @@ describe('VaultModule', function () { const deposit = depositAmount.div(50); const debt = depositAmount.div(100); - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(50) - ), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user2, + user2AccountId, + poolId, + collateralAddress(), + deposit, + ethers.utils.parseEther('1') + ); await assertRevert( systems() @@ -912,7 +817,8 @@ describe('VaultModule', function () { user2AccountId, poolId, collateralAddress(), - await fixedToDeltaAmount( + await expectedToDeltaDelegatedCollateral( + systems, user2AccountId, poolId, collateralAddress(), @@ -932,34 +838,16 @@ describe('VaultModule', function () { !(await systems().Core.connect(user2).callStatic.isMarketCapacityLocked(marketId)) ); - const intentId = await systems() - .Core.connect(user2) - .callStatic.declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(10) - ), - ethers.utils.parseEther('1') - ); - await systems() - .Core.connect(user2) - .declareIntentToDelegateCollateral( - user2AccountId, - poolId, - collateralAddress(), - await fixedToDeltaAmount( - user2AccountId, - poolId, - collateralAddress(), - depositAmount.div(10) - ), - ethers.utils.parseEther('1') - ); + const intentId = await declareDelegateIntent( + systems, + user2, + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); + await assertRevert( systems() .Core.connect(user2) @@ -989,6 +877,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -1033,6 +922,7 @@ describe('VaultModule', function () { before('delegate', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -1051,6 +941,7 @@ describe('VaultModule', function () { it('lets user2 re-stake again', async () => { await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( + systems, user2, user2AccountId, poolId, @@ -1072,6 +963,7 @@ describe('VaultModule', function () { before('undelegate', async () => { await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( + systems, user1, accountId, poolId, From 6ae5345edd8179f70d05ee745dc8c4ca0e623c61 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 16 May 2024 12:34:19 -0300 Subject: [PATCH 16/50] fix another tests on bfp --- .../integration/modules/OrderModule.test.ts | 7 +++-- ...erpMarketFactoryModule.utilisation.test.ts | 29 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/markets/bfp-market/test/integration/modules/OrderModule.test.ts b/markets/bfp-market/test/integration/modules/OrderModule.test.ts index 4af7ed007f..933bedf18c 100644 --- a/markets/bfp-market/test/integration/modules/OrderModule.test.ts +++ b/markets/bfp-market/test/integration/modules/OrderModule.test.ts @@ -37,6 +37,7 @@ import { import { ethers } from 'ethers'; import { calcFillPrice } from '../../calculations'; import { shuffle } from 'lodash'; +import { delegateCollateral } from '@synthetixio/main/test/common'; describe('OrderModule', () => { const bs = bootstrap(genBootstrap()); @@ -1372,7 +1373,7 @@ describe('OrderModule', () => { }); it('should accurately account for utilization when holding for a long time', async () => { - const { BfpMarketProxy, Core } = systems(); + const { BfpMarketProxy } = systems(); const { collateral, collateralDepositAmount, trader, marketId, market } = await depositMargin( bs, @@ -1402,7 +1403,9 @@ describe('OrderModule', () => { const { stakedAmount, staker, stakerAccountId, collateral: stakedCollateral, id } = pool(); // Unstake 50% of the delegated amount on the core side, this should lead to a increased utilization rate. const newDelegated = wei(stakedAmount).mul(0.5).toBN(); - await Core.connect(staker()).delegateCollateral( + await delegateCollateral( + systems, + staker(), stakerAccountId, id, stakedCollateral().address, diff --git a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts index 5926149571..22b5a44f95 100644 --- a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts +++ b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts @@ -23,6 +23,7 @@ import { withExplicitEvmMine, } from '../../helpers'; import { calcUtilization, calcUtilizationRate } from '../../calculations'; +import { delegateCollateral } from '@synthetixio/main/test/common'; describe('PerpMarketFactoryModule Utilization', () => { const bs = bootstrap(genBootstrap()); @@ -152,7 +153,9 @@ describe('PerpMarketFactoryModule Utilization', () => { stakedCollateral().address ); const stakedCollateralAddress = stakedCollateral().address; - await Core.connect(staker()).delegateCollateral( + await delegateCollateral( + systems, + staker(), stakerAccountId, poolId, stakedCollateralAddress, @@ -222,7 +225,9 @@ describe('PerpMarketFactoryModule Utilization', () => { stakedCollateral().address ); const stakedCollateralAddress = stakedCollateral().address; - await Core.connect(staker()).delegateCollateral( + await delegateCollateral( + systems, + staker(), stakerAccountId, poolId, stakedCollateralAddress, @@ -377,7 +382,7 @@ describe('PerpMarketFactoryModule Utilization', () => { }); describe('getUtilizationDigest', async () => { it('should return utilization data', async () => { - const { BfpMarketProxy, Core } = systems(); + const { BfpMarketProxy } = systems(); const market = genOneOf(markets()); const { marketId, trader, collateral, collateralDepositAmount } = await depositMargin( @@ -412,16 +417,14 @@ describe('PerpMarketFactoryModule Utilization', () => { await fastForwardBySec(provider(), genNumber(1, SECONDS_ONE_DAY)); // Decrease amount of staked collateral on the core side. const stakedCollateralAddress = stakedCollateral().address; - await withExplicitEvmMine( - () => - Core.connect(staker()).delegateCollateral( - stakerAccountId, - poolId, - stakedCollateralAddress, - wei(stakedAmount).mul(0.9).toBN(), - bn(1) - ), - provider() + await delegateCollateral( + systems, + staker(), + stakerAccountId, + poolId, + stakedCollateralAddress, + wei(stakedAmount).mul(0.9).toBN(), + bn(1) ); const utilizationDigest2 = await BfpMarketProxy.getUtilizationDigest(marketId); From 8289f1169b17df641725ed8e837138ec91e14505 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Fri, 17 May 2024 11:25:39 -0300 Subject: [PATCH 17/50] Only owner can delete all intents --- .../integration/modules/OrderModule.test.ts | 2 + ...erpMarketFactoryModule.utilisation.test.ts | 3 + .../LegacyMarket.iosiroInfiniteMoney.ts | 2 + .../test/integration/Insolvent.test.ts | 3 +- .../integration/Position/InterestRate.test.ts | 60 +++++++++++-------- .../contracts/interfaces/IVaultModule.sol | 13 +++- .../contracts/modules/core/VaultModule.sol | 32 +++++++++- .../storage/AccountDelegationIntents.sol | 16 +++++ .../contracts/storage/DelegationIntent.sol | 8 +-- .../test/common/delegateCollateral.ts | 5 +- .../modules/core/VaultModule.test.ts | 30 ++++++---- 11 files changed, 126 insertions(+), 48 deletions(-) diff --git a/markets/bfp-market/test/integration/modules/OrderModule.test.ts b/markets/bfp-market/test/integration/modules/OrderModule.test.ts index 933bedf18c..e79c436a17 100644 --- a/markets/bfp-market/test/integration/modules/OrderModule.test.ts +++ b/markets/bfp-market/test/integration/modules/OrderModule.test.ts @@ -45,6 +45,7 @@ describe('OrderModule', () => { systems, restore, provider, + owner, keeper, collateralsWithoutSusd, markets, @@ -1405,6 +1406,7 @@ describe('OrderModule', () => { const newDelegated = wei(stakedAmount).mul(0.5).toBN(); await delegateCollateral( systems, + owner(), staker(), stakerAccountId, id, diff --git a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts index 22b5a44f95..6332fe09c9 100644 --- a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts +++ b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts @@ -155,6 +155,7 @@ describe('PerpMarketFactoryModule Utilization', () => { const stakedCollateralAddress = stakedCollateral().address; await delegateCollateral( systems, + owner(), staker(), stakerAccountId, poolId, @@ -227,6 +228,7 @@ describe('PerpMarketFactoryModule Utilization', () => { const stakedCollateralAddress = stakedCollateral().address; await delegateCollateral( systems, + owner(), staker(), stakerAccountId, poolId, @@ -419,6 +421,7 @@ describe('PerpMarketFactoryModule Utilization', () => { const stakedCollateralAddress = stakedCollateral().address; await delegateCollateral( systems, + owner(), staker(), stakerAccountId, poolId, diff --git a/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts b/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts index 3c56c17640..bb56e2261e 100644 --- a/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts +++ b/markets/legacy-market/test/integration/LegacyMarket.iosiroInfiniteMoney.ts @@ -279,6 +279,7 @@ describe('LegacyMarket (iosiro)', function () { () => ({ Core: v3System, }), + owner, whaleAccount, whaleAccountId, otherPoolId, @@ -332,6 +333,7 @@ describe('LegacyMarket (iosiro)', function () { () => ({ Core: v3System, }), + owner, attacker, accountID, poolId, diff --git a/markets/perps-market/test/integration/Insolvent.test.ts b/markets/perps-market/test/integration/Insolvent.test.ts index ad7352bd64..60e3971e69 100644 --- a/markets/perps-market/test/integration/Insolvent.test.ts +++ b/markets/perps-market/test/integration/Insolvent.test.ts @@ -19,7 +19,7 @@ const interestRateParams = { }; describe('Position - interest rates', () => { - const { systems, perpsMarkets, superMarketId, provider, trader1, keeper, staker } = + const { systems, perpsMarkets, superMarketId, provider, owner, trader1, keeper, staker } = bootstrapMarkets({ interestRateParams: { lowUtilGradient: interestRateParams.lowUtilGradient.toBN(), @@ -86,6 +86,7 @@ describe('Position - interest rates', () => { // very low amount to make market insolvent await delegateCollateral( systems, + owner(), staker(), 1, 1, diff --git a/markets/perps-market/test/integration/Position/InterestRate.test.ts b/markets/perps-market/test/integration/Position/InterestRate.test.ts index 51a81b3746..5e8c83d35c 100644 --- a/markets/perps-market/test/integration/Position/InterestRate.test.ts +++ b/markets/perps-market/test/integration/Position/InterestRate.test.ts @@ -27,32 +27,41 @@ const interestRateParams = { const proportionalTime = (seconds: number) => wei(seconds).div(_SECONDS_IN_YEAR); describe('Position - interest rates', () => { - const { systems, perpsMarkets, superMarketId, provider, trader1, trader2, keeper, staker } = - bootstrapMarkets({ - interestRateParams: { - lowUtilGradient: interestRateParams.lowUtilGradient.toBN(), - gradientBreakpoint: interestRateParams.gradientBreakpoint.toBN(), - highUtilGradient: interestRateParams.highUtilGradient.toBN(), + const { + systems, + perpsMarkets, + superMarketId, + provider, + owner, + trader1, + trader2, + keeper, + staker, + } = bootstrapMarkets({ + interestRateParams: { + lowUtilGradient: interestRateParams.lowUtilGradient.toBN(), + gradientBreakpoint: interestRateParams.gradientBreakpoint.toBN(), + highUtilGradient: interestRateParams.highUtilGradient.toBN(), + }, + synthMarkets: [], + perpsMarkets: [ + { + lockedOiRatioD18: _ETH_LOCKED_OI_RATIO.toBN(), + requestedMarketId: 25, + name: 'Ether', + token: 'snxETH', + price: _ETH_PRICE.toBN(), }, - synthMarkets: [], - perpsMarkets: [ - { - lockedOiRatioD18: _ETH_LOCKED_OI_RATIO.toBN(), - requestedMarketId: 25, - name: 'Ether', - token: 'snxETH', - price: _ETH_PRICE.toBN(), - }, - { - lockedOiRatioD18: _BTC_LOCKED_OI_RATIO.toBN(), - requestedMarketId: 50, - name: 'Bitcoin', - token: 'snxBTC', - price: _BTC_PRICE.toBN(), - }, - ], - traderAccountIds: [2, 3], - }); + { + lockedOiRatioD18: _BTC_LOCKED_OI_RATIO.toBN(), + requestedMarketId: 50, + name: 'Bitcoin', + token: 'snxBTC', + price: _BTC_PRICE.toBN(), + }, + ], + traderAccountIds: [2, 3], + }); let ethMarket: PerpsMarket, btcMarket: PerpsMarket; @@ -233,6 +242,7 @@ describe('Position - interest rates', () => { // current assumption = 1000 collateral at $2000 price == $2M delegated collateral value await delegateCollateral( systems, + owner(), staker(), 1, 1, diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index eaae6d1f71..cc4bac47c4 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -27,6 +27,11 @@ interface IVaultModule { */ error InvalidDelegationIntent(); + /** + * @notice Thrown when the specified intent is not related to the account id. + */ + error DelegationIntentNotExpired(uint256 intentId); + /** * @notice Thrown when the specified intent is not executable due to pending intents. */ @@ -196,7 +201,13 @@ interface IVaultModule { */ function processIntentToDelegateCollateralByPair(uint128 accountId, uint128 poolId) external; - function deleteAllIntents(uint128 accountId) external; + function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external; + + function deleteAllExpiredIntents(uint128 accountId) external; + + function forceDeleteIntents(uint128 accountId, uint256[] calldata intentIds) external; + + function forceDeleteAllAccountIntents(uint128 accountId) external; /** * @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index ece0746a15..a374d2e662 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -1,6 +1,7 @@ //SPDX-License-Identifier: MIT pragma solidity >=0.8.11 <0.9.0; +import "@synthetixio/core-contracts/contracts/ownership/OwnableStorage.sol"; import "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol"; import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; import "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; @@ -164,7 +165,7 @@ contract VaultModule is IVaultModule { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (!intent.isExecutable()) { // Remove the intent. - if (intent.windowIsClosed()) { + if (intent.intentExpired()) { AccountDelegationIntents.getValid(accountId).removeIntent(intent); emit DelegationIntentRemoved( intent.id, @@ -233,11 +234,38 @@ contract VaultModule is IVaultModule { ); } - function deleteAllIntents(uint128 accountId) external override { + function forceDeleteAllAccountIntents(uint128 accountId) external override { + OwnableStorage.onlyOwner(); + AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + } + + function forceDeleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { + OwnableStorage.onlyOwner(); + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + AccountDelegationIntents.getValid(accountId).removeIntent(intent); + } + } + + function deleteAllExpiredIntents(uint128 accountId) external override { Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); AccountDelegationIntents.getValid(accountId).cleanAllIntents(); } + function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { + Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (intent.accountId != accountId) { + revert InvalidDelegationIntent(); + } + if (!intent.intentExpired()) { + revert DelegationIntentNotExpired(intent.id); + } + AccountDelegationIntents.getValid(accountId).removeIntent(intent); + } + } + /** * @inheritdoc IVaultModule */ diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index d379cad7ad..1d11b93106 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -14,6 +14,7 @@ library AccountDelegationIntents { using SafeCastU256 for uint256; using SetUtil for SetUtil.UintSet; using SetUtil for SetUtil.AddressSet; + using DelegationIntent for DelegationIntent.Data; error InvalidAccountDelegationIntents(); @@ -159,6 +160,20 @@ library AccountDelegationIntents { return self.intentsByPair[keccak256(abi.encodePacked(poolId, accountId))].values(); } + /** + * @dev Cleans all intents related to the account. This should be called upon liquidation. + */ + function cleanAllExpiredIntents(Data storage self) internal { + uint256[] memory intentIds = self.intentsId.values(); + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (intent.intentExpired()) { + removeIntent(self, intent); + } + removeIntent(self, intent); + } + } + /** * @dev Cleans all intents related to the account. This should be called upon liquidation. */ @@ -169,6 +184,7 @@ library AccountDelegationIntents { removeIntent(self, intent); } + // Sanity clean all the cached values self.netAcountCachedDelegatedCollateral = 0; self.delegateAcountCachedCollateral = 0; self.undelegateAcountCachedCollateral = 0; diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 9dc006d565..aaed80012d 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -122,11 +122,7 @@ library DelegationIntent { block.timestamp >= self.processingStartTime && block.timestamp < self.processingEndTime; } - function windowIsOpened(Data storage self) internal view returns (bool) { - return block.timestamp >= self.processingStartTime; - } - - function windowIsClosed(Data storage self) internal view returns (bool) { - return block.timestamp < self.processingEndTime; + function intentExpired(Data storage self) internal view returns (bool) { + return block.timestamp >= self.processingEndTime; } } diff --git a/protocol/synthetix/test/common/delegateCollateral.ts b/protocol/synthetix/test/common/delegateCollateral.ts index 7fb549d2c4..c954bc06fb 100644 --- a/protocol/synthetix/test/common/delegateCollateral.ts +++ b/protocol/synthetix/test/common/delegateCollateral.ts @@ -23,6 +23,7 @@ export async function expectedToDeltaDelegatedCollateral( export async function declareDelegateIntent( systems: () => SystemArgs, + owner: ethers.Signer, signer: ethers.Signer, accountId: number, poolId: number, @@ -32,7 +33,7 @@ export async function declareDelegateIntent( shouldCleanBefore: boolean = true ): Promise { if (shouldCleanBefore) { - await systems().Core.connect(signer).deleteAllIntents(accountId); + await systems().Core.connect(owner).forceDeleteAllAccountIntents(accountId); } const intentId = await systems() .Core.connect(signer) @@ -69,6 +70,7 @@ export async function declareDelegateIntent( export async function delegateCollateral( systems: () => SystemArgs, + owner: ethers.Signer, signer: ethers.Signer, accountId: number, poolId: number, @@ -79,6 +81,7 @@ export async function delegateCollateral( ): Promise { const intentId = await declareDelegateIntent( systems, + owner, signer, accountId, poolId, diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index ac086435f3..040cbfdd92 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -343,6 +343,7 @@ describe('VaultModule', function () { it('fails when trying to open delegation position with disabled collateral', async () => { const intentId = await declareDelegateIntent( systems, + owner, user1, accountId, fakeVaultId, @@ -393,6 +394,7 @@ describe('VaultModule', function () { it('fails when trying to open delegation position with disabled collateral', async () => { const intentId = await declareDelegateIntent( systems, + owner, user1, accountId, fakeVaultId, @@ -422,9 +424,9 @@ describe('VaultModule', function () { }); it('the delegation works as expected with the enabled collateral', async () => { - await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( systems, + owner, user1, accountId, fakeVaultId, @@ -448,6 +450,7 @@ describe('VaultModule', function () { it('fails when pool does not allow sufficient deposit amount', async () => { const intentId = await declareDelegateIntent( systems, + owner, user1, accountId, poolId, @@ -527,9 +530,9 @@ describe('VaultModule', function () { .Core.connect(user2) .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -635,9 +638,9 @@ describe('VaultModule', function () { // than 1 are allowed (which might be something we want to do) describe.skip('increase exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -659,9 +662,9 @@ describe('VaultModule', function () { describe.skip('reduce exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -683,9 +686,9 @@ describe('VaultModule', function () { describe('remove exposure', async () => { before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -703,6 +706,7 @@ describe('VaultModule', function () { const intentId = await declareDelegateIntent( systems, + owner, user2, user2AccountId, poolId, @@ -736,6 +740,7 @@ describe('VaultModule', function () { it('fails when trying to open delegation position with disabled collateral', async () => { const intentId = await declareDelegateIntent( systems, + owner, user2, user2AccountId, poolId, @@ -756,9 +761,9 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -789,6 +794,7 @@ describe('VaultModule', function () { const intentId = await declareDelegateIntent( systems, + owner, user2, user2AccountId, poolId, @@ -809,7 +815,7 @@ describe('VaultModule', function () { }); it('fails when reducing to below minDelegation amount', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); + await systems().Core.connect(owner).forceDeleteAllAccountIntents(user2AccountId); await assertRevert( systems() .Core.connect(user2) @@ -833,13 +839,13 @@ describe('VaultModule', function () { it('fails when market becomes capacity locked', async () => { // sanity - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); assert.ok( !(await systems().Core.connect(user2).callStatic.isMarketCapacityLocked(marketId)) ); const intentId = await declareDelegateIntent( systems, + owner, user2, user2AccountId, poolId, @@ -875,9 +881,9 @@ describe('VaultModule', function () { describe('success', () => { before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -920,9 +926,9 @@ describe('VaultModule', function () { }); before('delegate', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -939,9 +945,9 @@ describe('VaultModule', function () { it('user2 position is closed', verifyAccountState(user2AccountId, poolId, 0, 0)); it('lets user2 re-stake again', async () => { - await systems().Core.connect(user2).deleteAllIntents(user2AccountId); await delegateCollateral( systems, + owner, user2, user2AccountId, poolId, @@ -961,9 +967,9 @@ describe('VaultModule', function () { }); before('undelegate', async () => { - await systems().Core.connect(user1).deleteAllIntents(accountId); await delegateCollateral( systems, + owner, user1, accountId, poolId, From c6b5332b8d5ce48c88fa823254a841fe5783c5ef Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 20 May 2024 11:05:16 -0300 Subject: [PATCH 18/50] Make window time flexible with configuration changes --- .../contracts/modules/core/PoolModule.sol | 3 - .../contracts/modules/core/VaultModule.sol | 21 ++--- .../storage/AccountDelegationIntents.sol | 40 ++++----- .../contracts/storage/DelegationIntent.sol | 84 ++++++++++++++----- protocol/synthetix/contracts/storage/Pool.sol | 53 ------------ 5 files changed, 86 insertions(+), 115 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/PoolModule.sol b/protocol/synthetix/contracts/modules/core/PoolModule.sol index 032b618403..851fb3e061 100644 --- a/protocol/synthetix/contracts/modules/core/PoolModule.sol +++ b/protocol/synthetix/contracts/modules/core/PoolModule.sol @@ -148,9 +148,6 @@ contract PoolModule is IPoolModule { ) external override { Pool.Data storage pool = Pool.loadExisting(poolId); Pool.onlyPoolOwner(poolId, ERC2771Context._msgSender()); - // TODO LJM - - // pool.requireMinDelegationTimeElapsed(pool.lastConfigurationTime); // Update each market's pro-rata liquidity and collect accumulated debt into the pool's debt distribution. // Note: This follows the same pattern as Pool.recalculateVaultCollateral(), diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index a374d2e662..713feafa98 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -113,15 +113,6 @@ contract VaultModule is IVaultModule { ); } - // Prepare data for storing the new intent. - (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool - .loadExisting(poolId) - .getRequiredDelegationDelayAndWindow( - deltaCollateralAmountD18 + - accountIntents.netDelegatedCollateralAmountPerPool[poolId] > - 0 - ); - // Create a new delegation intent. intentId = DelegationIntent.nextId(); DelegationIntent.Data storage intent = DelegationIntent.load(intentId); @@ -129,11 +120,9 @@ contract VaultModule is IVaultModule { intent.accountId = accountId; intent.poolId = poolId; intent.collateralType = collateralType; - intent.collateralDeltaAmountD18 = deltaCollateralAmountD18; + intent.deltaCollateralAmountD18 = deltaCollateralAmountD18; intent.leverage = leverage; intent.declarationTime = block.timestamp.to32(); - intent.processingStartTime = intent.declarationTime + requiredDelayTime; - intent.processingEndTime = intent.processingStartTime + requiredWindowTime; // Add intent to the account's delegation intents. AccountDelegationIntents.getValid(intent.accountId).addIntent(intent); @@ -147,8 +136,8 @@ contract VaultModule is IVaultModule { leverage, intentId, intent.declarationTime, - intent.processingStartTime, - intent.processingEndTime, + intent.processingStartTime(), + intent.processingEndTime(), ERC2771Context._msgSender() ); } @@ -198,7 +187,7 @@ contract VaultModule is IVaultModule { accountId, intent.poolId, intent.collateralType, - intent.collateralDeltaAmountD18, + intent.deltaCollateralAmountD18, intent.leverage ); @@ -455,7 +444,7 @@ contract VaultModule is IVaultModule { collateralType, newCollateralAmountD18, leverage, - ERC2771Context._msgSender() // TODO LJM this is the executor address, not the account owner or authorized (the one that posted the intent) + ERC2771Context._msgSender() // this is the executor address, not the account owner or authorized (the one that posted the intent) ); vault.updateRewards( diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 1d11b93106..80b354fbcf 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -72,29 +72,29 @@ library AccountDelegationIntents { ] .add(delegationIntent.id); - if (delegationIntent.collateralDeltaAmountD18 >= 0) { + if (delegationIntent.deltaCollateralAmountD18 >= 0) { self.delegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); self.delegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); self.delegateAcountCachedCollateral += delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); } else { self.undelegatedAmountPerCollateral[ delegationIntent.collateralType - ] += (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); + ] += (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] += (delegationIntent - .collateralDeltaAmountD18 * -1).toUint(); - self.undelegateAcountCachedCollateral += (delegationIntent.collateralDeltaAmountD18 * + .deltaCollateralAmountD18 * -1).toUint(); + self.undelegateAcountCachedCollateral += (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); } self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent - .collateralDeltaAmountD18; + .deltaCollateralAmountD18; self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent - .collateralDeltaAmountD18; + .deltaCollateralAmountD18; if (!self.delegatedPools.contains(delegationIntent.poolId)) { self.delegatedPools.add(delegationIntent.poolId); @@ -104,7 +104,7 @@ library AccountDelegationIntents { self.delegatedCollaterals.add(delegationIntent.collateralType); } - self.netAcountCachedDelegatedCollateral += delegationIntent.collateralDeltaAmountD18; + self.netAcountCachedDelegatedCollateral += delegationIntent.deltaCollateralAmountD18; } function removeIntent( @@ -122,31 +122,31 @@ library AccountDelegationIntents { ] .remove(delegationIntent.id); - if (delegationIntent.collateralDeltaAmountD18 >= 0) { + if (delegationIntent.deltaCollateralAmountD18 >= 0) { self.delegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); self.delegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); self.delegateAcountCachedCollateral -= delegationIntent - .collateralDeltaAmountD18 + .deltaCollateralAmountD18 .toUint(); } else { self.undelegatedAmountPerCollateral[ delegationIntent.collateralType - ] -= (delegationIntent.collateralDeltaAmountD18 * -1).toUint(); + ] -= (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] -= (delegationIntent - .collateralDeltaAmountD18 * -1).toUint(); - self.undelegateAcountCachedCollateral -= (delegationIntent.collateralDeltaAmountD18 * + .deltaCollateralAmountD18 * -1).toUint(); + self.undelegateAcountCachedCollateral -= (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); } self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent - .collateralDeltaAmountD18; + .deltaCollateralAmountD18; self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent - .collateralDeltaAmountD18; + .deltaCollateralAmountD18; - self.netAcountCachedDelegatedCollateral -= delegationIntent.collateralDeltaAmountD18; + self.netAcountCachedDelegatedCollateral -= delegationIntent.deltaCollateralAmountD18; } /** diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index aaed80012d..2b2dfcb1c7 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -2,11 +2,14 @@ pragma solidity >=0.8.11 <0.9.0; import "./Config.sol"; +import "./Pool.sol"; /** * @title Represents a delegation (or undelegation) intent. */ library DelegationIntent { + using Pool for Pool.Data; + error InvalidDelegationIntentId(); error DelegationIntentNotReady(uint32 declarationTime, uint32 processingStartTime); error DelegationIntentExpired(uint32 declarationTime, uint32 processingEndTime); @@ -51,7 +54,7 @@ library DelegationIntent { * outstanding intent to delegate/undelegate to the pool, * denominated with 18 decimals of precision */ - int256 collateralDeltaAmountD18; + int256 deltaCollateralAmountD18; /** * @notice The intended amount of leverage associated with the new * amount of collateral that the account has an outstanding intent @@ -63,21 +66,6 @@ library DelegationIntent { * @notice The timestamp at which the intent was declared */ uint32 declarationTime; - /** - * @notice The timestamp before which the intent cannot be processed - * @dev dependent on - * {MarketManagerModule.undelegateCollateralDelay} or - * {MarketManagerModule.delegateCollateralDelay} - */ - uint32 processingStartTime; - /** - * @notice The timestamp after which the intent cannot be processed - * (i.e., intent expiry) - * @dev dependent on - * {MarketManagerModule.undelegateCollateralWindow} or - * {MarketManagerModule.delegateCollateralWindow} - */ - uint32 processingEndTime; } /** @@ -110,19 +98,69 @@ library DelegationIntent { Config.put(_ATOMIC_VALUE_LATEST_ID, bytes32(id)); } + function processingStartTime(Data storage self) internal view returns (uint32) { + (uint32 requiredDelayTime, ) = Pool + .loadExisting(self.poolId) + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + return self.declarationTime + requiredDelayTime; + } + + function processingEndTime(Data storage self) internal view returns (uint32) { + (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool + .loadExisting(self.poolId) + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + + // Apply default (forever) window time if not set + if (requiredWindowTime == 0) { + requiredWindowTime = 86400 * 360; // 1 year + } + + return self.declarationTime + requiredDelayTime + requiredWindowTime; + } + function checkIsExecutable(Data storage self) internal view { - if (block.timestamp < self.processingStartTime) - revert DelegationIntentNotReady(self.declarationTime, self.processingStartTime); - if (block.timestamp >= self.processingEndTime) - revert DelegationIntentExpired(self.declarationTime, self.processingEndTime); + (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool + .loadExisting(self.poolId) + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + + // Apply default (forever) window time if not set + if (requiredWindowTime == 0) { + requiredWindowTime = 86400 * 360; // 1 year + } + + uint32 _processingStartTime = self.declarationTime + requiredDelayTime; + uint32 _processingEndTime = _processingStartTime + requiredWindowTime; + + if (block.timestamp < _processingStartTime) + revert DelegationIntentNotReady(self.declarationTime, _processingStartTime); + if (block.timestamp >= _processingEndTime) + revert DelegationIntentExpired(self.declarationTime, _processingEndTime); } function isExecutable(Data storage self) internal view returns (bool) { - return - block.timestamp >= self.processingStartTime && block.timestamp < self.processingEndTime; + (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool + .loadExisting(self.poolId) + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + + // Apply default (forever) window time if not set + if (requiredWindowTime == 0) { + requiredWindowTime = 86400 * 360; // 1 year + } + + uint32 _processingStartTime = self.declarationTime + requiredDelayTime; + uint32 _processingEndTime = _processingStartTime + requiredWindowTime; + + return block.timestamp >= _processingStartTime && block.timestamp < _processingEndTime; } function intentExpired(Data storage self) internal view returns (bool) { - return block.timestamp >= self.processingEndTime; + (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool + .loadExisting(self.poolId) + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + + // Note: here we don't apply the forever defaul if window time is not set to allow the intent to expire. If it's zero it means is not configured, then it can expire immediately. + + uint32 _processingEndTime = self.declarationTime + requiredDelayTime + requiredWindowTime; + return block.timestamp >= _processingEndTime; } } diff --git a/protocol/synthetix/contracts/storage/Pool.sol b/protocol/synthetix/contracts/storage/Pool.sol index 868fbf26b4..ae9e46a4e6 100644 --- a/protocol/synthetix/contracts/storage/Pool.sol +++ b/protocol/synthetix/contracts/storage/Pool.sol @@ -436,46 +436,8 @@ library Pool { : market.delegateCollateralWindow; } } - - if (requiredWindowTime == 0) { - requiredWindowTime = 86400 * 360; // 1 year - } - - // TODO use global max delay and window ?? - // // solhint-disable-next-line numcast/safe-cast - // uint32 maxMinDelegateTime = uint32( - // Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) - // ); - // return - // maxMinDelegateTime < requiredMinDelegateTime - // ? maxMinDelegateTime - // : requiredMinDelegateTime; } - // TODO LJM - // function getRequiredMinDelegationTime( - // Data storage self - // ) internal view returns (uint32 requiredMinDelegateTime) { - // for (uint256 i = 0; i < self.marketConfigurations.length; i++) { - // uint32 marketMinDelegateTime = Market - // .load(self.marketConfigurations[i].marketId) - // .minDelegateTime; - - // if (marketMinDelegateTime > requiredMinDelegateTime) { - // requiredMinDelegateTime = marketMinDelegateTime; - // } - // } - - // // solhint-disable-next-line numcast/safe-cast - // uint32 maxMinDelegateTime = uint32( - // Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) - // ); - // return - // maxMinDelegateTime < requiredMinDelegateTime - // ? maxMinDelegateTime - // : requiredMinDelegateTime; - // } - /** * @dev Returns the debt of the vault that tracks the given collateral type. * @@ -553,21 +515,6 @@ library Pool { } } - // TODO LJM - // function requireMinDelegationTimeElapsed( - // Data storage self, - // uint64 lastDelegationTime - // ) internal view { - // uint32 requiredMinDelegationTime = getRequiredMinDelegationTime(self); - // if (block.timestamp < lastDelegationTime + requiredMinDelegationTime) { - // revert MinDelegationTimeoutPending( - // self.id, - // // solhint-disable-next-line numcast/safe-cast - // uint32(lastDelegationTime + requiredMinDelegationTime - block.timestamp) - // ); - // } - // } - function checkPoolCollateralLimit( Data storage self, address collateralType, From 55cf9242a972588fdbac3d76e56167d4ca07fee7 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 20 May 2024 15:18:43 -0300 Subject: [PATCH 19/50] wip: timing tests --- .../contracts/storage/DelegationIntent.sol | 8 +- .../modules/core/VaultModule.test.ts | 59 ----- .../core/VaultModuleDelegationTiming.test.ts | 244 ++++++++++++++++++ 3 files changed, 248 insertions(+), 63 deletions(-) create mode 100644 protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 2b2dfcb1c7..e7c88a126f 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -101,14 +101,14 @@ library DelegationIntent { function processingStartTime(Data storage self) internal view returns (uint32) { (uint32 requiredDelayTime, ) = Pool .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); return self.declarationTime + requiredDelayTime; } function processingEndTime(Data storage self) internal view returns (uint32) { (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); // Apply default (forever) window time if not set if (requiredWindowTime == 0) { @@ -121,7 +121,7 @@ library DelegationIntent { function checkIsExecutable(Data storage self) internal view { (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); // Apply default (forever) window time if not set if (requiredWindowTime == 0) { @@ -156,7 +156,7 @@ library DelegationIntent { function intentExpired(Data storage self) internal view returns (bool) { (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); // Note: here we don't apply the forever defaul if window time is not set to allow the intent to expire. If it's zero it means is not configured, then it can expire immediately. diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index 040cbfdd92..5275fd522b 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -575,65 +575,6 @@ describe('VaultModule', function () { verifyAccountState(user2AccountId, poolId, depositAmount.div(3), depositAmount.div(100)) ); - // describe('if one of the markets has a min delegation time', () => { - // const restore = snapshotCheckpoint(provider); - - // before('set market min delegation time to something high', async () => { - // await MockMarket.setMinDelegationTime(86400); - // }); - - // describe('without time passing', async () => { - // it('fails when min delegation timeout not elapsed', async () => { - // await assertRevert( - // systems().Core.connect(user2).delegateCollateral( - // user2AccountId, - // poolId, - // collateralAddress(), - // depositAmount.div(4), // user1 50%, user2 50% - // ethers.utils.parseEther('1') - // ), - // `MinDelegationTimeoutPending("${poolId}",`, - // systems().Core - // ); - // }); - - // it('can increase delegation without waiting', async () => { - // await systems() - // .Core.connect(user2) - // .delegateCollateral( - // user2AccountId, - // poolId, - // collateralAddress(), - // depositAmount.mul(2), - // ethers.utils.parseEther('1') - // ); - // }); - - // after(restore); - // }); - - // describe('after time passes', () => { - // before('fast forward', async () => { - // // for some reason `fastForward` doesn't seem to work with anvil - // await fastForwardTo((await getTime(provider())) + 86400, provider()); - // }); - - // it('works', async () => { - // await systems() - // .Core.connect(user2) - // .delegateCollateral( - // user2AccountId, - // poolId, - // collateralAddress(), - // depositAmount.div(2), - // ethers.utils.parseEther('1') - // ); - // }); - // }); - - // after(restore); - // }); - // these exposure tests should be enabled when exposures other // than 1 are allowed (which might be something we want to do) describe.skip('increase exposure', async () => { diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts new file mode 100644 index 0000000000..d51cff1c07 --- /dev/null +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -0,0 +1,244 @@ +import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; +import assert from 'assert/strict'; +import { BigNumber, ethers } from 'ethers'; +import hre from 'hardhat'; +import { bn, bootstrapWithStakedPool } from '../../bootstrap'; +import { delegateCollateral, declareDelegateIntent } from '../../../common'; +import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; + +describe('VaultModule Two-step Delegation', function () { + const { + signers, + systems, + provider, + accountId, + poolId, + depositAmount, + // collateralContract, + collateralAddress, + oracleNodeId, + } = bootstrapWithStakedPool(); + + // const MAX_UINT = ethers.constants.MaxUint256; + + let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer; + + let MockMarket: ethers.Contract; + let marketId: BigNumber; + + before('identify signers', async () => { + [owner, user1, user2] = signers(); + }); + + before('give user1 permission to register market', async () => { + await systems() + .Core.connect(owner) + .addToFeatureFlagAllowlist( + ethers.utils.formatBytes32String('registerMarket'), + await user1.getAddress() + ); + }); + + before('deploy and connect fake market', async () => { + const factory = await hre.ethers.getContractFactory('MockMarket'); + + MockMarket = await factory.connect(owner).deploy(); + + marketId = await systems().Core.connect(user1).callStatic.registerMarket(MockMarket.address); + + await systems().Core.connect(user1).registerMarket(MockMarket.address); + + await MockMarket.connect(owner).initialize( + systems().Core.address, + marketId, + ethers.utils.parseEther('1') + ); + + await systems() + .Core.connect(owner) + .setPoolConfiguration(poolId, [ + { + marketId: marketId, + weightD18: ethers.utils.parseEther('1'), + maxDebtShareValueD18: ethers.utils.parseEther('10000000000000000'), + }, + ]); + }); + + before('add second collateral type', async () => { + // add collateral + await ( + await systems().Core.connect(owner).configureCollateral({ + tokenAddress: systems().Collateral2Mock.address, + oracleNodeId: oracleNodeId(), + issuanceRatioD18: '5000000000000000000', + liquidationRatioD18: '1500000000000000000', + liquidationRewardD18: '20000000000000000000', + minDelegationD18: '20000000000000000000', + depositingEnabled: true, + }) + ).wait(); + + await systems() + .Core.connect(owner) + .configureCollateral({ + tokenAddress: await systems().Core.getUsdToken(), + oracleNodeId: ethers.utils.formatBytes32String(''), + issuanceRatioD18: bn(1.5), + liquidationRatioD18: bn(1.1), + liquidationRewardD18: 0, + minDelegationD18: 0, + depositingEnabled: true, + }); + }); + + const restore = snapshotCheckpoint(provider); + + it('sanity check (to ensure describes happen after the snapshot)', async () => { + assert(true); + }); + + // Note: Generic Delegation tests uses the default configuration where the delegation window is open immediately and forever. + // The tests below will test the delegation timing configuration. + describe('Delegation Timing failures', async () => { + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before('set market window times', async () => { + await MockMarket.setDelegateCollateralDelay(100); + await MockMarket.setDelegateCollateralWindow(20); + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + after(restore); + + it('fails to execute a delegation if window is not open (too soon)', async () => { + await fastForwardTo(declareDelegateIntentTime + 95, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentNotReady("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 100}")`, + systems().Core + ); + }); + + it('fails to execute a delegation if window is already closed (too late)', async () => { + await fastForwardTo(declareDelegateIntentTime + 121, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentExpired("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 120}")`, + systems().Core + ); + }); + }); + + describe('Un-Delegation Timing failures', async () => { + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before('set market window times', async () => { + await MockMarket.setUndelegateCollateralDelay(100); + await MockMarket.setUndelegateCollateralWindow(20); + }); + + before('first delegete some', async () => { + // Delegeta something that can be undelegated later + await delegateCollateral( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + }); + + before('declare intent to un-delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + after(restore); + + it('fails to execute an un-delegation if window is not open (too soon)', async () => { + await fastForwardTo(declareDelegateIntentTime + 95, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentNotReady("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 100}")`, + systems().Core + ); + }); + + it('fails to execute an un-delegation if window is already closed (too late)', async () => { + await fastForwardTo(declareDelegateIntentTime + 121, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentExpired("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 120}")`, + systems().Core + ); + }); + }); + + describe('Force Delete intents (only system owner)', async () => { + it('fails to call the functions as non-owner', async () => {}); + it('falis to delete an unexistent intent (wrong id)', async () => {}); + it('can force delete an intent by id', async () => {}); + it('can force delete all expired account intents', async () => {}); + }); + + describe('Self Delete intents (only account owner)', async () => { + it('fails to delete an intent as non-owner', async () => {}); + it("fails to delete an intent that didn't expire", async () => {}); + it('can delete an intent by id', async () => {}); + it('can delete all expired intents', async () => {}); + }); + + describe('Edge case - Self delete after configuration change', async () => { + it("fails to delete an intent that didn't expire (expiration time not set)", async () => {}); + + it('after the market config is changed, it can delete the expired intent', async () => {}); + }); + + describe('Edge case - Multiple markets with different timing configuration', async () => { + // note: with 2 markets with different config, will use the longest delay time configuration + it('fails to execute an intent based on the shortest delay', async () => {}); + + it('can execute using the longest delay', async () => {}); + }); +}); From 383d27e4002527aa8c006ff5bb1d69d335419d09 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 20 May 2024 17:47:56 -0300 Subject: [PATCH 20/50] test edge cases --- .../contracts/interfaces/IVaultModule.sol | 14 + .../contracts/modules/core/VaultModule.sol | 18 +- .../storage/AccountDelegationIntents.sol | 27 +- .../contracts/storage/DelegationIntent.sol | 1 - .../core/VaultModuleDelegationTiming.test.ts | 282 ++++++++++++++++-- 5 files changed, 317 insertions(+), 25 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index cc4bac47c4..f140991663 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -314,4 +314,18 @@ interface IVaultModule { uint128 poolId, address collateralType ) external returns (uint256 ratioD18); + + function getAccountIntent( + uint128 accountId, + uint256 intentId + ) + external + view + returns ( + uint128 poolId, + address collateralType, + int256 collateralDeltaAmount, + uint256 leverage, + uint32 declarationTime + ); } diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 713feafa98..8a61298929 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -238,7 +238,7 @@ contract VaultModule is IVaultModule { function deleteAllExpiredIntents(uint128 accountId) external override { Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); - AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + AccountDelegationIntents.getValid(accountId).cleanAllExpiredIntents(); } function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { @@ -345,6 +345,22 @@ contract VaultModule is IVaultModule { return Pool.loadExisting(poolId).currentVaultDebt(collateralType); } + function getAccountIntent( + uint128 accountId, + uint256 intentId + ) external view override returns (uint128, address, int256, uint256, uint32) { + DelegationIntent.Data storage intent = AccountDelegationIntents + .loadValid(accountId) + .getIntent(intentId); + return ( + intent.poolId, + intent.collateralType, + intent.deltaCollateralAmountD18, + intent.leverage, + intent.declarationTime + ); + } + function _delegateCollateral( uint128 accountId, uint128 poolId, diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 80b354fbcf..8f89f39a18 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -49,19 +49,25 @@ library AccountDelegationIntents { } } + /** + * @dev Returns the account delegation intents stored at the specified account id. + */ + function loadValid(uint128 id) internal view returns (Data storage accountDelegationIntents) { + accountDelegationIntents = load(id); + if (accountDelegationIntents.accountId != 0 && accountDelegationIntents.accountId != id) { + revert InvalidAccountDelegationIntents(); + } + } + /** * @dev Returns the account delegation intents stored at the specified account id. Checks if it's valid */ function getValid(uint128 id) internal returns (Data storage accountDelegationIntents) { - accountDelegationIntents = load(id); + accountDelegationIntents = loadValid(id); if (accountDelegationIntents.accountId == 0) { // Uninitialized storage will have a 0 accountId accountDelegationIntents.accountId = id; } - - if (accountDelegationIntents.accountId != id) { - revert InvalidAccountDelegationIntents(); - } } function addIntent(Data storage self, DelegationIntent.Data storage delegationIntent) internal { @@ -149,6 +155,16 @@ library AccountDelegationIntents { self.netAcountCachedDelegatedCollateral -= delegationIntent.deltaCollateralAmountD18; } + function getIntent( + Data storage self, + uint256 intentId + ) internal view returns (DelegationIntent.Data storage) { + if (!self.intentsId.contains(intentId)) { + revert DelegationIntent.InvalidDelegationIntentId(); + } + return DelegationIntent.load(intentId); + } + /** * @dev Returns the delegation intent stored at the specified nonce id. */ @@ -170,7 +186,6 @@ library AccountDelegationIntents { if (intent.intentExpired()) { removeIntent(self, intent); } - removeIntent(self, intent); } } diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index e7c88a126f..21efc0a875 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -159,7 +159,6 @@ library DelegationIntent { .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); // Note: here we don't apply the forever defaul if window time is not set to allow the intent to expire. If it's zero it means is not configured, then it can expire immediately. - uint32 _processingEndTime = self.declarationTime + requiredDelayTime + requiredWindowTime; return block.timestamp >= _processingEndTime; } diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index d51cff1c07..3b3d99773f 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -1,6 +1,7 @@ +import assert from 'assert/strict'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; -import assert from 'assert/strict'; import { BigNumber, ethers } from 'ethers'; import hre from 'hardhat'; import { bn, bootstrapWithStakedPool } from '../../bootstrap'; @@ -15,12 +16,12 @@ describe('VaultModule Two-step Delegation', function () { accountId, poolId, depositAmount, - // collateralContract, collateralAddress, oracleNodeId, } = bootstrapWithStakedPool(); // const MAX_UINT = ethers.constants.MaxUint256; + const permission = ethers.utils.formatBytes32String('DELEGATE'); let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer; @@ -133,7 +134,7 @@ describe('VaultModule Two-step Delegation', function () { systems() .Core.connect(user2) .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentNotReady("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 100}")`, + `DelegationIntentNotReady`, systems().Core ); }); @@ -145,7 +146,7 @@ describe('VaultModule Two-step Delegation', function () { systems() .Core.connect(user2) .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentExpired("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 120}")`, + `DelegationIntentExpired`, systems().Core ); }); @@ -197,7 +198,7 @@ describe('VaultModule Two-step Delegation', function () { systems() .Core.connect(user2) .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentNotReady("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 100}")`, + `DelegationIntentNotReady`, systems().Core ); }); @@ -209,33 +210,280 @@ describe('VaultModule Two-step Delegation', function () { systems() .Core.connect(user2) .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentExpired("${declareDelegateIntentTime}", "${declareDelegateIntentTime + 120}")`, + `DelegationIntentExpired`, systems().Core ); }); }); describe('Force Delete intents (only system owner)', async () => { - it('fails to call the functions as non-owner', async () => {}); - it('falis to delete an unexistent intent (wrong id)', async () => {}); - it('can force delete an intent by id', async () => {}); - it('can force delete all expired account intents', async () => {}); + let intentId: BigNumber; + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + }); + + const restoreToDeclare = snapshotCheckpoint(provider); + + it('fails to call the functions as non-owner', async () => { + await assertRevert( + systems().Core.connect(user1).forceDeleteAllAccountIntents(accountId), + `Unauthorized("${await user1.getAddress()}")`, + systems().Core + ); + }); + + it('fails to call the functions as non-owner', async () => { + await assertRevert( + systems().Core.connect(user1).forceDeleteIntents(accountId, [intentId]), + `Unauthorized("${await user1.getAddress()}")`, + systems().Core + ); + }); + + describe('Force Delete intents by ID', async () => { + before(restoreToDeclare); + it('sanity check. The intent exists', async () => { + const intent = await systems().Core.connect(owner).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('can force delete an intent by id', async () => { + await systems().Core.connect(owner).forceDeleteIntents(accountId, [intentId]); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(owner).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); + }); + + describe('Force Delete intents by Account', async () => { + before(restoreToDeclare); + it('sanity check. The intent exists', async () => { + const intent = await systems().Core.connect(owner).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('can force delete all expired account intents', async () => { + await systems().Core.connect(owner).forceDeleteAllAccountIntents(accountId); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(owner).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); + }); }); describe('Self Delete intents (only account owner)', async () => { - it('fails to delete an intent as non-owner', async () => {}); - it("fails to delete an intent that didn't expire", async () => {}); - it('can delete an intent by id', async () => {}); - it('can delete all expired intents', async () => {}); + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before('set market window times', async () => { + await MockMarket.setDelegateCollateralDelay(100); + await MockMarket.setDelegateCollateralWindow(20); + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + const restoreToDeclare = snapshotCheckpoint(provider); + + it('fails to delete all expired intents as non-owner', async () => { + await assertRevert( + systems().Core.connect(user2).deleteAllExpiredIntents(accountId), + `"PermissionDenied("${accountId}", "${permission}", "${await user2.getAddress()}")`, + systems().Core + ); + }); + it('fails to delete an intents by ID as non-owner', async () => { + await assertRevert( + systems().Core.connect(user2).deleteIntents(accountId, [intentId]), + `"PermissionDenied("${accountId}", "${permission}", "${await user2.getAddress()}")`, + systems().Core + ); + }); + it("fails to delete an intent that didn't expire", async () => { + await fastForwardTo(declareDelegateIntentTime + 115, provider()); + await assertRevert( + systems().Core.connect(user1).deleteIntents(accountId, [intentId]), + `DelegationIntentNotExpired`, + systems().Core + ); + }); + + describe('can delete an intent by id', async () => { + before(restoreToDeclare); + + it('sanity check. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('can delete an expired intent', async () => { + await fastForwardTo(declareDelegateIntentTime + 121, provider()); + await systems().Core.connect(user1).deleteIntents(accountId, [intentId]); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(user1).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); + }); + + describe('can delete all expired intents', async () => { + before(restoreToDeclare); + + it('sanity check. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('can delete all account expired intents', async () => { + await fastForwardTo(declareDelegateIntentTime + 121, provider()); + await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(user1).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); + }); }); describe('Edge case - Self delete after configuration change', async () => { - it("fails to delete an intent that didn't expire (expiration time not set)", async () => {}); + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before('set market window times', async () => { + await MockMarket.setDelegateCollateralDelay(10000); + await MockMarket.setDelegateCollateralWindow(2000); + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + before('set market window times', async () => { + await MockMarket.setDelegateCollateralDelay(100); + await MockMarket.setDelegateCollateralWindow(20); + }); + + it('sanity check. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); - it('after the market config is changed, it can delete the expired intent', async () => {}); + it('can force delete all expired account intents', async () => { + await fastForwardTo(declareDelegateIntentTime + 121, provider()); + await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(user1).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); + }); + + describe('Edge case - Self delete non configured (default expiration is after delay)', async () => { + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before(restore); + before('set market window times', async () => { + await MockMarket.setDelegateCollateralDelay(150); + // Note: not setting the window size (it means defaults to 0) - forever expiration to execute, immediate expiration to delete + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + it('sanity check 1. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('fails to delete before window starts', async () => { + await fastForwardTo(declareDelegateIntentTime + 145, provider()); + await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + }); + + it('sanity check 2. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], accountId); + }); + + it('can force delete all expired account intents', async () => { + await fastForwardTo(declareDelegateIntentTime + 155, provider()); + await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + }); + + it('intent is deleted', async () => { + await assertRevert( + systems().Core.connect(user1).getAccountIntent(accountId, intentId), + 'InvalidDelegationIntentId()', + systems().Core + ); + }); }); - describe('Edge case - Multiple markets with different timing configuration', async () => { + describe.skip('Edge case - Multiple markets with different timing configuration', async () => { // note: with 2 markets with different config, will use the longest delay time configuration it('fails to execute an intent based on the shortest delay', async () => {}); From 18a430887f1ec36e862ae2af2432af4a92225ebf Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 21 May 2024 11:55:33 -0300 Subject: [PATCH 21/50] Reduce AccountDelegationIntents footprint --- .../storage/AccountDelegationIntents.sol | 80 +------------------ 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 8f89f39a18..910bbdc249 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -23,20 +23,9 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralId => intentIds[] // accounting for the intents collateral delegated - // Per Pool - SetUtil.UintSet delegatedPools; - mapping(uint128 => int256) netDelegatedCollateralAmountPerPool; // poolId => net delegatedCollateralAmount - mapping(uint128 => uint256) delegatedCollateralAmountPerPool; // poolId => delegatedCollateralAmount - uint256 delegateAcountCachedCollateral; - mapping(uint128 => uint256) undelegatedCollateralAmountPerPool; // poolId => undelegatedCollateralAmount - uint256 undelegateAcountCachedCollateral; // Per Collateral SetUtil.AddressSet delegatedCollaterals; - mapping(address => int256) netDelegatedAmountPerCollateral; // poolId => net delegatedCollateralAmount - mapping(address => uint256) delegatedAmountPerCollateral; // collateralType => delegatedCollateralAmount - mapping(address => uint256) undelegatedAmountPerCollateral; // collateralType => undelegatedCollateralAmount - // Global - int256 netAcountCachedDelegatedCollateral; + mapping(address => int256) netDelegatedAmountPerCollateral; // collateralType => net delegatedCollateralAmount } /** @@ -78,39 +67,12 @@ library AccountDelegationIntents { ] .add(delegationIntent.id); - if (delegationIntent.deltaCollateralAmountD18 >= 0) { - self.delegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent - .deltaCollateralAmountD18 - .toUint(); - self.delegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent - .deltaCollateralAmountD18 - .toUint(); - self.delegateAcountCachedCollateral += delegationIntent - .deltaCollateralAmountD18 - .toUint(); - } else { - self.undelegatedAmountPerCollateral[ - delegationIntent.collateralType - ] += (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); - self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] += (delegationIntent - .deltaCollateralAmountD18 * -1).toUint(); - self.undelegateAcountCachedCollateral += (delegationIntent.deltaCollateralAmountD18 * - -1).toUint(); - } self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent .deltaCollateralAmountD18; - self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] += delegationIntent - .deltaCollateralAmountD18; - - if (!self.delegatedPools.contains(delegationIntent.poolId)) { - self.delegatedPools.add(delegationIntent.poolId); - } if (!self.delegatedCollaterals.contains(delegationIntent.collateralType)) { self.delegatedCollaterals.add(delegationIntent.collateralType); } - - self.netAcountCachedDelegatedCollateral += delegationIntent.deltaCollateralAmountD18; } function removeIntent( @@ -128,31 +90,8 @@ library AccountDelegationIntents { ] .remove(delegationIntent.id); - if (delegationIntent.deltaCollateralAmountD18 >= 0) { - self.delegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent - .deltaCollateralAmountD18 - .toUint(); - self.delegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent - .deltaCollateralAmountD18 - .toUint(); - self.delegateAcountCachedCollateral -= delegationIntent - .deltaCollateralAmountD18 - .toUint(); - } else { - self.undelegatedAmountPerCollateral[ - delegationIntent.collateralType - ] -= (delegationIntent.deltaCollateralAmountD18 * -1).toUint(); - self.undelegatedCollateralAmountPerPool[delegationIntent.poolId] -= (delegationIntent - .deltaCollateralAmountD18 * -1).toUint(); - self.undelegateAcountCachedCollateral -= (delegationIntent.deltaCollateralAmountD18 * - -1).toUint(); - } self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent .deltaCollateralAmountD18; - self.netDelegatedCollateralAmountPerPool[delegationIntent.poolId] -= delegationIntent - .deltaCollateralAmountD18; - - self.netAcountCachedDelegatedCollateral -= delegationIntent.deltaCollateralAmountD18; } function getIntent( @@ -199,26 +138,9 @@ library AccountDelegationIntents { removeIntent(self, intent); } - // Sanity clean all the cached values - self.netAcountCachedDelegatedCollateral = 0; - self.delegateAcountCachedCollateral = 0; - self.undelegateAcountCachedCollateral = 0; - - // Clear the cached collateral per pool - uint256[] memory pools = self.delegatedPools.values(); - for (uint256 i = 0; i < pools.length; i++) { - self.delegatedCollateralAmountPerPool[pools[i].to128()] = 0; - self.undelegatedCollateralAmountPerPool[pools[i].to128()] = 0; - self.netDelegatedCollateralAmountPerPool[pools[i].to128()] = 0; - - self.delegatedPools.remove(pools[i]); - } - // Clear the cached collateral per collateral address[] memory addresses = self.delegatedCollaterals.values(); for (uint256 i = 0; i < addresses.length; i++) { - self.delegatedAmountPerCollateral[addresses[i]] = 0; - self.undelegatedAmountPerCollateral[addresses[i]] = 0; self.netDelegatedAmountPerCollateral[addresses[i]] = 0; self.delegatedCollaterals.remove(addresses[i]); From a47d06c23c3ebae443cd66832ba1de2b711dd319 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 21 May 2024 14:41:39 -0300 Subject: [PATCH 22/50] Cleanup and comments --- .../contracts/interfaces/IVaultModule.sol | 88 +++++++++++-------- .../contracts/modules/core/VaultModule.sol | 15 ++++ .../modules/core/VaultModule.test.ts | 1 - .../core/VaultModuleDelegationTiming.test.ts | 1 - 4 files changed, 65 insertions(+), 40 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index f140991663..0b063b9a4e 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -128,30 +128,6 @@ interface IVaultModule { address collateralType ); - // /** - // * @notice Updates an account's delegated collateral amount for the specified pool and collateral type pair. - // * @param accountId The id of the account associated with the position that will be updated. - // * @param poolId The id of the pool associated with the position. - // * @param collateralType The address of the collateral used in the position. - // * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. - // * @param leverage The new leverage amount used in the position, denominated with 18 decimals of precision. - // * - // * Requirements: - // * - // * - `ERC2771Context._msgSender()` must be the owner of the account, have the `ADMIN` permission, or have the `DELEGATE` permission. - // * - If increasing the amount delegated, it must not exceed the available collateral (`getAccountAvailableCollateral`) associated with the account. - // * - If decreasing the amount delegated, the liquidity position must have a collateralization ratio greater than the target collateralization ratio for the corresponding collateral type. - // * - // * Emits a {DelegationUpdated} event. - // */ - // function delegateCollateral( - // uint128 accountId, - // uint128 poolId, - // address collateralType, - // uint256 amount, - // uint256 leverage - // ) external; - /** * @notice Declare an intent to update the delegated amount for the specified pool and collateral type pair. * @param accountId The id of the account associated with the position that intends to update the collateral amount. @@ -201,14 +177,64 @@ interface IVaultModule { */ function processIntentToDelegateCollateralByPair(uint128 accountId, uint128 poolId) external; + /** + * @notice Attempt to delete delegation intents. + * @param accountId The id of the account owning the intents. + * @param intentIds Array of ids to attempt to delete. + * @dev It will only delete expired intents. + * @dev Only the owner of the account, or given the DELEGATE permission can execute this call. + */ function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external; + /** + * @notice Attempt to delete all expired delegation intents from an account. + * @param accountId The id of the account owning the intents. + * @dev It will only delete expired intents. + * @dev Only the owner of the account, or given the DELEGATE permission can execute this call. + */ function deleteAllExpiredIntents(uint128 accountId) external; + /** + * @notice Attempt to delete delegation intents. + * @param accountId The id of the account owning the intents. + * @param intentIds Array of ids to attempt to delete. + * @dev It will delete any existent intent on the list (expired or not). + * @dev Only the vault owner can execute this call. + */ function forceDeleteIntents(uint128 accountId, uint256[] calldata intentIds) external; + /** + * @notice Attempt to delete delegation intents. + * @param accountId The id of the account owning the intents. + * @dev It will delete all the existent intents for the account (expired or not). + * @dev Only the vault owner can execute this call. + */ function forceDeleteAllAccountIntents(uint128 accountId) external; + /** + * @notice Returns details of the requested intent. + * @param accountId The id of the account owning the intent. + * @param intentId The id of the intents. + * @return poolId The id of the pool associated with the position. + * @return collateralType The address of the collateral used in the position. + * @return deltaCollateralAmountD18 The delta amount of collateral delegated in the position, denominated with 18 decimals of precision. + * @return leverage The new leverage amount used in the position, denominated with 18 decimals of precision. + * @return declarationTime The time at which the intent was declared. + */ + function getAccountIntent( + uint128 accountId, + uint256 intentId + ) + external + view + returns ( + uint128 poolId, + address collateralType, + int256 deltaCollateralAmountD18, + uint256 leverage, + uint32 declarationTime + ); + /** * @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0. * @dev Call this function using `callStatic` to treat it as a view function. @@ -314,18 +340,4 @@ interface IVaultModule { uint128 poolId, address collateralType ) external returns (uint256 ratioD18); - - function getAccountIntent( - uint128 accountId, - uint256 intentId - ) - external - view - returns ( - uint128 poolId, - address collateralType, - int256 collateralDeltaAmount, - uint256 leverage, - uint32 declarationTime - ); } diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 8a61298929..8db3f67f6a 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -223,11 +223,17 @@ contract VaultModule is IVaultModule { ); } + /** + * @inheritdoc IVaultModule + */ function forceDeleteAllAccountIntents(uint128 accountId) external override { OwnableStorage.onlyOwner(); AccountDelegationIntents.getValid(accountId).cleanAllIntents(); } + /** + * @inheritdoc IVaultModule + */ function forceDeleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { OwnableStorage.onlyOwner(); for (uint256 i = 0; i < intentIds.length; i++) { @@ -236,11 +242,17 @@ contract VaultModule is IVaultModule { } } + /** + * @inheritdoc IVaultModule + */ function deleteAllExpiredIntents(uint128 accountId) external override { Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); AccountDelegationIntents.getValid(accountId).cleanAllExpiredIntents(); } + /** + * @inheritdoc IVaultModule + */ function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); for (uint256 i = 0; i < intentIds.length; i++) { @@ -345,6 +357,9 @@ contract VaultModule is IVaultModule { return Pool.loadExisting(poolId).currentVaultDebt(collateralType); } + /** + * @inheritdoc IVaultModule + */ function getAccountIntent( uint128 accountId, uint256 intentId diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index 5275fd522b..ec29a2ebac 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -12,7 +12,6 @@ import { declareDelegateIntent, expectedToDeltaDelegatedCollateral, } from '../../../common'; -// import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import { wei } from '@synthetixio/wei'; describe('VaultModule', function () { diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 3b3d99773f..00b87ded81 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -20,7 +20,6 @@ describe('VaultModule Two-step Delegation', function () { oracleNodeId, } = bootstrapWithStakedPool(); - // const MAX_UINT = ethers.constants.MaxUint256; const permission = ethers.utils.formatBytes32String('DELEGATE'); let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer; From 12db07733a6b6e4e9f2880266195b02092071a6a Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 10:44:05 -0300 Subject: [PATCH 23/50] update storage.dump --- markets/bfp-market/storage.dump.sol | 11 +++--- markets/perps-market/storage.dump.sol | 11 +++--- protocol/synthetix/storage.dump.sol | 49 +++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/markets/bfp-market/storage.dump.sol b/markets/bfp-market/storage.dump.sol index 917f50fd71..ebdbc954be 100644 --- a/markets/bfp-market/storage.dump.sol +++ b/markets/bfp-market/storage.dump.sol @@ -228,11 +228,13 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 minDelegateTime; + uint32 __unusedLegacyStorageSlot; + uint32 undelegateCollateralDelay; + uint32 undelegateCollateralWindow; + uint32 delegateCollateralDelay; + uint32 delegateCollateralWindow; uint32 __reservedForLater1; uint64 __reservedForLater2; - uint64 __reservedForLater3; - uint64 __reservedForLater4; uint256 minLiquidityRatioD18; } struct DepositedCollateral { @@ -280,7 +282,6 @@ library OracleManager { // @custom:artifact @synthetixio/main/contracts/storage/Pool.sol:Pool library Pool { - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; struct Data { uint128 id; string name; @@ -388,7 +389,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) lastDelegationTime; + mapping(uint128 => uint64) __unused_legacy_slot; } } diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index 54420bd22d..61898e866a 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -227,11 +227,13 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 minDelegateTime; + uint32 __unusedLegacyStorageSlot; + uint32 undelegateCollateralDelay; + uint32 undelegateCollateralWindow; + uint32 delegateCollateralDelay; + uint32 delegateCollateralWindow; uint32 __reservedForLater1; uint64 __reservedForLater2; - uint64 __reservedForLater3; - uint64 __reservedForLater4; uint256 minLiquidityRatioD18; } struct DepositedCollateral { @@ -279,7 +281,6 @@ library OracleManager { // @custom:artifact @synthetixio/main/contracts/storage/Pool.sol:Pool library Pool { - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; struct Data { uint128 id; string name; @@ -387,7 +388,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) lastDelegationTime; + mapping(uint128 => uint64) __unused_legacy_slot; } } diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index ab05352c1b..e9e7a9932c 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -353,7 +353,6 @@ contract MarketManagerModule { bytes32 private constant _MARKET_FEATURE_FLAG = "registerMarket"; bytes32 private constant _DEPOSIT_MARKET_FEATURE_FLAG = "depositMarketUsd"; bytes32 private constant _WITHDRAW_MARKET_FEATURE_FLAG = "withdrawMarketUsd"; - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_RATIO = "depositMarketUsd_feeRatio"; bytes32 private constant _CONFIG_WITHDRAW_MARKET_USD_FEE_RATIO = "withdrawMarketUsd_feeRatio"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_ADDRESS = "depositMarketUsd_feeAddress"; @@ -407,6 +406,23 @@ library Account { } } +// @custom:artifact contracts/storage/AccountDelegationIntents.sol:AccountDelegationIntents +library AccountDelegationIntents { + struct Data { + uint128 accountId; + SetUtil.UintSet intentsId; + mapping(bytes32 => SetUtil.UintSet) intentsByPair; + SetUtil.AddressSet delegatedCollaterals; + mapping(address => int256) netDelegatedAmountPerCollateral; + } + function load(uint128 id) internal pure returns (Data storage accountDelegationIntents) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", id)); + assembly { + accountDelegationIntents.slot := s + } + } +} + // @custom:artifact contracts/storage/AccountRBAC.sol:AccountRBAC library AccountRBAC { bytes32 internal constant _ADMIN_PERMISSION = "ADMIN"; @@ -491,6 +507,26 @@ library CrossChain { } } +// @custom:artifact contracts/storage/DelegationIntent.sol:DelegationIntent +library DelegationIntent { + bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; + struct Data { + uint256 id; + uint128 accountId; + uint128 poolId; + address collateralType; + int256 deltaCollateralAmountD18; + uint256 leverage; + uint32 declarationTime; + } + function load(uint256 id) internal pure returns (Data storage delegationIntent) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); + assembly { + delegationIntent.slot := s + } + } +} + // @custom:artifact contracts/storage/Distribution.sol:Distribution library Distribution { struct Data { @@ -522,11 +558,13 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 minDelegateTime; + uint32 __unusedLegacyStorageSlot; + uint32 undelegateCollateralDelay; + uint32 undelegateCollateralWindow; + uint32 delegateCollateralDelay; + uint32 delegateCollateralWindow; uint32 __reservedForLater1; uint64 __reservedForLater2; - uint64 __reservedForLater3; - uint64 __reservedForLater4; uint256 minLiquidityRatioD18; } struct DepositedCollateral { @@ -589,7 +627,6 @@ library OracleManager { // @custom:artifact contracts/storage/Pool.sol:Pool library Pool { - bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; struct Data { uint128 id; string name; @@ -711,7 +748,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) lastDelegationTime; + mapping(uint128 => uint64) __unused_legacy_slot; } } From 1027f7fc9eb755a37f38addc8669a1dde434c68f Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 13:05:41 -0300 Subject: [PATCH 24/50] add skipped test - multiple markets with diff timing --- .../core/VaultModuleDelegationTiming.test.ts | 119 +++++++++++++++++- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 00b87ded81..39ba164285 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -1,5 +1,6 @@ import assert from 'assert/strict'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; import { BigNumber, ethers } from 'ethers'; @@ -482,10 +483,122 @@ describe('VaultModule Two-step Delegation', function () { }); }); - describe.skip('Edge case - Multiple markets with different timing configuration', async () => { + describe('Edge case - Multiple markets with different timing configuration', async () => { // note: with 2 markets with different config, will use the longest delay time configuration - it('fails to execute an intent based on the shortest delay', async () => {}); - it('can execute using the longest delay', async () => {}); + let SecondMockMarket: ethers.Contract; + let secondMarketId: BigNumber; + let intentId: BigNumber; + let declareDelegateIntentTime: number; + + before('deploy and connect a second fake market', async () => { + const factory = await hre.ethers.getContractFactory('MockMarket'); + + SecondMockMarket = await factory.connect(owner).deploy(); + + secondMarketId = await systems() + .Core.connect(user1) + .callStatic.registerMarket(SecondMockMarket.address); + + await systems().Core.connect(user1).registerMarket(SecondMockMarket.address); + + await SecondMockMarket.connect(owner).initialize( + systems().Core.address, + secondMarketId, + ethers.utils.parseEther('1') + ); + + await systems() + .Core.connect(owner) + .setPoolConfiguration(poolId, [ + { + marketId: secondMarketId, + weightD18: ethers.utils.parseEther('1'), + maxDebtShareValueD18: ethers.utils.parseEther('10000000000000000'), + }, + ]); + }); + + before('set both market window times', async () => { + await MockMarket.setDelegateCollateralDelay(100); + await MockMarket.setUndelegateCollateralDelay(100); + await MockMarket.setDelegateCollateralWindow(0); + await MockMarket.setUndelegateCollateralWindow(0); + + await SecondMockMarket.setDelegateCollateralDelay(200); + await SecondMockMarket.setUndelegateCollateralDelay(200); + await SecondMockMarket.setDelegateCollateralWindow(20); + await SecondMockMarket.setUndelegateCollateralWindow(20); + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + it('intent is skipped to execute an intent based on the shortest delay', async () => { + await fastForwardTo(declareDelegateIntentTime + 120, provider()); + + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentSkipped(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, + systems().Core + ); + }); + + it('can execute using the longest delay', async () => { + await fastForwardTo(declareDelegateIntentTime + 210, provider()); + + await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + }); + + it('intent is removed (skipped) to execute an intent based on the wrong window', async () => { + const newIntentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(1), + ethers.utils.parseEther('1') + ); + + const newDeclareDelegateIntentTime = await getTime(provider()); + + await fastForwardTo(newDeclareDelegateIntentTime + 250, provider()); + + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [newIntentId]); + + await assertEvent( + tx, + `DelegationIntentSkipped(${newIntentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, + systems().Core + ); + + await assertEvent( + tx, + `DelegationIntentRemoved(${newIntentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, + systems().Core + ); + }); }); }); From 3a4ac693de59d3e651ad90a31b3cacb8479ad66a Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 13:09:53 -0300 Subject: [PATCH 25/50] deps fix --- markets/legacy-market/package.json | 8 +------- yarn.lock | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/markets/legacy-market/package.json b/markets/legacy-market/package.json index 905c5234a5..419dc4980f 100644 --- a/markets/legacy-market/package.json +++ b/markets/legacy-market/package.json @@ -17,12 +17,6 @@ "author": "", "license": "MIT", "devDependencies": { - "@synthetixio/common-config": "workspace:*", - "@synthetixio/core-utils": "workspace:*", - "@synthetixio/docgen": "workspace:*", - "@synthetixio/wei": "^2.74.4", - "ethers": "^5.7.2", - "hardhat": "^2.19.5", - "solidity-docgen": "^0.6.0-beta.36" + "@synthetixio/main": "workspace:*" } } diff --git a/yarn.lock b/yarn.lock index 0c6be326e7..662f895eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3097,13 +3097,7 @@ __metadata: version: 0.0.0-use.local resolution: "@synthetixio/legacy-market@workspace:markets/legacy-market" dependencies: - "@synthetixio/common-config": "workspace:*" - "@synthetixio/core-utils": "workspace:*" - "@synthetixio/docgen": "workspace:*" - "@synthetixio/wei": "npm:^2.74.4" - ethers: "npm:^5.7.2" - hardhat: "npm:^2.19.5" - solidity-docgen: "npm:^0.6.0-beta.36" + "@synthetixio/main": "workspace:*" languageName: unknown linkType: soft From ed9d256f3bf86a24fc12383daa9a58fca7be4d04 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 13:14:12 -0300 Subject: [PATCH 26/50] undo deps fix --- markets/legacy-market/package.json | 8 +------- yarn.lock | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/markets/legacy-market/package.json b/markets/legacy-market/package.json index 905c5234a5..419dc4980f 100644 --- a/markets/legacy-market/package.json +++ b/markets/legacy-market/package.json @@ -17,12 +17,6 @@ "author": "", "license": "MIT", "devDependencies": { - "@synthetixio/common-config": "workspace:*", - "@synthetixio/core-utils": "workspace:*", - "@synthetixio/docgen": "workspace:*", - "@synthetixio/wei": "^2.74.4", - "ethers": "^5.7.2", - "hardhat": "^2.19.5", - "solidity-docgen": "^0.6.0-beta.36" + "@synthetixio/main": "workspace:*" } } diff --git a/yarn.lock b/yarn.lock index 0c6be326e7..662f895eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3097,13 +3097,7 @@ __metadata: version: 0.0.0-use.local resolution: "@synthetixio/legacy-market@workspace:markets/legacy-market" dependencies: - "@synthetixio/common-config": "workspace:*" - "@synthetixio/core-utils": "workspace:*" - "@synthetixio/docgen": "workspace:*" - "@synthetixio/wei": "npm:^2.74.4" - ethers: "npm:^5.7.2" - hardhat: "npm:^2.19.5" - solidity-docgen: "npm:^0.6.0-beta.36" + "@synthetixio/main": "workspace:*" languageName: unknown linkType: soft From f0531e2580fe9774ed380eec4a6de6238f64c304 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 13:19:38 -0300 Subject: [PATCH 27/50] fix deps --- markets/legacy-market/package.json | 9 ++++++++- yarn.lock | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/markets/legacy-market/package.json b/markets/legacy-market/package.json index 419dc4980f..c02b4ebebb 100644 --- a/markets/legacy-market/package.json +++ b/markets/legacy-market/package.json @@ -17,6 +17,13 @@ "author": "", "license": "MIT", "devDependencies": { - "@synthetixio/main": "workspace:*" + "@synthetixio/common-config": "workspace:*", + "@synthetixio/core-utils": "workspace:*", + "@synthetixio/docgen": "workspace:*", + "@synthetixio/main": "workspace:*", + "@synthetixio/wei": "^2.74.4", + "ethers": "^5.7.2", + "hardhat": "^2.19.5", + "solidity-docgen": "^0.6.0-beta.36" } } diff --git a/yarn.lock b/yarn.lock index 662f895eb3..2b58923f6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3097,7 +3097,14 @@ __metadata: version: 0.0.0-use.local resolution: "@synthetixio/legacy-market@workspace:markets/legacy-market" dependencies: + "@synthetixio/common-config": "workspace:*" + "@synthetixio/core-utils": "workspace:*" + "@synthetixio/docgen": "workspace:*" "@synthetixio/main": "workspace:*" + "@synthetixio/wei": "npm:^2.74.4" + ethers: "npm:^5.7.2" + hardhat: "npm:^2.19.5" + solidity-docgen: "npm:^0.6.0-beta.36" languageName: unknown linkType: soft From d03d3ca021ef1168d763ce1e4843b7615bce064e Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 22 May 2024 15:35:14 -0300 Subject: [PATCH 28/50] typos + remove account owner check on delete expired intents --- .../contracts/interfaces/IVaultModule.sol | 8 ++--- .../contracts/modules/core/VaultModule.sol | 2 -- .../core/VaultModuleDelegationTiming.test.ts | 30 +++++-------------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 0b063b9a4e..2de3592ba8 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -153,10 +153,10 @@ interface IVaultModule { ) external returns (uint256 intentId); /** - * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by intent ids. + * @notice Attempt to process the outstanding intents to update the delegated amount of collateral by intent ids. * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param intentIds An array of intents to attempt to process. - * @dev The intents that are not executable at this time will be ignored and am event will be emitted to show that. + * @dev The intents that are not executable at this time will be ignored and an event will be emitted to show that. * Requirements: * * Emits a {DelegationUpdated} event. @@ -167,7 +167,7 @@ interface IVaultModule { ) external; /** - * @notice Attempt to process the outstanding intents to udpate the delegated amount of collateral by pool/accountID pair. + * @notice Attempt to process the outstanding intents to update the delegated amount of collateral by pool/accountID pair. * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @param poolId The ID of the pool for which the intent of the account to delegate a new amount of collateral is being processed * @dev The intents that are not executable at this time will be ignored and am event will be emitted to show that. @@ -182,7 +182,6 @@ interface IVaultModule { * @param accountId The id of the account owning the intents. * @param intentIds Array of ids to attempt to delete. * @dev It will only delete expired intents. - * @dev Only the owner of the account, or given the DELEGATE permission can execute this call. */ function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external; @@ -190,7 +189,6 @@ interface IVaultModule { * @notice Attempt to delete all expired delegation intents from an account. * @param accountId The id of the account owning the intents. * @dev It will only delete expired intents. - * @dev Only the owner of the account, or given the DELEGATE permission can execute this call. */ function deleteAllExpiredIntents(uint128 accountId) external; diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 8db3f67f6a..d7f27e2f06 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -246,7 +246,6 @@ contract VaultModule is IVaultModule { * @inheritdoc IVaultModule */ function deleteAllExpiredIntents(uint128 accountId) external override { - Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); AccountDelegationIntents.getValid(accountId).cleanAllExpiredIntents(); } @@ -254,7 +253,6 @@ contract VaultModule is IVaultModule { * @inheritdoc IVaultModule */ function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { - Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (intent.accountId != accountId) { diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 39ba164285..99f96ae850 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -21,8 +21,6 @@ describe('VaultModule Two-step Delegation', function () { oracleNodeId, } = bootstrapWithStakedPool(); - const permission = ethers.utils.formatBytes32String('DELEGATE'); - let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer; let MockMarket: ethers.Contract; @@ -315,20 +313,6 @@ describe('VaultModule Two-step Delegation', function () { const restoreToDeclare = snapshotCheckpoint(provider); - it('fails to delete all expired intents as non-owner', async () => { - await assertRevert( - systems().Core.connect(user2).deleteAllExpiredIntents(accountId), - `"PermissionDenied("${accountId}", "${permission}", "${await user2.getAddress()}")`, - systems().Core - ); - }); - it('fails to delete an intents by ID as non-owner', async () => { - await assertRevert( - systems().Core.connect(user2).deleteIntents(accountId, [intentId]), - `"PermissionDenied("${accountId}", "${permission}", "${await user2.getAddress()}")`, - systems().Core - ); - }); it("fails to delete an intent that didn't expire", async () => { await fastForwardTo(declareDelegateIntentTime + 115, provider()); await assertRevert( @@ -338,22 +322,22 @@ describe('VaultModule Two-step Delegation', function () { ); }); - describe('can delete an intent by id', async () => { + describe('can delete an intent by id (as another user)', async () => { before(restoreToDeclare); it('sanity check. The intent exists', async () => { - const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + const intent = await systems().Core.connect(user2).getAccountIntent(accountId, intentId); assertBn.equal(intent[0], accountId); }); it('can delete an expired intent', async () => { await fastForwardTo(declareDelegateIntentTime + 121, provider()); - await systems().Core.connect(user1).deleteIntents(accountId, [intentId]); + await systems().Core.connect(user2).deleteIntents(accountId, [intentId]); }); it('intent is deleted', async () => { await assertRevert( - systems().Core.connect(user1).getAccountIntent(accountId, intentId), + systems().Core.connect(user2).getAccountIntent(accountId, intentId), 'InvalidDelegationIntentId()', systems().Core ); @@ -364,18 +348,18 @@ describe('VaultModule Two-step Delegation', function () { before(restoreToDeclare); it('sanity check. The intent exists', async () => { - const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + const intent = await systems().Core.connect(user2).getAccountIntent(accountId, intentId); assertBn.equal(intent[0], accountId); }); it('can delete all account expired intents', async () => { await fastForwardTo(declareDelegateIntentTime + 121, provider()); - await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + await systems().Core.connect(user2).deleteAllExpiredIntents(accountId); }); it('intent is deleted', async () => { await assertRevert( - systems().Core.connect(user1).getAccountIntent(accountId, intentId), + systems().Core.connect(user2).getAccountIntent(accountId, intentId), 'InvalidDelegationIntentId()', systems().Core ); From 0cf55495196b76e086213cbc5458ff466949cbaf Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 10:57:24 -0300 Subject: [PATCH 29/50] PR review fixes (partial) --- .../contracts/interfaces/IVaultModule.sol | 78 +++++++++- .../contracts/modules/core/VaultModule.sol | 136 +++++++++++++++--- .../synthetix/contracts/storage/Account.sol | 13 +- .../storage/AccountDelegationIntents.sol | 73 ++++++---- .../contracts/storage/DelegationIntent.sol | 15 +- protocol/synthetix/contracts/storage/Pool.sol | 4 +- protocol/synthetix/test/common/stakers.ts | 12 +- .../modules/core/AssociateDebtModule.test.ts | 2 +- .../modules/core/LiquidationModule.test.ts | 4 +- .../modules/core/MarketManagerModule.test.ts | 8 +- .../modules/core/PoolModuleFundAdmin.test.ts | 2 +- .../core/VaultModuleDelegationTiming.test.ts | 4 +- .../test/integration/storage/Pool.test.ts | 2 +- 13 files changed, 275 insertions(+), 78 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 2de3592ba8..a5b6e48326 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -28,7 +28,22 @@ interface IVaultModule { error InvalidDelegationIntent(); /** - * @notice Thrown when the specified intent is not related to the account id. + * @notice Thrown when the specified intent does not exist. + */ + error DelegationIntentNotExists(); + + /** + * @notice Thrown when the specified intent is ready to be executed yet. + */ + error DelegationIntentNotReady(uint32 declarationTime, uint32 processingStartTime); + + /** + * @notice Thrown when the specified intent is already expired. + */ + error DelegationIntentExpired(uint32 declarationTime, uint32 processingEndTime); + + /** + * @notice Thrown when the specified intent is not expired yet. */ error DelegationIntentNotExpired(uint256 intentId); @@ -167,15 +182,20 @@ interface IVaultModule { ) external; /** - * @notice Attempt to process the outstanding intents to update the delegated amount of collateral by pool/accountID pair. + * @notice Attempt to process the outstanding intents to update the delegated amount of collateral by pool/collateral pair. * @param accountId The id of the account associated with the position that intends to update the collateral amount. * @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 used in the position. * @dev The intents that are not executable at this time will be ignored and am event will be emitted to show that. * Requirements: * * Emits a {DelegationUpdated} event. */ - function processIntentToDelegateCollateralByPair(uint128 accountId, uint128 poolId) external; + function processIntentToDelegateCollateralByPair( + uint128 accountId, + uint128 poolId, + address collateralType + ) external; /** * @notice Attempt to delete delegation intents. @@ -183,7 +203,7 @@ interface IVaultModule { * @param intentIds Array of ids to attempt to delete. * @dev It will only delete expired intents. */ - function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external; + function deleteExpiredIntents(uint128 accountId, uint256[] calldata intentIds) external; /** * @notice Attempt to delete all expired delegation intents from an account. @@ -233,6 +253,56 @@ interface IVaultModule { uint32 declarationTime ); + /** + * @notice Returns the total (positive and negative) amount of collateral intended to be delegated to the vault by the account. + * @param accountId The id of the account owning the intents. + * @param collateralType The address of the collateral. + * @return netDelegatedPerCollateral The total amount of collateral intended to be delegated to the vault by the account, denominated with 18 decimals of precision. + */ + function getNetDelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view returns (int256 netDelegatedPerCollateral); + + /** + * @notice Returns the list of executable (by timing) intents for the account. + * @param accountId The id of the account owning the intents. + * @param maxProcessableIntent The maximum number of intents to process. + * @return intentIds The list of intents. + * @return foundIntents The number of found intents. + * + * @dev The array of intent ids might have empty items at the end, use `foundIntents` to know the actual number + * of valid intents. + */ + function getAccountExecutableIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view returns (uint256[] memory intentIds, uint256 foundIntents); + + /** + * @notice Returns the list of expired (by timing) intents for the account. + * @param accountId The id of the account owning the intents. + * @param maxProcessableIntent The maximum number of intents to process. + * @return intentIds The list of intents. + * @return foundIntents The number of found intents. + * + * @dev The array of intent ids might have empty items at the end, use `foundIntents` to know the actual number + * of valid intents. + */ + function getAccountExpiredIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view returns (uint256[] memory intentIds, uint256 foundIntents); + + /** + * @notice Returns the list of intents for the account. + * @param accountId The id of the account owning the intents. + * @return intentIds The list of intents. + */ + function getAccountIntentIds( + uint128 accountId + ) external view returns (uint256[] memory intentIds); + /** * @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0. * @dev Call this function using `callStatic` to treat it as a view function. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index d7f27e2f06..6ef6e897c6 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -82,9 +82,8 @@ contract VaultModule is IVaultModule { ); } - uint256 newCollateralAmountD18 = accumulatedDelta > 0 - ? currentCollateralAmount + (accumulatedDelta).toUint() - : currentCollateralAmount - (-1 * accumulatedDelta).toUint(); + uint256 newCollateralAmountD18 = (currentCollateralAmount.toInt() + accumulatedDelta) + .toUint(); // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. @@ -97,7 +96,7 @@ contract VaultModule is IVaultModule { // Check the validity of the collateral amount to be delegated, respecting the caches that track outstanding intents to delegate or undelegate collateral. // If increasing delegated collateral amount, // Check that the account has sufficient collateral. - else if (newCollateralAmountD18 > currentCollateralAmount) { + if (newCollateralAmountD18 > currentCollateralAmount) { // Check if the collateral is enabled here because we still want to allow reducing delegation for disabled collaterals. CollateralConfiguration.collateralEnabled(collateralType); @@ -153,7 +152,15 @@ contract VaultModule is IVaultModule { for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (!intent.isExecutable()) { - // Remove the intent. + // emit an Skipped event + emit DelegationIntentSkipped( + intent.id, + accountId, + intent.poolId, + intent.collateralType + ); + + // If expired, remove the intent. if (intent.intentExpired()) { AccountDelegationIntents.getValid(accountId).removeIntent(intent); emit DelegationIntentRemoved( @@ -164,14 +171,6 @@ contract VaultModule is IVaultModule { ); } - // emit an event - emit DelegationIntentSkipped( - intent.id, - accountId, - intent.poolId, - intent.collateralType - ); - // skip to the next intent continue; } @@ -215,11 +214,12 @@ contract VaultModule is IVaultModule { */ function processIntentToDelegateCollateralByPair( uint128 accountId, - uint128 poolId + uint128 poolId, + address collateralType ) external override { processIntentToDelegateCollateralByIntents( accountId, - AccountDelegationIntents.getValid(accountId).intentIdsByPair(poolId, accountId) + AccountDelegationIntents.getValid(accountId).intentIdsByPair(poolId, collateralType) ); } @@ -252,7 +252,10 @@ contract VaultModule is IVaultModule { /** * @inheritdoc IVaultModule */ - function deleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { + function deleteExpiredIntents( + uint128 accountId, + uint256[] calldata intentIds + ) external override { for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (intent.accountId != accountId) { @@ -374,6 +377,98 @@ contract VaultModule is IVaultModule { ); } + /** + * @inheritdoc IVaultModule + */ + function getAccountIntentIds( + uint128 accountId + ) external view override returns (uint256[] memory) { + return AccountDelegationIntents.loadValid(accountId).intentsId.values(); + } + + /** + * @inheritdoc IVaultModule + */ + function getAccountExpiredIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view override returns (uint256[] memory expiredIntents, uint256 foundItems) { + uint256[] memory allIntents = AccountDelegationIntents + .loadValid(accountId) + .intentsId + .values(); + uint256 max = maxProcessableIntent > allIntents.length + ? allIntents.length + : maxProcessableIntent; + expiredIntents = new uint256[](max); + for (uint256 i = 0; i < max; i++) { + if (DelegationIntent.load(allIntents[i]).intentExpired()) { + expiredIntents[i] = allIntents[i]; + foundItems++; + } + } + } + + /** + * @inheritdoc IVaultModule + */ + function getAccountExecutableIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view override returns (uint256[] memory executableIntents, uint256 foundItems) { + uint256[] memory allIntents = AccountDelegationIntents + .loadValid(accountId) + .intentsId + .values(); + uint256 max = maxProcessableIntent > allIntents.length + ? allIntents.length + : maxProcessableIntent; + executableIntents = new uint256[](max); + for (uint256 i = 0; i < max; i++) { + if (DelegationIntent.load(allIntents[i]).isExecutable()) { + executableIntents[i] = allIntents[i]; + foundItems++; + } + } + } + + /** + * @inheritdoc IVaultModule + */ + function getNetDelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view override returns (int256) { + return + AccountDelegationIntents.loadValid(accountId).netDelegatedAmountPerCollateral[ + collateralType + ]; + } + + function getDelegationReminder( + uint128 accountId, + uint128 poolId, + address collateralType + ) external view returns (uint256 reminder) { + uint256[] memory intentIds = AccountDelegationIntents.loadValid(accountId).intentIdsByPair( + poolId, + collateralType + ); + int256 accumulatedIntentDelta = 0; + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (!intent.intentExpired()) { + accumulatedIntentDelta += intent.deltaCollateralAmountD18; + } + } + // TODO LJM Continue from here + // Also another function that loops on all intents and gives the remainder that can be delegated, assuming + // those that expired expire + // those that can execute can execute + // those that are yet to be executed execute + // Capped at the amount that can be delegated, would help integrators / front-ends, given that the gas limit can be pretty high for reads (as opposed to writes, limited to block space) + } + function _delegateCollateral( uint128 accountId, uint128 poolId, @@ -390,9 +485,10 @@ contract VaultModule is IVaultModule { accountId.toBytes32() ); - uint256 newCollateralAmountD18 = deltaCollateralAmountD18 > 0 - ? vault.currentAccountCollateral(accountId) + deltaCollateralAmountD18.toUint() - : vault.currentAccountCollateral(accountId) - (deltaCollateralAmountD18 * -1).toUint(); + uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + + uint256 newCollateralAmountD18 = (currentCollateralAmount.toInt() + + deltaCollateralAmountD18).toUint(); // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. @@ -403,8 +499,6 @@ contract VaultModule is IVaultModule { ); } - uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); - // Conditions for collateral amount // If increasing delegated collateral amount, diff --git a/protocol/synthetix/contracts/storage/Account.sol b/protocol/synthetix/contracts/storage/Account.sol index 9eb3e811ff..2bfdff379d 100644 --- a/protocol/synthetix/contracts/storage/Account.sol +++ b/protocol/synthetix/contracts/storage/Account.sol @@ -54,7 +54,10 @@ library Account { AccountRBAC.Data rbac; uint64 lastInteraction; uint64 __slotAvailableForFutureUse; - uint128 __slot2AvailableForFutureUse; + /** + * @dev Account Delegation Intents index is used to nuke previous intents using a new epoch (useful on liquidations). + */ + uint128 currentDelegationIntentsEpoch; /** * @dev Address set of collaterals that are being used in the system by this account. */ @@ -207,4 +210,12 @@ library Account { revert ICollateralModule.InsufficientAccountCollateral(amountD18); } } + + /** + * @dev Returns the new delegation intents epoch (by incrementing the currentDelegationIntentsEpoch). + */ + function getNewDelegationIntentsEpoch(Data storage self) internal returns (uint128) { + self.currentDelegationIntentsEpoch += 1; + return self.currentDelegationIntentsEpoch; + } } diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 910bbdc249..c7a71a321a 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -4,6 +4,8 @@ pragma solidity >=0.8.11 <0.9.0; import "./DelegationIntent.sol"; import "@synthetixio/core-contracts/contracts/utils/SetUtil.sol"; import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; +import "../interfaces/IVaultModule.sol"; +import "./Account.sol"; /** * @title Represents a delegation (or undelegation) intent. @@ -15,13 +17,13 @@ library AccountDelegationIntents { using SetUtil for SetUtil.UintSet; using SetUtil for SetUtil.AddressSet; using DelegationIntent for DelegationIntent.Data; - - error InvalidAccountDelegationIntents(); + using Account for Account.Data; struct Data { uint128 accountId; + uint128 delegationIntentsEpoch; // nonce used to nuke previous intents using a new era (useful on liquidations) SetUtil.UintSet intentsId; - mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralId => intentIds[] + mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralType => intentIds[] // accounting for the intents collateral delegated // Per Collateral SetUtil.AddressSet delegatedCollaterals; @@ -31,8 +33,17 @@ library AccountDelegationIntents { /** * @dev Returns the account delegation intents stored at the specified account id. */ - function load(uint128 id) internal pure returns (Data storage accountDelegationIntents) { - bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", id)); + function load( + uint128 accountId, + uint128 delegationIntentsEpoch + ) internal pure returns (Data storage accountDelegationIntents) { + bytes32 s = keccak256( + abi.encode( + "io.synthetix.synthetix.AccountDelegationIntents", + accountId, + delegationIntentsEpoch + ) + ); assembly { accountDelegationIntents.slot := s } @@ -41,21 +52,31 @@ library AccountDelegationIntents { /** * @dev Returns the account delegation intents stored at the specified account id. */ - function loadValid(uint128 id) internal view returns (Data storage accountDelegationIntents) { - accountDelegationIntents = load(id); - if (accountDelegationIntents.accountId != 0 && accountDelegationIntents.accountId != id) { - revert InvalidAccountDelegationIntents(); + function loadValid( + uint128 accountId + ) internal view returns (Data storage accountDelegationIntents) { + uint128 delegationIntentsEpoch = Account.load(accountId).currentDelegationIntentsEpoch; + accountDelegationIntents = load(accountId, delegationIntentsEpoch); + if ( + accountDelegationIntents.accountId != 0 && + (accountDelegationIntents.accountId != accountId || + accountDelegationIntents.delegationIntentsEpoch != delegationIntentsEpoch) + ) { + revert IVaultModule.InvalidDelegationIntent(); } } /** * @dev Returns the account delegation intents stored at the specified account id. Checks if it's valid */ - function getValid(uint128 id) internal returns (Data storage accountDelegationIntents) { - accountDelegationIntents = loadValid(id); + function getValid(uint128 accountId) internal returns (Data storage accountDelegationIntents) { + accountDelegationIntents = loadValid(accountId); if (accountDelegationIntents.accountId == 0) { - // Uninitialized storage will have a 0 accountId - accountDelegationIntents.accountId = id; + // Uninitialized storage will have a 0 accountId; it means we need to initialize it (new accountDelegationIntents era) + accountDelegationIntents.accountId = accountId; + accountDelegationIntents.delegationIntentsEpoch = Account + .load(accountId) + .currentDelegationIntentsEpoch; } } @@ -99,7 +120,7 @@ library AccountDelegationIntents { uint256 intentId ) internal view returns (DelegationIntent.Data storage) { if (!self.intentsId.contains(intentId)) { - revert DelegationIntent.InvalidDelegationIntentId(); + revert IVaultModule.InvalidDelegationIntent(); } return DelegationIntent.load(intentId); } @@ -110,13 +131,13 @@ library AccountDelegationIntents { function intentIdsByPair( Data storage self, uint128 poolId, - uint128 accountId + address collateralType ) internal view returns (uint256[] memory intentIds) { - return self.intentsByPair[keccak256(abi.encodePacked(poolId, accountId))].values(); + return self.intentsByPair[keccak256(abi.encodePacked(poolId, collateralType))].values(); } /** - * @dev Cleans all intents related to the account. This should be called upon liquidation. + * @dev Cleans all expired intents related to the account. */ function cleanAllExpiredIntents(Data storage self) internal { uint256[] memory intentIds = self.intentsId.values(); @@ -129,21 +150,11 @@ library AccountDelegationIntents { } /** - * @dev Cleans all intents related to the account. This should be called upon liquidation. + * @dev Cleans all intents (expired and not) related to the account. This should be called upon liquidation. */ function cleanAllIntents(Data storage self) internal { - uint256[] memory intentIds = self.intentsId.values(); - for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - removeIntent(self, intent); - } - - // Clear the cached collateral per collateral - address[] memory addresses = self.delegatedCollaterals.values(); - for (uint256 i = 0; i < addresses.length; i++) { - self.netDelegatedAmountPerCollateral[addresses[i]] = 0; - - self.delegatedCollaterals.remove(addresses[i]); - } + // Nuke all intents by incrementing the delegationIntentsEpoch nonce + // This is useful to avoid iterating over all intents to remove them and risking a for loop revert. + Account.load(self.accountId).getNewDelegationIntentsEpoch(); } } diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 21efc0a875..72fb9b5410 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -4,16 +4,14 @@ pragma solidity >=0.8.11 <0.9.0; import "./Config.sol"; import "./Pool.sol"; +import "../interfaces/IVaultModule.sol"; + /** * @title Represents a delegation (or undelegation) intent. */ library DelegationIntent { using Pool for Pool.Data; - error InvalidDelegationIntentId(); - error DelegationIntentNotReady(uint32 declarationTime, uint32 processingStartTime); - error DelegationIntentExpired(uint32 declarationTime, uint32 processingEndTime); - bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; /** @@ -85,7 +83,7 @@ library DelegationIntent { delegationIntent = load(id); if (delegationIntent.id != id) { - revert InvalidDelegationIntentId(); + revert IVaultModule.DelegationIntentNotExists(); } } @@ -132,9 +130,12 @@ library DelegationIntent { uint32 _processingEndTime = _processingStartTime + requiredWindowTime; if (block.timestamp < _processingStartTime) - revert DelegationIntentNotReady(self.declarationTime, _processingStartTime); + revert IVaultModule.DelegationIntentNotReady( + self.declarationTime, + _processingStartTime + ); if (block.timestamp >= _processingEndTime) - revert DelegationIntentExpired(self.declarationTime, _processingEndTime); + revert IVaultModule.DelegationIntentExpired(self.declarationTime, _processingEndTime); } function isExecutable(Data storage self) internal view returns (bool) { diff --git a/protocol/synthetix/contracts/storage/Pool.sol b/protocol/synthetix/contracts/storage/Pool.sol index ae9e46a4e6..8d0572c49d 100644 --- a/protocol/synthetix/contracts/storage/Pool.sol +++ b/protocol/synthetix/contracts/storage/Pool.sol @@ -428,9 +428,11 @@ library Pool { ? market.undelegateCollateralDelay : market.delegateCollateralDelay; + // Find the most restrictive delay time market and use that market to get the delay and window configured times. if (marketDelayTime > requiredDelayTime) { requiredDelayTime = marketDelayTime; - // Also get the window from the more restrictive market + + // Pull the window time from the same market. requiredWindowTime = isUndelegation ? market.undelegateCollateralWindow : market.delegateCollateralWindow; diff --git a/protocol/synthetix/test/common/stakers.ts b/protocol/synthetix/test/common/stakers.ts index a0a270fdbf..455541ea1e 100644 --- a/protocol/synthetix/test/common/stakers.ts +++ b/protocol/synthetix/test/common/stakers.ts @@ -82,7 +82,11 @@ export const stake = async ( ethers.utils.parseEther('1') ); - await Core.connect(user).processIntentToDelegateCollateralByPair(accountId, poolId); + await Core.connect(user).processIntentToDelegateCollateralByPair( + accountId, + poolId, + CollateralMock.address + ); // also for convenience invest in the 0 pool await Core.connect(user).declareIntentToDelegateCollateral( @@ -93,5 +97,9 @@ export const stake = async ( ethers.utils.parseEther('1') ); - await Core.connect(user).processIntentToDelegateCollateralByPair(accountId, 0); + await Core.connect(user).processIntentToDelegateCollateralByPair( + accountId, + 0, + CollateralMock.address + ); }; diff --git a/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts b/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts index d0c799fabe..6870435e0f 100644 --- a/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/AssociateDebtModule.test.ts @@ -129,7 +129,7 @@ describe('AssociateDebtModule', function () { await systems() .Core.connect(user2) - .processIntentToDelegateCollateralByPair(user2AccountId, poolId); + .processIntentToDelegateCollateralByPair(user2AccountId, poolId, collateralAddress()); }); describe('when the market reported debt is 100', function () { diff --git a/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts b/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts index 5645911578..0f68e7b668 100644 --- a/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/LiquidationModule.test.ts @@ -120,7 +120,7 @@ describe('LiquidationModule', function () { ); await systems() .Core.connect(user2) - .processIntentToDelegateCollateralByPair(accountId2, poolId); + .processIntentToDelegateCollateralByPair(accountId2, poolId, collateralAddress()); }); let txn: ethers.providers.TransactionResponse; @@ -288,7 +288,7 @@ describe('LiquidationModule', function () { ); await systems() .Core.connect(user2) - .processIntentToDelegateCollateralByPair(liquidatorAccountId, 0); + .processIntentToDelegateCollateralByPair(liquidatorAccountId, 0, collateralAddress()); await systems() .Core.connect(user2) diff --git a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts index 22ba17af22..9ffe313db6 100644 --- a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts @@ -496,7 +496,7 @@ describe('MarketManagerModule', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, poolId + 1); + .processIntentToDelegateCollateralByPair(accountId, poolId + 1, collateralAddress()); await systems() .Core.connect(user1) @@ -509,7 +509,7 @@ describe('MarketManagerModule', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, poolId + 2); + .processIntentToDelegateCollateralByPair(accountId, poolId + 2, collateralAddress()); }); before('accumulate debt', async () => { @@ -799,7 +799,7 @@ describe('MarketManagerModule', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, poolId + 1); + .processIntentToDelegateCollateralByPair(accountId, poolId + 1, collateralAddress()); await systems() .Core.connect(user1) @@ -812,7 +812,7 @@ describe('MarketManagerModule', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, poolId + 2); + .processIntentToDelegateCollateralByPair(accountId, poolId + 2, collateralAddress()); }); it('inRangePools and outRangePools are returned correctly', async () => { diff --git a/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts b/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts index 0ffe754009..3d5fbf4b1e 100644 --- a/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts +++ b/protocol/synthetix/test/integration/modules/core/PoolModuleFundAdmin.test.ts @@ -392,7 +392,7 @@ describe('PoolModule Admin', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, secondPoolId); + .processIntentToDelegateCollateralByPair(accountId, secondPoolId, collateralAddress()); await systems() .Core.connect(user1) diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 99f96ae850..7ee34125c5 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -316,7 +316,7 @@ describe('VaultModule Two-step Delegation', function () { it("fails to delete an intent that didn't expire", async () => { await fastForwardTo(declareDelegateIntentTime + 115, provider()); await assertRevert( - systems().Core.connect(user1).deleteIntents(accountId, [intentId]), + systems().Core.connect(user1).deleteExpiredIntents(accountId, [intentId]), `DelegationIntentNotExpired`, systems().Core ); @@ -332,7 +332,7 @@ describe('VaultModule Two-step Delegation', function () { it('can delete an expired intent', async () => { await fastForwardTo(declareDelegateIntentTime + 121, provider()); - await systems().Core.connect(user2).deleteIntents(accountId, [intentId]); + await systems().Core.connect(user2).deleteExpiredIntents(accountId, [intentId]); }); it('intent is deleted', async () => { diff --git a/protocol/synthetix/test/integration/storage/Pool.test.ts b/protocol/synthetix/test/integration/storage/Pool.test.ts index e98ea0ecac..74ac2b33c4 100644 --- a/protocol/synthetix/test/integration/storage/Pool.test.ts +++ b/protocol/synthetix/test/integration/storage/Pool.test.ts @@ -114,7 +114,7 @@ describe('Pool', function () { ); await systems() .Core.connect(user1) - .processIntentToDelegateCollateralByPair(accountId, poolId); + .processIntentToDelegateCollateralByPair(accountId, poolId, collateralAddress()); }); it('the ultimate capacity of the pool ends up higher', async () => { From 6c54a5dc669bdf5c17e1810b84e83f13679f8023 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 11:21:10 -0300 Subject: [PATCH 30/50] fix: ensure intentId to process is still valid for the account (not removed or nuked) --- protocol/synthetix/contracts/interfaces/IVaultModule.sol | 5 +++++ protocol/synthetix/contracts/modules/core/VaultModule.sol | 7 +++++++ .../contracts/storage/AccountDelegationIntents.sol | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index a5b6e48326..fe9d40ffb0 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -47,6 +47,11 @@ interface IVaultModule { */ error DelegationIntentNotExpired(uint256 intentId); + /** + * @notice Thrown when the specified intent is not in current epoch (it was nuked in a liquidation or administrative fix). + */ + error DelegationIntentNotInCurrentEpoch(uint256 intentId); + /** * @notice Thrown when the specified intent is not executable due to pending intents. */ diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 6ef6e897c6..a854007f05 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -149,8 +149,15 @@ contract VaultModule is IVaultModule { uint256[] memory intentIds ) public override { FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); + AccountDelegationIntents.Data storage accountIntents = AccountDelegationIntents.loadValid( + accountId + ); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (!accountIntents.isInCurrentEpoch(intent.id)) { + revert DelegationIntentNotInCurrentEpoch(intent.id); + } + if (!intent.isExecutable()) { // emit an Skipped event emit DelegationIntentSkipped( diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index c7a71a321a..33c753c3ca 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -136,6 +136,12 @@ library AccountDelegationIntents { return self.intentsByPair[keccak256(abi.encodePacked(poolId, collateralType))].values(); } + function isInCurrentEpoch(Data storage self, uint256 intentId) internal view returns (bool) { + // Notice: not checking that `self.delegationIntentsEpoch == account.currentDelegationIntentsEpoch` since + // it was loadValid and getValid use it at load time + return self.intentsId.contains(intentId); + } + /** * @dev Cleans all expired intents related to the account. */ From 3c9f5be4effb47d1c89a913432c08cdd33aa5727 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 11:32:50 -0300 Subject: [PATCH 31/50] fix pair using wrong key --- .../contracts/storage/AccountDelegationIntents.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 33c753c3ca..74d86b411a 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -84,7 +84,9 @@ library AccountDelegationIntents { self.intentsId.add(delegationIntent.id); self .intentsByPair[ - keccak256(abi.encodePacked(delegationIntent.poolId, delegationIntent.accountId)) + keccak256( + abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) + ) ] .add(delegationIntent.id); @@ -107,7 +109,9 @@ library AccountDelegationIntents { self.intentsId.remove(delegationIntent.id); self .intentsByPair[ - keccak256(abi.encodePacked(delegationIntent.poolId, delegationIntent.accountId)) + keccak256( + abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) + ) ] .remove(delegationIntent.id); From 50b6b61ab3a9dca425224c8ad28faddd15475930 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 12:10:48 -0300 Subject: [PATCH 32/50] fix tests --- .../modules/core/VaultModule.test.ts | 127 +++++++++--------- .../core/VaultModuleDelegationTiming.test.ts | 12 +- 2 files changed, 72 insertions(+), 67 deletions(-) diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index ec29a2ebac..a2f91f5692 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -340,20 +340,22 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await declareDelegateIntent( - systems, - owner, - user1, - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50), - ethers.utils.parseEther('1') - ); await assertRevert( systems() .Core.connect(user1) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + .declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50) + ), + ethers.utils.parseEther('1') + ), `CollateralDepositDisabled("${collateralAddress()}")`, systems().Core ); @@ -379,7 +381,7 @@ describe('VaultModule', function () { .configureCollateral({ ...beforeConfiguration, depositingEnabled: true }); }); - // fails when collateral is disabled for the pool by pool owner + // fails when collateral limit is zero (disabled for the pool by pool owner) before('disable collateral for the pool by the pool owner', async () => { await systems() .Core.connect(user1) @@ -389,23 +391,23 @@ describe('VaultModule', function () { }); }); - // fails when collateral is disabled for the pool by pool owner it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await declareDelegateIntent( - systems, - owner, - user1, - accountId, - fakeVaultId, - collateralAddress(), - depositAmount.div(50), - ethers.utils.parseEther('1') - ); - await assertRevert( systems() .Core.connect(user1) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + .declareIntentToDelegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50) + ), + ethers.utils.parseEther('1') + ), `PoolCollateralLimitExceeded("${fakeVaultId}", "${collateralAddress()}", "${depositAmount .div(50) .toString()}", "${bn(10).toString()}")`, @@ -447,21 +449,22 @@ describe('VaultModule', function () { }); it('fails when pool does not allow sufficient deposit amount', async () => { - const intentId = await declareDelegateIntent( - systems, - owner, - user1, - accountId, - poolId, - collateralAddress(), - depositAmount.mul(2), - ethers.utils.parseEther('1') - ); - await assertRevert( systems() .Core.connect(user1) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2) + ), + ethers.utils.parseEther('1') + ), `PoolCollateralLimitExceeded("${poolId}", "${collateralAddress()}", "${depositAmount .mul(2) .toString()}", "${depositAmount.div(2).toString()}")`, @@ -644,21 +647,22 @@ describe('VaultModule', function () { const wanted = depositAmount.mul(3); const missing = wanted.sub(depositAmount.div(3)); - const intentId = await declareDelegateIntent( - systems, - owner, - user2, - user2AccountId, - poolId, - collateralAddress(), - wanted, - ethers.utils.parseEther('1') - ); - await assertRevert( systems() .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + user2AccountId, + poolId, + collateralAddress(), + wanted + ), + ethers.utils.parseEther('1') + ), `InsufficientAccountCollateral("${missing}")`, systems().Core ); @@ -678,21 +682,22 @@ describe('VaultModule', function () { }); it('fails when trying to open delegation position with disabled collateral', async () => { - const intentId = await declareDelegateIntent( - systems, - owner, - user2, - user2AccountId, - poolId, - collateralAddress(), - depositAmount, // user1 50%, user2 50% - ethers.utils.parseEther('1') - ); - await assertRevert( systems() .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(user2AccountId, [intentId]), + .declareIntentToDelegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + user2AccountId, + poolId, + collateralAddress(), + depositAmount + ), + ethers.utils.parseEther('1') + ), `CollateralDepositDisabled("${collateralAddress()}")`, systems().Core ); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 7ee34125c5..8763bb717b 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -261,7 +261,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(owner).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); @@ -281,7 +281,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(owner).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); @@ -338,7 +338,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(user2).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); @@ -360,7 +360,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(user2).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); @@ -408,7 +408,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(user1).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); @@ -461,7 +461,7 @@ describe('VaultModule Two-step Delegation', function () { it('intent is deleted', async () => { await assertRevert( systems().Core.connect(user1).getAccountIntent(accountId, intentId), - 'InvalidDelegationIntentId()', + 'InvalidDelegationIntent()', systems().Core ); }); From f62dad5d25d044a0cf15abd427e06e3bc3e8f66f Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 13:05:52 -0300 Subject: [PATCH 33/50] propagate storage change --- markets/bfp-market/storage.dump.sol | 2 +- markets/perps-market/storage.dump.sol | 2 +- protocol/synthetix/storage.dump.sol | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/markets/bfp-market/storage.dump.sol b/markets/bfp-market/storage.dump.sol index ebdbc954be..6850792fbd 100644 --- a/markets/bfp-market/storage.dump.sol +++ b/markets/bfp-market/storage.dump.sol @@ -119,7 +119,7 @@ library Account { AccountRBAC.Data rbac; uint64 lastInteraction; uint64 __slotAvailableForFutureUse; - uint128 __slot2AvailableForFutureUse; + uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; } function load(uint128 id) internal pure returns (Data storage account) { diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index 61898e866a..1aabb0efbf 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -118,7 +118,7 @@ library Account { AccountRBAC.Data rbac; uint64 lastInteraction; uint64 __slotAvailableForFutureUse; - uint128 __slot2AvailableForFutureUse; + uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; } function load(uint128 id) internal pure returns (Data storage account) { diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index e9e7a9932c..42c0ab4896 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -395,7 +395,7 @@ library Account { AccountRBAC.Data rbac; uint64 lastInteraction; uint64 __slotAvailableForFutureUse; - uint128 __slot2AvailableForFutureUse; + uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; } function load(uint128 id) internal pure returns (Data storage account) { @@ -410,13 +410,14 @@ library Account { library AccountDelegationIntents { struct Data { uint128 accountId; + uint128 delegationIntentsEpoch; SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; SetUtil.AddressSet delegatedCollaterals; mapping(address => int256) netDelegatedAmountPerCollateral; } - function load(uint128 id) internal pure returns (Data storage accountDelegationIntents) { - bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", id)); + function load(uint128 accountId, uint128 delegationIntentsEpoch) internal pure returns (Data storage accountDelegationIntents) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", accountId, delegationIntentsEpoch)); assembly { accountDelegationIntents.slot := s } From 901f5429818ac10da5dccc70879a37bf2e7b9634 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 27 May 2024 13:06:05 -0300 Subject: [PATCH 34/50] rename function (wip) --- protocol/synthetix/contracts/modules/core/VaultModule.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index a854007f05..f8ebb27777 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -452,16 +452,16 @@ contract VaultModule is IVaultModule { ]; } - function getDelegationReminder( + function getDelegationAccumulated( uint128 accountId, uint128 poolId, address collateralType - ) external view returns (uint256 reminder) { + ) external view returns (int256 accumulatedIntentDelta) { uint256[] memory intentIds = AccountDelegationIntents.loadValid(accountId).intentIdsByPair( poolId, collateralType ); - int256 accumulatedIntentDelta = 0; + accumulatedIntentDelta = 0; for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (!intent.intentExpired()) { From 30953ee8d40a53c90bf7431277bf39a79f596a16 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 28 May 2024 15:16:43 -0300 Subject: [PATCH 35/50] add two new views --- .../contracts/interfaces/IVaultModule.sol | 31 +++++++++ .../contracts/modules/core/VaultModule.sol | 68 ++++++++++++++++--- .../storage/CollateralConfiguration.sol | 19 +++++- 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index fe9d40ffb0..ba02a10449 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -269,6 +269,37 @@ interface IVaultModule { address collateralType ) external view returns (int256 netDelegatedPerCollateral); + /** + * @notice Returns the total executable (not expired) amount of collateral intended to be delegated to the vault by the account. + * @param accountId The id of the account owning the intents. + * @param poolId The id of the pool associated with the position. + * @param collateralType The address of the collateral. + * @return accumulatedIntentDelta The total amount of collateral intended to be delegated that is not expired, denominated with 18 decimals of precision. + */ + function getExecutableDelegationAccumulated( + uint128 accountId, + uint128 poolId, + address collateralType + ) external view returns (int256 accumulatedIntentDelta); + + /** + * @notice Returns the amount of debt to repay in order to an intent to reduce the delegated collateral will meet the issuance ratio. + * @param accountId The id of the account owning the position. + * @param poolId The id of the pool associated with the position. + * @param collateralType The address of the collateral. + * @param deltaCollateralAmountD18 The delta collateral to be delegated, denominated with 18 decimals of precision. + * @param collateralPrice Reference price of the collateral. + * @return howMuchToRepayD18 The debt to repay. + * + */ + function requiredDebtRepaymentForUndelegation( + uint128 accountId, + uint128 poolId, + address collateralType, + int256 deltaCollateralAmountD18, + uint256 collateralPrice + ) external view returns (uint256 howMuchToRepayD18); + /** * @notice Returns the list of executable (by timing) intents for the account. * @param accountId The id of the account owning the intents. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index f8ebb27777..0312d582aa 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -452,11 +452,14 @@ contract VaultModule is IVaultModule { ]; } - function getDelegationAccumulated( + /** + * @inheritdoc IVaultModule + */ + function getExecutableDelegationAccumulated( uint128 accountId, uint128 poolId, address collateralType - ) external view returns (int256 accumulatedIntentDelta) { + ) external view override returns (int256 accumulatedIntentDelta) { uint256[] memory intentIds = AccountDelegationIntents.loadValid(accountId).intentIdsByPair( poolId, collateralType @@ -468,12 +471,61 @@ contract VaultModule is IVaultModule { accumulatedIntentDelta += intent.deltaCollateralAmountD18; } } - // TODO LJM Continue from here - // Also another function that loops on all intents and gives the remainder that can be delegated, assuming - // those that expired expire - // those that can execute can execute - // those that are yet to be executed execute - // Capped at the amount that can be delegated, would help integrators / front-ends, given that the gas limit can be pretty high for reads (as opposed to writes, limited to block space) + } + + /** + * @inheritdoc IVaultModule + */ + function requiredDebtRepaymentForUndelegation( + uint128 accountId, + uint128 poolId, + address collateralType, + int256 deltaCollateralAmountD18, + uint256 collateralPrice + ) external view override returns (uint256) { + // Identify the vault that corresponds to this collateral type and pool id. + if (deltaCollateralAmountD18 > 0) { + return 0; + } + + Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; + + int256 debt = vault.currentEpoch().consolidatedDebtAmountsD18[accountId]; + uint256 effectiveDebt = debt < 0 ? 0 : debt.toUint(); + if (effectiveDebt == 0) { + return 0; + } + + uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + + if (currentCollateralAmount < (-1 * deltaCollateralAmountD18).toUint()) { + revert ExceedingUndelegateAmount( + deltaCollateralAmountD18, + 0, + deltaCollateralAmountD18, + currentCollateralAmount + ); + } + + uint256 newCollateralAmountD18 = (currentCollateralAmount.toInt() + + deltaCollateralAmountD18).toUint(); + + uint256 minIssuanceRatioD18 = Pool + .loadExisting(poolId) + .collateralConfigurations[collateralType] + .issuanceRatioD18; + + uint256 effectiveIssuanceRatioD18 = CollateralConfiguration + .load(collateralType) + .getEffectiveIssuanceRatio(minIssuanceRatioD18); + + uint256 collateralValue = newCollateralAmountD18.mulDecimal(collateralPrice); + + uint256 maxDebt = effectiveIssuanceRatioD18.mulDecimal(collateralValue); + if (maxDebt >= effectiveDebt) { + return 0; + } + return maxDebt - effectiveDebt; } function _delegateCollateral( diff --git a/protocol/synthetix/contracts/storage/CollateralConfiguration.sol b/protocol/synthetix/contracts/storage/CollateralConfiguration.sol index 5bb1ea464b..4d80a7b9c6 100644 --- a/protocol/synthetix/contracts/storage/CollateralConfiguration.sol +++ b/protocol/synthetix/contracts/storage/CollateralConfiguration.sol @@ -222,6 +222,21 @@ library CollateralConfiguration { return node.price.toUint(); } + /** + * @dev Gets the effective issuance ratio taking capped to the minimum sent as parameter. + * @param self The CollateralConfiguration object whose collateral and settings are being queried. + * @param minIssuanceRatioD18 The minimum cap on issuanceRatio. + * @return issuanceRatioD18 The effective issuance ratio. + */ + function getEffectiveIssuanceRatio( + Data storage self, + uint256 minIssuanceRatioD18 + ) internal view returns (uint256 issuanceRatioD18) { + issuanceRatioD18 = self.issuanceRatioD18 > minIssuanceRatioD18 + ? self.issuanceRatioD18 + : minIssuanceRatioD18; + } + /** * @dev Reverts if the specified collateral and debt values produce a collateralization ratio which is below the amount required for new issuance of snxUSD. * @param self The CollateralConfiguration object whose collateral and settings are being queried. @@ -234,9 +249,7 @@ library CollateralConfiguration { uint256 collateralValueD18, uint256 minIssuanceRatioD18 ) internal view { - uint256 issuanceRatioD18 = self.issuanceRatioD18 > minIssuanceRatioD18 - ? self.issuanceRatioD18 - : minIssuanceRatioD18; + uint256 issuanceRatioD18 = getEffectiveIssuanceRatio(self, minIssuanceRatioD18); if ( debtD18 != 0 && From 05cd81fab29ae65d8711ec2a88e9ffa8f58317a1 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 30 May 2024 13:05:08 -0300 Subject: [PATCH 36/50] some fixes --- protocol/synthetix/contracts/interfaces/IVaultModule.sol | 2 +- protocol/synthetix/contracts/modules/core/VaultModule.sol | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index ba02a10449..527a34512d 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -283,7 +283,7 @@ interface IVaultModule { ) external view returns (int256 accumulatedIntentDelta); /** - * @notice Returns the amount of debt to repay in order to an intent to reduce the delegated collateral will meet the issuance ratio. + * @notice Returns the amount of debt that needs to be repaid, which allows execution of intents that aim at undelegating collalteral, ensuring complyiance with the issuance ratio requirements * @param accountId The id of the account owning the position. * @param poolId The id of the pool associated with the position. * @param collateralType The address of the collateral. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 0312d582aa..a2a0da3f56 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -185,9 +185,6 @@ contract VaultModule is IVaultModule { // Ensure the intent is valid. if (intent.accountId != accountId) revert InvalidDelegationIntent(); - // Ensure the intent is within the processing window. - intent.checkIsExecutable(); - // Process the intent. _delegateCollateral( accountId, @@ -519,6 +516,11 @@ contract VaultModule is IVaultModule { .load(collateralType) .getEffectiveIssuanceRatio(minIssuanceRatioD18); + // edge case. Issuance set to max + if (effectiveIssuanceRatioD18 == type(uint256).max) { + return effectiveDebt; + } + uint256 collateralValue = newCollateralAmountD18.mulDecimal(collateralPrice); uint256 maxDebt = effectiveIssuanceRatioD18.mulDecimal(collateralValue); From 2e5ddcdd7835207b4d98cc9649dec5068c81903a Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 4 Jun 2024 14:56:40 -0300 Subject: [PATCH 37/50] add views test + small fix --- .../contracts/modules/core/VaultModule.sol | 4 +- .../test/common/delegateCollateral.ts | 2 +- .../VaultModuleDelegationIntentViews.test.ts | 295 ++++++++++++++++++ 3 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index a2a0da3f56..ea7b440aca 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -407,7 +407,7 @@ contract VaultModule is IVaultModule { expiredIntents = new uint256[](max); for (uint256 i = 0; i < max; i++) { if (DelegationIntent.load(allIntents[i]).intentExpired()) { - expiredIntents[i] = allIntents[i]; + expiredIntents[foundItems] = allIntents[i]; foundItems++; } } @@ -430,7 +430,7 @@ contract VaultModule is IVaultModule { executableIntents = new uint256[](max); for (uint256 i = 0; i < max; i++) { if (DelegationIntent.load(allIntents[i]).isExecutable()) { - executableIntents[i] = allIntents[i]; + executableIntents[foundItems] = allIntents[i]; foundItems++; } } diff --git a/protocol/synthetix/test/common/delegateCollateral.ts b/protocol/synthetix/test/common/delegateCollateral.ts index c954bc06fb..65541d8b54 100644 --- a/protocol/synthetix/test/common/delegateCollateral.ts +++ b/protocol/synthetix/test/common/delegateCollateral.ts @@ -35,7 +35,7 @@ export async function declareDelegateIntent( if (shouldCleanBefore) { await systems().Core.connect(owner).forceDeleteAllAccountIntents(accountId); } - const intentId = await systems() + const intentId: BigNumber = await systems() .Core.connect(signer) .callStatic.declareIntentToDelegateCollateral( accountId, diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts new file mode 100644 index 0000000000..06d6c3a3a1 --- /dev/null +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts @@ -0,0 +1,295 @@ +import assert from 'assert/strict'; +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; +import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; +import { BigNumber, ethers } from 'ethers'; +import hre from 'hardhat'; +import { bn, bootstrapWithStakedPool } from '../../bootstrap'; +import { declareDelegateIntent } from '../../../common'; +import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; + +describe('VaultModule Two-step Delegation views', function () { + const { + signers, + systems, + provider, + accountId, + poolId, + depositAmount, + collateralAddress, + oracleNodeId, + } = bootstrapWithStakedPool(); + + let owner: ethers.Signer, user1: ethers.Signer; + + let MockMarket: ethers.Contract; + let marketId: BigNumber; + + before('identify signers', async () => { + [owner, user1] = signers(); + }); + + before('give user1 permission to register market', async () => { + await systems() + .Core.connect(owner) + .addToFeatureFlagAllowlist( + ethers.utils.formatBytes32String('registerMarket'), + await user1.getAddress() + ); + }); + + before('deploy and connect fake market', async () => { + const factory = await hre.ethers.getContractFactory('MockMarket'); + + MockMarket = await factory.connect(owner).deploy(); + + marketId = await systems().Core.connect(user1).callStatic.registerMarket(MockMarket.address); + + await systems().Core.connect(user1).registerMarket(MockMarket.address); + + await MockMarket.connect(owner).initialize( + systems().Core.address, + marketId, + ethers.utils.parseEther('1') + ); + + await systems() + .Core.connect(owner) + .setPoolConfiguration(poolId, [ + { + marketId: marketId, + weightD18: ethers.utils.parseEther('1'), + maxDebtShareValueD18: ethers.utils.parseEther('10000000000000000'), + }, + ]); + }); + + before('add second collateral type', async () => { + // add collateral + await ( + await systems().Core.connect(owner).configureCollateral({ + tokenAddress: systems().Collateral2Mock.address, + oracleNodeId: oracleNodeId(), + issuanceRatioD18: '5000000000000000000', + liquidationRatioD18: '1500000000000000000', + liquidationRewardD18: '20000000000000000000', + minDelegationD18: '20000000000000000000', + depositingEnabled: true, + }) + ).wait(); + + await systems() + .Core.connect(owner) + .configureCollateral({ + tokenAddress: await systems().Core.getUsdToken(), + oracleNodeId: ethers.utils.formatBytes32String(''), + issuanceRatioD18: bn(1.5), + liquidationRatioD18: bn(1.1), + liquidationRewardD18: 0, + minDelegationD18: 0, + depositingEnabled: true, + }); + }); + + before('set initial market window times', async () => { + await MockMarket.setDelegateCollateralDelay(200); + await MockMarket.setDelegateCollateralWindow(200); + await MockMarket.setUndelegateCollateralDelay(200); + await MockMarket.setUndelegateCollateralWindow(200); + }); + + const intentIds = new Array(); + let accumulatedDeposit = depositAmount; + let accummulatedTime = 0; + + before('add timed delegation intents', async () => { + // add some intents separated by 20 seconds each to test the timed views + accummulatedTime = await getTime(provider()); + + for (let i = 0; i < 5; i++) { + accumulatedDeposit = accumulatedDeposit.add(depositAmount.div(10)); + accummulatedTime += 20; + + await fastForwardTo(accummulatedTime, provider()); + const intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + accumulatedDeposit, + ethers.utils.parseEther('1'), + false + ); + + intentIds.push(intentId); + } + }); + + const restore = snapshotCheckpoint(provider); + + it('sanity check. It should be 5 intents', async () => { + assert.equal(intentIds.length, 5); + }); + + describe('Intents Views at initial time (none executable neither expired)', async () => { + before(restore); + + it('should have all intents', async () => { + const accountIntentsId = await systems().Core.getAccountIntentIds(accountId); + assertBn.equal(accountIntentsId.length, 5); + assert.deepEqual(accountIntentsId, intentIds); + }); + + it('should have no intents as executable', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExecutableIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 0); + // 5 is the total of intents, so all are in the array + assertBn.equal(pendingExecutableIntents.executableIntents.length, 5); + // all the intents ids are zero, it means none found as expired + assert.deepEqual(pendingExecutableIntents.executableIntents, [ + bn(0), + bn(0), + bn(0), + bn(0), + bn(0), + ]); + }); + + it('should have no intents as expired', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExpiredIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 0); + // 5 is the total of intents, so all are in the array + assertBn.equal(pendingExecutableIntents.expiredIntents.length, 5); + // all the intents ids are zero, it means none found as expired + assert.deepEqual(pendingExecutableIntents.expiredIntents, [ + bn(0), + bn(0), + bn(0), + bn(0), + bn(0), + ]); + }); + }); + + describe('Intents Views at window opened for all (all executable none expired)', async () => { + before(restore); + + before('fast forward to mid time', async () => { + await fastForwardTo(accummulatedTime + 100 + 110, provider()); + }); + + it('should have all intents', async () => { + const accountIntentsId = await systems().Core.getAccountIntentIds(accountId); + assertBn.equal(accountIntentsId.length, 5); + assert.deepEqual(accountIntentsId, intentIds); + }); + + it('should have all intents as pending and executable', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExecutableIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 5); + assertBn.equal(pendingExecutableIntents.executableIntents.length, 5); + assert.deepEqual(pendingExecutableIntents.executableIntents, intentIds); + }); + + it('should have no intents as expired', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExpiredIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 0); + // 5 is the total of intents, so all are in the array + assertBn.equal(pendingExecutableIntents.expiredIntents.length, 5); + // all the intents ids are zero, it means none found as expired + assert.deepEqual(pendingExecutableIntents.expiredIntents, [ + bn(0), + bn(0), + bn(0), + bn(0), + bn(0), + ]); + }); + }); + + describe('Intents Views at mid time (3 expired)', async () => { + before(restore); + + before('fast forward to mid time', async () => { + await fastForwardTo(accummulatedTime + 100 + 200 + 70, provider()); + }); + + it('should have all intents', async () => { + const accountIntentsId = await systems().Core.getAccountIntentIds(accountId); + assertBn.equal(accountIntentsId.length, 5); + assert.deepEqual(accountIntentsId, intentIds); + }); + + it('should have some as pending and executable', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExecutableIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 2); + assertBn.equal(pendingExecutableIntents.executableIntents[0], intentIds[3]); + assertBn.equal(pendingExecutableIntents.executableIntents[1], intentIds[4]); + }); + + it('should have some intents as expired', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExpiredIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 3); + assertBn.equal(pendingExecutableIntents.expiredIntents[0], intentIds[0]); + assertBn.equal(pendingExecutableIntents.expiredIntents[1], intentIds[1]); + assertBn.equal(pendingExecutableIntents.expiredIntents[2], intentIds[2]); + }); + }); + + describe('Intents Views at closed window for all (all expired)', async () => { + before(restore); + + before('fast forward to mid time', async () => { + await fastForwardTo(accummulatedTime + 100 + 200 + 110, provider()); + }); + + it('should have all intents', async () => { + const accountIntentsId = await systems().Core.getAccountIntentIds(accountId); + assertBn.equal(accountIntentsId.length, 5); + assert.deepEqual(accountIntentsId, intentIds); + }); + + it('should have some as pending and executable', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExecutableIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 0); + assert.deepEqual(pendingExecutableIntents.executableIntents, [ + bn(0), + bn(0), + bn(0), + bn(0), + bn(0), + ]); + }); + + it('should have some intents as expired', async () => { + const pendingExecutableIntents = await systems().Core.getAccountExpiredIntentIds( + accountId, + 100 + ); + assertBn.equal(pendingExecutableIntents.foundItems, 5); + assert.deepEqual(pendingExecutableIntents.expiredIntents, intentIds); + }); + }); +}); From 9e836d5ef4bff43e1b1dd8eba8f08f0097f9bf9c Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 26 Jun 2024 12:48:17 -0300 Subject: [PATCH 38/50] PR Fixes -> SIP 366 (#2171) * renames and comments on storage changes * reduce setter/getter footprint * apply same fix to mock market * fix test related to SetDelegateCollateralConfiguration * Missing fix on tests * rollback `minDelegationTime` and `delegateCollateral` removal * Add separate FF for legacy and two steps delegation * use twoStepDelegateCollateral in cannonfile test * fix test (attempt) * uses right FF * test both modes * remove duplicated code * update storage * rename getValid * add global min/max to delegate delay and window * update storage dump * Add test to both FFs enabled * Link `AccountDelegationIntents` directly to `Account` * add tests to global params * use declarationTime as id for intents * Revert "use declarationTime as id for intents" This reverts commit beb4f8c4e7be18aa08bea548bcd2430c9bc95f34. * fix typo in comments --- markets/bfp-market/storage.dump.sol | 38 +- markets/perps-market/storage.dump.sol | 38 +- .../interfaces/IMarketManagerModule.sol | 101 +- .../contracts/interfaces/IVaultModule.sol | 29 + .../synthetix/contracts/mocks/MockMarket.sol | 34 +- .../modules/core/LiquidationModule.sol | 6 +- .../modules/core/MarketManagerModule.sol | 113 +-- .../contracts/modules/core/PoolModule.sol | 2 +- .../contracts/modules/core/VaultModule.sol | 114 ++- .../synthetix/contracts/storage/Account.sol | 16 +- .../storage/AccountDelegationIntents.sol | 61 -- .../synthetix/contracts/storage/Market.sol | 14 +- protocol/synthetix/contracts/storage/Pool.sol | 61 ++ .../contracts/storage/VaultEpoch.sol | 7 +- protocol/synthetix/storage.dump.sol | 18 +- protocol/synthetix/test/common/stakedPool.ts | 20 +- protocol/synthetix/test/common/stakers.ts | 75 +- .../synthetix/test/integration/bootstrap.ts | 6 +- .../modules/core/MarketManagerModule.test.ts | 101 +- .../core/VaultModule.legacyDelagate.test.ts | 959 ++++++++++++++++++ .../modules/core/VaultModule.test.ts | 75 +- .../VaultModuleDelegationIntentViews.test.ts | 5 +- .../core/VaultModuleDelegationTiming.test.ts | 139 ++- 23 files changed, 1630 insertions(+), 402 deletions(-) create mode 100644 protocol/synthetix/test/integration/modules/core/VaultModule.legacyDelagate.test.ts diff --git a/markets/bfp-market/storage.dump.sol b/markets/bfp-market/storage.dump.sol index 6850792fbd..a7a893ee36 100644 --- a/markets/bfp-market/storage.dump.sol +++ b/markets/bfp-market/storage.dump.sol @@ -121,6 +121,7 @@ library Account { uint64 __slotAvailableForFutureUse; uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; + mapping(uint128 => AccountDelegationIntents.Data) delegationIntents; } function load(uint128 id) internal pure returns (Data storage account) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.Account", id)); @@ -130,6 +131,16 @@ library Account { } } +// @custom:artifact @synthetixio/main/contracts/storage/AccountDelegationIntents.sol:AccountDelegationIntents +library AccountDelegationIntents { + struct Data { + SetUtil.UintSet intentsId; + mapping(bytes32 => SetUtil.UintSet) intentsByPair; + SetUtil.AddressSet delegatedCollaterals; + mapping(address => int256) netDelegatedAmountPerCollateral; + } +} + // @custom:artifact @synthetixio/main/contracts/storage/AccountRBAC.sol:AccountRBAC library AccountRBAC { bytes32 internal constant _ADMIN_PERMISSION = "ADMIN"; @@ -197,6 +208,26 @@ library Config { } } +// @custom:artifact @synthetixio/main/contracts/storage/DelegationIntent.sol:DelegationIntent +library DelegationIntent { + bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; + struct Data { + uint256 id; + uint128 accountId; + uint128 poolId; + address collateralType; + int256 deltaCollateralAmountD18; + uint256 leverage; + uint32 declarationTime; + } + function load(uint256 id) internal pure returns (Data storage delegationIntent) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); + assembly { + delegationIntent.slot := s + } + } +} + // @custom:artifact @synthetixio/main/contracts/storage/Distribution.sol:Distribution library Distribution { struct Data { @@ -228,7 +259,7 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 __unusedLegacyStorageSlot; + uint32 minDelegateTime; uint32 undelegateCollateralDelay; uint32 undelegateCollateralWindow; uint32 delegateCollateralDelay; @@ -282,6 +313,9 @@ library OracleManager { // @custom:artifact @synthetixio/main/contracts/storage/Pool.sol:Pool library Pool { + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_DELAY_MIN = "delegateCollateralDelay_min"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_WINDOW_MAX = "delegateCollateralWindow_max"; struct Data { uint128 id; string name; @@ -389,7 +423,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) __unused_legacy_slot; + mapping(uint128 => uint64) lastDelegationTime; } } diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index 1aabb0efbf..e9ea722875 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -120,6 +120,7 @@ library Account { uint64 __slotAvailableForFutureUse; uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; + mapping(uint128 => AccountDelegationIntents.Data) delegationIntents; } function load(uint128 id) internal pure returns (Data storage account) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.Account", id)); @@ -129,6 +130,16 @@ library Account { } } +// @custom:artifact @synthetixio/main/contracts/storage/AccountDelegationIntents.sol:AccountDelegationIntents +library AccountDelegationIntents { + struct Data { + SetUtil.UintSet intentsId; + mapping(bytes32 => SetUtil.UintSet) intentsByPair; + SetUtil.AddressSet delegatedCollaterals; + mapping(address => int256) netDelegatedAmountPerCollateral; + } +} + // @custom:artifact @synthetixio/main/contracts/storage/AccountRBAC.sol:AccountRBAC library AccountRBAC { bytes32 internal constant _ADMIN_PERMISSION = "ADMIN"; @@ -196,6 +207,26 @@ library Config { } } +// @custom:artifact @synthetixio/main/contracts/storage/DelegationIntent.sol:DelegationIntent +library DelegationIntent { + bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; + struct Data { + uint256 id; + uint128 accountId; + uint128 poolId; + address collateralType; + int256 deltaCollateralAmountD18; + uint256 leverage; + uint32 declarationTime; + } + function load(uint256 id) internal pure returns (Data storage delegationIntent) { + bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); + assembly { + delegationIntent.slot := s + } + } +} + // @custom:artifact @synthetixio/main/contracts/storage/Distribution.sol:Distribution library Distribution { struct Data { @@ -227,7 +258,7 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 __unusedLegacyStorageSlot; + uint32 minDelegateTime; uint32 undelegateCollateralDelay; uint32 undelegateCollateralWindow; uint32 delegateCollateralDelay; @@ -281,6 +312,9 @@ library OracleManager { // @custom:artifact @synthetixio/main/contracts/storage/Pool.sol:Pool library Pool { + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_DELAY_MIN = "delegateCollateralDelay_min"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_WINDOW_MAX = "delegateCollateralWindow_max"; struct Data { uint128 id; string name; @@ -388,7 +422,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) __unused_legacy_slot; + mapping(uint128 => uint64) lastDelegationTime; } } diff --git a/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol b/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol index 2c015ce282..44a27ec65d 100644 --- a/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol +++ b/protocol/synthetix/contracts/interfaces/IMarketManagerModule.sol @@ -73,36 +73,28 @@ interface IMarketManagerModule { event MarketSystemFeePaid(uint128 indexed marketId, uint256 feeAmount); /** - * @notice Emitted when a market sets an updated undelegate collateral delay + * @notice Emitted when a market sets an updated minimum delegation time * @param marketId The id of the market that the setting is applied to - * @param undelegateCollateralDelay The minimum amount of time to undelegate collateral + * @param minDelegateTime The minimum amount of time between delegation changes */ - event SetUndelegateCollateralDelay(uint128 indexed marketId, uint32 undelegateCollateralDelay); + event SetMinDelegateTime(uint128 indexed marketId, uint32 minDelegateTime); /** - * @notice Emitted when a market sets an updated undelegate collateral window + * @notice Emitted when a market sets its delegation and undelegation configuration * @param marketId The id of the market that the setting is applied to + * @param delegateCollateralDelay The minimum amount of time to delegate collateral + * @param delegateCollateralWindow The maximum window of time to delegate collateral + * @param undelegateCollateralDelay The minimum amount of time to undelegate collateral * @param undelegateCollateralWindow The maximum window of time to undelegate collateral */ - event SetUndelegateCollateralWindow( + event SetDelegateCollateralConfiguration( uint128 indexed marketId, + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, uint32 undelegateCollateralWindow ); - /** - * @notice Emitted when a market sets an updated delegate collateral delay - * @param marketId The id of the market that the setting is applied to - * @param delegateCollateralDelay The minimum amount of time to delegate collateral - */ - event SetDelegateCollateralDelay(uint128 indexed marketId, uint32 delegateCollateralDelay); - - /** - * @notice Emitted when a market sets an updated delegate collateral window - * @param marketId The id of the market that the setting is applied to - * @param delegateCollateralWindow The maximum window of time to delegate collateral - */ - event SetDelegateCollateralWindow(uint128 indexed marketId, uint32 delegateCollateralWindow); - /** * @notice Emitted when a market-specific minimum liquidity ratio is set * @param marketId The id of the market that the setting is applied to @@ -245,65 +237,50 @@ interface IMarketManagerModule { ) external returns (bool finishedDistributing); /** - * @notice allows for a market to set its un-delegation delay time. (See SIP-366). By default, there is no delay for undelegation. - * @param marketId the id of the market that wants to set un-delegation delay time. - * @param undelegateCollateralDelay the minimum number of delay seconds to un-delegation - */ - function setUndelegateCollateralDelay( - uint128 marketId, - uint32 undelegateCollateralDelay - ) external; - - /** - * @notice Retrieve the un-delegation delay time of a market - * @param marketId the id of the market + * @notice allows for a market to set its minimum delegation time. This is useful for preventing stakers from frontrunning rewards or losses + * by limiting the frequency of `delegateCollateral` (or `setPoolConfiguration`) calls. By default, there is no minimum delegation time. + * @param marketId the id of the market that wants to set delegation time. + * @param minDelegateTime the minimum number of seconds between delegation calls. Note: this value must be less than the globally defined maximum minDelegateTime */ - function getUndelegateCollateralDelay(uint128 marketId) external view returns (uint32); - - /** - * @notice allows for a market to set its un-delegation window time. (See SIP-366). By default, (or if it's set to zero) there no window limit for undelegation. - * @param marketId the id of the market that wants to set un-delegation window time. - * @param undelegateCollateralWindow the maximum number of seconds that an undelegation can be executed after the delay. - */ - function setUndelegateCollateralWindow( - uint128 marketId, - uint32 undelegateCollateralWindow - ) external; - - /** - * @notice Retrieve the un-delegation window of a market - * @param marketId the id of the market - */ - function getUndelegateCollateralWindow(uint128 marketId) external view returns (uint32); - - /** - * @notice allows for a market to set its delegation delay time. (See SIP-366). By default, there is no delay for undelegation. - * @param marketId the id of the market that wants to set delegation delay time. - * @param delegateCollateralDelay the minimum number of delay seconds to delegation - */ - function setDelegateCollateralDelay(uint128 marketId, uint32 delegateCollateralDelay) external; + function setMarketMinDelegateTime(uint128 marketId, uint32 minDelegateTime) external; /** * @notice Retrieve the minimum delegation time of a market * @param marketId the id of the market */ - function getDelegateCollateralDelay(uint128 marketId) external view returns (uint32); + function getMarketMinDelegateTime(uint128 marketId) external view returns (uint32); /** - * @notice allows for a market to set its delegation window time. (See SIP-366). By default, (or if it's set to zero) there no window limit for delegation. - * @param marketId the id of the market that wants to set delegation window time. + * @notice allows for a market to set its delegation and undelegation delay and window times. (See SIP-366). By default, there is no delay and infinite windows. + * @param marketId the id of the market that wants to set delegation times. + * @param delegateCollateralDelay the minimum number of delay seconds to delegation * @param delegateCollateralWindow the maximum number of seconds that an delegation can be executed after the delay. + * @param undelegateCollateralDelay the minimum number of delay seconds to un-delegation + * @param undelegateCollateralWindow the maximum number of seconds that an undelegation can be executed after the delay. */ - function setDelegateCollateralWindow( + function setDelegationCollateralConfiguration( uint128 marketId, - uint32 delegateCollateralWindow + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, + uint32 undelegateCollateralWindow ) external; /** - * @notice Retrieve the delegation window of a market + * @notice Retrieve the delegation and undelegation delay and window times of a market * @param marketId the id of the market */ - function getDelegateCollateralWindow(uint128 marketId) external view returns (uint32); + function getDelegationCollateralConfiguration( + uint128 marketId + ) + external + view + returns ( + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, + uint32 undelegateCollateralWindow + ); /** * @notice Allows the system owner (not the pool owner) to set a market-specific minimum liquidity ratio. diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 527a34512d..83529225d1 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -62,6 +62,11 @@ interface IVaultModule { uint256 currentCollateralAmount ); + /** + * @notice Thrown when the both legacy and two steps delegation is enabled. + */ + error LegacyAndTwoStepsDelegateCollateralEnabled(); + /** * @notice Emitted when {sender} updates the delegation of collateral in the specified liquidity position. * @param accountId The id of the account whose position was updated. @@ -148,6 +153,30 @@ interface IVaultModule { address collateralType ); + /** + * @notice Updates an account's delegated collateral amount for the specified pool and collateral type pair. + * @param accountId The id of the account associated with the position that will be updated. + * @param poolId The id of the pool associated with the position. + * @param collateralType The address of the collateral used in the position. + * @param amount The new amount of collateral delegated in the position, denominated with 18 decimals of precision. + * @param leverage The new leverage amount used in the position, denominated with 18 decimals of precision. + * + * Requirements: + * + * - `ERC2771Context._msgSender()` must be the owner of the account, have the `ADMIN` permission, or have the `DELEGATE` permission. + * - If increasing the amount delegated, it must not exceed the available collateral (`getAccountAvailableCollateral`) associated with the account. + * - If decreasing the amount delegated, the liquidity position must have a collateralization ratio greater than the target collateralization ratio for the corresponding collateral type. + * + * Emits a {DelegationUpdated} event. + */ + function delegateCollateral( + uint128 accountId, + uint128 poolId, + address collateralType, + uint256 amount, + uint256 leverage + ) external; + /** * @notice Declare an intent to update the delegated amount for the specified pool and collateral type pair. * @param accountId The id of the account associated with the position that intends to update the collateral amount. diff --git a/protocol/synthetix/contracts/mocks/MockMarket.sol b/protocol/synthetix/contracts/mocks/MockMarket.sol index cb07608d0f..d8a662b774 100644 --- a/protocol/synthetix/contracts/mocks/MockMarket.sol +++ b/protocol/synthetix/contracts/mocks/MockMarket.sol @@ -102,29 +102,31 @@ contract MockMarket is IMarket { _price = newPrice; } - function setUndelegateCollateralDelay(uint32 undelegateCollateralDelay) external { - IMarketManagerModule(_proxy).setUndelegateCollateralDelay( - _marketId, - undelegateCollateralDelay - ); + function setMinDelegationTime(uint32 minDelegationTime) external { + IMarketManagerModule(_proxy).setMarketMinDelegateTime(_marketId, minDelegationTime); } - function setUndelegateCollateralWindow(uint32 undelegateCollateralWindow) external { - IMarketManagerModule(_proxy).setUndelegateCollateralWindow( + function setDelegationCollateralConfiguration( + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, + uint32 undelegateCollateralWindow + ) external { + IMarketManagerModule(_proxy).setDelegationCollateralConfiguration( _marketId, + delegateCollateralDelay, + delegateCollateralWindow, + undelegateCollateralDelay, undelegateCollateralWindow ); } - function setDelegateCollateralDelay(uint32 delegateCollateralDelay) external { - IMarketManagerModule(_proxy).setDelegateCollateralDelay(_marketId, delegateCollateralDelay); - } - - function setDelegateCollateralWindow(uint32 delegateCollateralWindow) external { - IMarketManagerModule(_proxy).setDelegateCollateralWindow( - _marketId, - delegateCollateralWindow - ); + function getDelegationCollateralConfiguration() + external + view + returns (uint32, uint32, uint32, uint32) + { + return IMarketManagerModule(_proxy).getDelegationCollateralConfiguration(_marketId); } function price() external view returns (uint256) { diff --git a/protocol/synthetix/contracts/modules/core/LiquidationModule.sol b/protocol/synthetix/contracts/modules/core/LiquidationModule.sol index eaba2260e5..c203af7edb 100644 --- a/protocol/synthetix/contracts/modules/core/LiquidationModule.sol +++ b/protocol/synthetix/contracts/modules/core/LiquidationModule.sol @@ -14,8 +14,6 @@ import "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; import "@synthetixio/core-modules/contracts/storage/FeatureFlag.sol"; -import "../../storage/AccountDelegationIntents.sol"; - /** * @title Module for liquidated positions and vaults that are below the liquidation ratio. * @dev See ILiquidationModule. @@ -35,7 +33,7 @@ contract LiquidationModule is ILiquidationModule { using VaultEpoch for VaultEpoch.Data; using Distribution for Distribution.Data; using ScalableMapping for ScalableMapping.Data; - using AccountDelegationIntents for AccountDelegationIntents.Data; + using Account for Account.Data; bytes32 private constant _USD_TOKEN = "USDToken"; @@ -118,7 +116,7 @@ contract LiquidationModule is ILiquidationModule { ); // Clean any outstanding intents to delegate collateral - AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + Account.load(accountId).cleanAllIntents(); emit Liquidation( accountId, diff --git a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol index a5b1b57501..e9f3281aaa 100644 --- a/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol +++ b/protocol/synthetix/contracts/modules/core/MarketManagerModule.sol @@ -43,6 +43,7 @@ contract MarketManagerModule is IMarketManagerModule { bytes32 private constant _DEPOSIT_MARKET_FEATURE_FLAG = "depositMarketUsd"; bytes32 private constant _WITHDRAW_MARKET_FEATURE_FLAG = "withdrawMarketUsd"; + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_RATIO = "depositMarketUsd_feeRatio"; bytes32 private constant _CONFIG_WITHDRAW_MARKET_USD_FEE_RATIO = "withdrawMarketUsd_feeRatio"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_ADDRESS = "depositMarketUsd_feeAddress"; @@ -328,61 +329,49 @@ contract MarketManagerModule is IMarketManagerModule { /** * @inheritdoc IMarketManagerModule */ - function setUndelegateCollateralDelay( - uint128 marketId, - uint32 undelegateCollateralDelay - ) external override { + function setMarketMinDelegateTime(uint128 marketId, uint32 minDelegateTime) external override { Market.Data storage market = Market.load(marketId); if (ERC2771Context._msgSender() != market.marketAddress) revert AccessError.Unauthorized(ERC2771Context._msgSender()); - market.undelegateCollateralDelay = undelegateCollateralDelay; - - emit SetUndelegateCollateralDelay(marketId, undelegateCollateralDelay); - } - - /** - * @inheritdoc IMarketManagerModule - */ - function getUndelegateCollateralDelay( - uint128 marketId - ) external view override returns (uint32) { - return Market.load(marketId).undelegateCollateralDelay; - } - - /** - * @inheritdoc IMarketManagerModule - */ - function setUndelegateCollateralWindow( - uint128 marketId, - uint32 undelegateCollateralWindow - ) external override { - Market.Data storage market = Market.load(marketId); + // min delegate time should not be unreasonably long + uint256 maxMinDelegateTime = Config.readUint( + _CONFIG_SET_MARKET_MIN_DELEGATE_MAX, + 86400 * 30 + ); - if (ERC2771Context._msgSender() != market.marketAddress) - revert AccessError.Unauthorized(ERC2771Context._msgSender()); + if (minDelegateTime > maxMinDelegateTime) { + revert ParameterError.InvalidParameter("minDelegateTime", "must not be too large"); + } - market.undelegateCollateralWindow = undelegateCollateralWindow; + market.minDelegateTime = minDelegateTime; - emit SetUndelegateCollateralWindow(marketId, undelegateCollateralWindow); + emit SetMinDelegateTime(marketId, minDelegateTime); } /** * @inheritdoc IMarketManagerModule */ - function getUndelegateCollateralWindow( - uint128 marketId - ) external view override returns (uint32) { - return Market.load(marketId).undelegateCollateralWindow; + function getMarketMinDelegateTime(uint128 marketId) external view override returns (uint32) { + // solhint-disable-next-line numcast/safe-cast + uint32 maxMinDelegateTime = uint32( + Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) + ); + uint32 marketMinDelegateTime = Market.load(marketId).minDelegateTime; + return + maxMinDelegateTime < marketMinDelegateTime ? maxMinDelegateTime : marketMinDelegateTime; } /** * @inheritdoc IMarketManagerModule */ - function setDelegateCollateralDelay( + function setDelegationCollateralConfiguration( uint128 marketId, - uint32 delegateCollateralDelay + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, + uint32 undelegateCollateralWindow ) external override { Market.Data storage market = Market.load(marketId); @@ -390,39 +379,41 @@ contract MarketManagerModule is IMarketManagerModule { revert AccessError.Unauthorized(ERC2771Context._msgSender()); market.delegateCollateralDelay = delegateCollateralDelay; + market.delegateCollateralWindow = delegateCollateralWindow; + market.undelegateCollateralDelay = undelegateCollateralDelay; + market.undelegateCollateralWindow = undelegateCollateralWindow; - emit SetDelegateCollateralDelay(marketId, delegateCollateralDelay); - } - - /** - * @inheritdoc IMarketManagerModule - */ - function getDelegateCollateralDelay(uint128 marketId) external view override returns (uint32) { - return Market.load(marketId).delegateCollateralDelay; + emit SetDelegateCollateralConfiguration( + marketId, + delegateCollateralDelay, + delegateCollateralWindow, + undelegateCollateralDelay, + undelegateCollateralWindow + ); } /** * @inheritdoc IMarketManagerModule */ - function setDelegateCollateralWindow( - uint128 marketId, - uint32 delegateCollateralWindow - ) external override { + function getDelegationCollateralConfiguration( + uint128 marketId + ) + external + view + override + returns ( + uint32 delegateCollateralDelay, + uint32 delegateCollateralWindow, + uint32 undelegateCollateralDelay, + uint32 undelegateCollateralWindow + ) + { Market.Data storage market = Market.load(marketId); - if (ERC2771Context._msgSender() != market.marketAddress) - revert AccessError.Unauthorized(ERC2771Context._msgSender()); - - market.delegateCollateralWindow = delegateCollateralWindow; - - emit SetDelegateCollateralWindow(marketId, delegateCollateralWindow); - } - - /** - * @inheritdoc IMarketManagerModule - */ - function getDelegateCollateralWindow(uint128 marketId) external view override returns (uint32) { - return Market.load(marketId).delegateCollateralWindow; + delegateCollateralDelay = market.delegateCollateralDelay; + delegateCollateralWindow = market.delegateCollateralWindow; + undelegateCollateralDelay = market.undelegateCollateralDelay; + undelegateCollateralWindow = market.undelegateCollateralWindow; } /** diff --git a/protocol/synthetix/contracts/modules/core/PoolModule.sol b/protocol/synthetix/contracts/modules/core/PoolModule.sol index 851fb3e061..08bf8a8cbd 100644 --- a/protocol/synthetix/contracts/modules/core/PoolModule.sol +++ b/protocol/synthetix/contracts/modules/core/PoolModule.sol @@ -148,7 +148,7 @@ contract PoolModule is IPoolModule { ) external override { Pool.Data storage pool = Pool.loadExisting(poolId); Pool.onlyPoolOwner(poolId, ERC2771Context._msgSender()); - + pool.requireMinDelegationTimeElapsed(pool.lastConfigurationTime); // Update each market's pro-rata liquidity and collect accumulated debt into the pool's debt distribution. // Note: This follows the same pattern as Pool.recalculateVaultCollateral(), // where we need to distribute the debt, adjust the market configurations and distribute again. diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index ea7b440aca..c840f4c1a5 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -38,8 +38,42 @@ contract VaultModule is IVaultModule { using SafeCastI256 for int256; using AccountDelegationIntents for AccountDelegationIntents.Data; using DelegationIntent for DelegationIntent.Data; + using Account for Account.Data; bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; + bytes32 private constant _TWO_STEPS_DELEGATE_FEATURE_FLAG = "twoStepsDelegateCollateral"; + + /** + * @inheritdoc IVaultModule + */ + function delegateCollateral( + uint128 accountId, + uint128 poolId, + address collateralType, + uint256 newCollateralAmountD18, + uint256 leverage + ) external override { + FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); + Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + if (FeatureFlag.hasAccess(_TWO_STEPS_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { + revert LegacyAndTwoStepsDelegateCollateralEnabled(); + } + + // System only supports leverage of 1.0 for now. + if (leverage != DecimalMath.UNIT) revert InvalidLeverage(leverage); + + // Identify the vault that corresponds to this collateral type and pool id. + Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; + + uint256 currentCollateralAmount = vault.currentAccountCollateral(accountId); + + int256 deltaCollateralAmountD18 = newCollateralAmountD18.toInt() - + currentCollateralAmount.toInt(); + + if (newCollateralAmountD18 == currentCollateralAmount) revert InvalidCollateralAmount(); + + _delegateCollateral(accountId, poolId, collateralType, deltaCollateralAmountD18, leverage); + } /** * @inheritdoc IVaultModule @@ -52,8 +86,14 @@ contract VaultModule is IVaultModule { uint256 leverage ) external override returns (uint256 intentId) { // Ensure the caller is authorized to represent the account. - FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); - Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); + FeatureFlag.ensureAccessToFeature(_TWO_STEPS_DELEGATE_FEATURE_FLAG); + Account.Data storage account = Account.loadAccountAndValidatePermission( + accountId, + AccountRBAC._DELEGATE_PERMISSION + ); + if (FeatureFlag.hasAccess(_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { + revert LegacyAndTwoStepsDelegateCollateralEnabled(); + } // Input checks // System only supports leverage of 1.0 for now. @@ -63,9 +103,9 @@ contract VaultModule is IVaultModule { // Verify the account holds enough collateral to execute the intent. // Get previous intents cache - AccountDelegationIntents.Data storage accountIntents = AccountDelegationIntents.getValid( - accountId - ); + AccountDelegationIntents.Data storage accountIntents = account.delegationIntents[ + account.currentDelegationIntentsEpoch + ]; // Identify the vault that corresponds to this collateral type and pool id. Vault.Data storage vault = Pool.loadExisting(poolId).vaults[collateralType]; @@ -124,7 +164,7 @@ contract VaultModule is IVaultModule { intent.declarationTime = block.timestamp.to32(); // Add intent to the account's delegation intents. - AccountDelegationIntents.getValid(intent.accountId).addIntent(intent); + accountIntents.addIntent(intent); // emit an event emit DelegationIntentDeclared( @@ -148,10 +188,15 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256[] memory intentIds ) public override { - FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); - AccountDelegationIntents.Data storage accountIntents = AccountDelegationIntents.loadValid( - accountId - ); + FeatureFlag.ensureAccessToFeature(_TWO_STEPS_DELEGATE_FEATURE_FLAG); + if (FeatureFlag.hasAccess(_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { + revert LegacyAndTwoStepsDelegateCollateralEnabled(); + } + + AccountDelegationIntents.Data storage accountIntents = Account + .load(accountId) + .getDelegationIntents(); + for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (!accountIntents.isInCurrentEpoch(intent.id)) { @@ -169,7 +214,7 @@ contract VaultModule is IVaultModule { // If expired, remove the intent. if (intent.intentExpired()) { - AccountDelegationIntents.getValid(accountId).removeIntent(intent); + accountIntents.removeIntent(intent); emit DelegationIntentRemoved( intent.id, accountId, @@ -195,7 +240,7 @@ contract VaultModule is IVaultModule { ); // Remove the intent. - AccountDelegationIntents.getValid(accountId).removeIntent(intent); + accountIntents.removeIntent(intent); emit DelegationIntentRemoved( intent.id, accountId, @@ -223,7 +268,7 @@ contract VaultModule is IVaultModule { ) external override { processIntentToDelegateCollateralByIntents( accountId, - AccountDelegationIntents.getValid(accountId).intentIdsByPair(poolId, collateralType) + Account.load(accountId).getDelegationIntents().intentIdsByPair(poolId, collateralType) ); } @@ -232,7 +277,7 @@ contract VaultModule is IVaultModule { */ function forceDeleteAllAccountIntents(uint128 accountId) external override { OwnableStorage.onlyOwner(); - AccountDelegationIntents.getValid(accountId).cleanAllIntents(); + Account.load(accountId).cleanAllIntents(); } /** @@ -240,9 +285,12 @@ contract VaultModule is IVaultModule { */ function forceDeleteIntents(uint128 accountId, uint256[] calldata intentIds) external override { OwnableStorage.onlyOwner(); + AccountDelegationIntents.Data storage accountIntents = Account + .load(accountId) + .getDelegationIntents(); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - AccountDelegationIntents.getValid(accountId).removeIntent(intent); + accountIntents.removeIntent(intent); } } @@ -250,7 +298,7 @@ contract VaultModule is IVaultModule { * @inheritdoc IVaultModule */ function deleteAllExpiredIntents(uint128 accountId) external override { - AccountDelegationIntents.getValid(accountId).cleanAllExpiredIntents(); + Account.load(accountId).getDelegationIntents().cleanAllExpiredIntents(); } /** @@ -260,6 +308,9 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256[] calldata intentIds ) external override { + AccountDelegationIntents.Data storage accountIntents = Account + .load(accountId) + .getDelegationIntents(); for (uint256 i = 0; i < intentIds.length; i++) { DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); if (intent.accountId != accountId) { @@ -268,7 +319,7 @@ contract VaultModule is IVaultModule { if (!intent.intentExpired()) { revert DelegationIntentNotExpired(intent.id); } - AccountDelegationIntents.getValid(accountId).removeIntent(intent); + accountIntents.removeIntent(intent); } } @@ -369,8 +420,9 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256 intentId ) external view override returns (uint128, address, int256, uint256, uint32) { - DelegationIntent.Data storage intent = AccountDelegationIntents - .loadValid(accountId) + DelegationIntent.Data storage intent = Account + .load(accountId) + .getDelegationIntents() .getIntent(intentId); return ( intent.poolId, @@ -387,7 +439,7 @@ contract VaultModule is IVaultModule { function getAccountIntentIds( uint128 accountId ) external view override returns (uint256[] memory) { - return AccountDelegationIntents.loadValid(accountId).intentsId.values(); + return Account.load(accountId).getDelegationIntents().intentsId.values(); } /** @@ -397,8 +449,9 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256 maxProcessableIntent ) external view override returns (uint256[] memory expiredIntents, uint256 foundItems) { - uint256[] memory allIntents = AccountDelegationIntents - .loadValid(accountId) + uint256[] memory allIntents = Account + .load(accountId) + .getDelegationIntents() .intentsId .values(); uint256 max = maxProcessableIntent > allIntents.length @@ -420,8 +473,9 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256 maxProcessableIntent ) external view override returns (uint256[] memory executableIntents, uint256 foundItems) { - uint256[] memory allIntents = AccountDelegationIntents - .loadValid(accountId) + uint256[] memory allIntents = Account + .load(accountId) + .getDelegationIntents() .intentsId .values(); uint256 max = maxProcessableIntent > allIntents.length @@ -444,7 +498,7 @@ contract VaultModule is IVaultModule { address collateralType ) external view override returns (int256) { return - AccountDelegationIntents.loadValid(accountId).netDelegatedAmountPerCollateral[ + Account.load(accountId).getDelegationIntents().netDelegatedAmountPerCollateral[ collateralType ]; } @@ -457,7 +511,7 @@ contract VaultModule is IVaultModule { uint128 poolId, address collateralType ) external view override returns (int256 accumulatedIntentDelta) { - uint256[] memory intentIds = AccountDelegationIntents.loadValid(accountId).intentIdsByPair( + uint256[] memory intentIds = Account.load(accountId).getDelegationIntents().intentIdsByPair( poolId, collateralType ); @@ -578,6 +632,11 @@ contract VaultModule is IVaultModule { collateralType, deltaCollateralAmountD18.toUint() ); + // if decreasing delegation amount, ensure min time has elapsed + } else { + Pool.loadExisting(poolId).requireMinDelegationTimeElapsed( + vault.currentEpoch().lastDelegationTime[accountId] + ); } // Update the account's position for the given pool and collateral type, @@ -622,6 +681,9 @@ contract VaultModule is IVaultModule { _verifyNotCapacityLocked(poolId); } + // solhint-disable-next-line numcast/safe-cast + vault.currentEpoch().lastDelegationTime[accountId] = uint64(block.timestamp); + emit DelegationUpdated( accountId, poolId, diff --git a/protocol/synthetix/contracts/storage/Account.sol b/protocol/synthetix/contracts/storage/Account.sol index 2bfdff379d..d9c398a4c0 100644 --- a/protocol/synthetix/contracts/storage/Account.sol +++ b/protocol/synthetix/contracts/storage/Account.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.11 <0.9.0; import "./AccountRBAC.sol"; import "./Collateral.sol"; import "./Pool.sol"; +import "./AccountDelegationIntents.sol"; import "../interfaces/ICollateralModule.sol"; @@ -62,6 +63,10 @@ library Account { * @dev Address set of collaterals that are being used in the system by this account. */ mapping(address => Collateral.Data) collaterals; + /** + * @dev Delegation Intents by epoch. Will use `currentDelegationIndentsEpoch` to point to the latest active delegation intents for this account. + */ + mapping(uint128 => AccountDelegationIntents.Data) delegationIntents; } /** @@ -211,11 +216,16 @@ library Account { } } + function getDelegationIntents( + Data storage self + ) internal view returns (AccountDelegationIntents.Data storage) { + return self.delegationIntents[self.currentDelegationIntentsEpoch]; + } + /** - * @dev Returns the new delegation intents epoch (by incrementing the currentDelegationIntentsEpoch). + * @dev It "deletes" all the account intents by moving to a new delegation intents epoch */ - function getNewDelegationIntentsEpoch(Data storage self) internal returns (uint128) { + function cleanAllIntents(Data storage self) internal { self.currentDelegationIntentsEpoch += 1; - return self.currentDelegationIntentsEpoch; } } diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 74d86b411a..40290175f1 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -20,8 +20,6 @@ library AccountDelegationIntents { using Account for Account.Data; struct Data { - uint128 accountId; - uint128 delegationIntentsEpoch; // nonce used to nuke previous intents using a new era (useful on liquidations) SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; // poolId/collateralType => intentIds[] // accounting for the intents collateral delegated @@ -30,56 +28,6 @@ library AccountDelegationIntents { mapping(address => int256) netDelegatedAmountPerCollateral; // collateralType => net delegatedCollateralAmount } - /** - * @dev Returns the account delegation intents stored at the specified account id. - */ - function load( - uint128 accountId, - uint128 delegationIntentsEpoch - ) internal pure returns (Data storage accountDelegationIntents) { - bytes32 s = keccak256( - abi.encode( - "io.synthetix.synthetix.AccountDelegationIntents", - accountId, - delegationIntentsEpoch - ) - ); - assembly { - accountDelegationIntents.slot := s - } - } - - /** - * @dev Returns the account delegation intents stored at the specified account id. - */ - function loadValid( - uint128 accountId - ) internal view returns (Data storage accountDelegationIntents) { - uint128 delegationIntentsEpoch = Account.load(accountId).currentDelegationIntentsEpoch; - accountDelegationIntents = load(accountId, delegationIntentsEpoch); - if ( - accountDelegationIntents.accountId != 0 && - (accountDelegationIntents.accountId != accountId || - accountDelegationIntents.delegationIntentsEpoch != delegationIntentsEpoch) - ) { - revert IVaultModule.InvalidDelegationIntent(); - } - } - - /** - * @dev Returns the account delegation intents stored at the specified account id. Checks if it's valid - */ - function getValid(uint128 accountId) internal returns (Data storage accountDelegationIntents) { - accountDelegationIntents = loadValid(accountId); - if (accountDelegationIntents.accountId == 0) { - // Uninitialized storage will have a 0 accountId; it means we need to initialize it (new accountDelegationIntents era) - accountDelegationIntents.accountId = accountId; - accountDelegationIntents.delegationIntentsEpoch = Account - .load(accountId) - .currentDelegationIntentsEpoch; - } - } - function addIntent(Data storage self, DelegationIntent.Data storage delegationIntent) internal { self.intentsId.add(delegationIntent.id); self @@ -158,13 +106,4 @@ library AccountDelegationIntents { } } } - - /** - * @dev Cleans all intents (expired and not) related to the account. This should be called upon liquidation. - */ - function cleanAllIntents(Data storage self) internal { - // Nuke all intents by incrementing the delegationIntentsEpoch nonce - // This is useful to avoid iterating over all intents to remove them and risking a for loop revert. - Account.load(self.accountId).getNewDelegationIntentsEpoch(); - } } diff --git a/protocol/synthetix/contracts/storage/Market.sol b/protocol/synthetix/contracts/storage/Market.sol index 7e31f43baa..bf16b9f94b 100644 --- a/protocol/synthetix/contracts/storage/Market.sol +++ b/protocol/synthetix/contracts/storage/Market.sol @@ -141,13 +141,13 @@ library Market { /** * @dev Delegation/Undelegation frontrunning protection. */ - uint32 __unusedLegacyStorageSlot; - uint32 undelegateCollateralDelay; - uint32 undelegateCollateralWindow; - uint32 delegateCollateralDelay; - uint32 delegateCollateralWindow; - uint32 __reservedForLater1; - uint64 __reservedForLater2; + uint32 minDelegateTime; // Accumulated Alignment 32 + uint32 undelegateCollateralDelay; // Accumulated Alignment 64 + uint32 undelegateCollateralWindow; // Accumulated Alignment 96 + uint32 delegateCollateralDelay; // Accumulated Alignment 128 + uint32 delegateCollateralWindow; // Accumulated Alignment 160 + uint32 __reservedForLater1; // Accumulated Alignment 192 + uint64 __reservedForLater2; // Accumulated Alignment 256 /** * @dev Market-specific override of the minimum liquidity ratio */ diff --git a/protocol/synthetix/contracts/storage/Pool.sol b/protocol/synthetix/contracts/storage/Pool.sol index 8d0572c49d..47844cb55d 100644 --- a/protocol/synthetix/contracts/storage/Pool.sol +++ b/protocol/synthetix/contracts/storage/Pool.sol @@ -61,6 +61,11 @@ library Pool { uint256 maxCollateral ); + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_DELAY_MIN = "delegateCollateralDelay_min"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_WINDOW_MAX = + "delegateCollateralWindow_max"; + struct Data { /** * @dev Numeric identifier for the pool. Must be unique. @@ -418,10 +423,43 @@ library Pool { return Market.load(0); } + function getRequiredMinDelegationTime( + Data storage self + ) internal view returns (uint32 requiredMinDelegateTime) { + for (uint256 i = 0; i < self.marketConfigurations.length; i++) { + uint32 marketMinDelegateTime = Market + .load(self.marketConfigurations[i].marketId) + .minDelegateTime; + + if (marketMinDelegateTime > requiredMinDelegateTime) { + requiredMinDelegateTime = marketMinDelegateTime; + } + } + + // solhint-disable-next-line numcast/safe-cast + uint32 maxMinDelegateTime = uint32( + Config.readUint(_CONFIG_SET_MARKET_MIN_DELEGATE_MAX, 86400 * 30) + ); + return + maxMinDelegateTime < requiredMinDelegateTime + ? maxMinDelegateTime + : requiredMinDelegateTime; + } + function getRequiredDelegationDelayAndWindow( Data storage self, bool isUndelegation ) internal view returns (uint32 requiredDelayTime, uint32 requiredWindowTime) { + // solhint-disable-next-line numcast/safe-cast + uint32 globalMinDelegateDelay = uint32( + Config.readUint(_CONFIG_DELEGATE_COLLATERAL_DELAY_MIN, 0) + ); + + // solhint-disable-next-line numcast/safe-cast + uint32 globalMaxDelegateWindow = uint32( + Config.readUint(_CONFIG_DELEGATE_COLLATERAL_WINDOW_MAX, 0) + ); + for (uint256 i = 0; i < self.marketConfigurations.length; i++) { Market.Data storage market = Market.load(self.marketConfigurations[i].marketId); uint32 marketDelayTime = isUndelegation @@ -438,6 +476,15 @@ library Pool { : market.delegateCollateralWindow; } } + + // Apply global limits if set. + if (globalMinDelegateDelay > 0 && globalMinDelegateDelay > requiredDelayTime) { + requiredDelayTime = globalMinDelegateDelay; + } + + if (globalMaxDelegateWindow > 0 && globalMaxDelegateWindow < requiredWindowTime) { + requiredWindowTime = globalMaxDelegateWindow; + } } /** @@ -517,6 +564,20 @@ library Pool { } } + function requireMinDelegationTimeElapsed( + Data storage self, + uint64 lastDelegationTime + ) internal view { + uint32 requiredMinDelegationTime = getRequiredMinDelegationTime(self); + if (block.timestamp < lastDelegationTime + requiredMinDelegationTime) { + revert MinDelegationTimeoutPending( + self.id, + // solhint-disable-next-line numcast/safe-cast + uint32(lastDelegationTime + requiredMinDelegationTime - block.timestamp) + ); + } + } + function checkPoolCollateralLimit( Data storage self, address collateralType, diff --git a/protocol/synthetix/contracts/storage/VaultEpoch.sol b/protocol/synthetix/contracts/storage/VaultEpoch.sol index ef82be07ad..69d5c84121 100644 --- a/protocol/synthetix/contracts/storage/VaultEpoch.sol +++ b/protocol/synthetix/contracts/storage/VaultEpoch.sol @@ -64,7 +64,12 @@ library VaultEpoch { * and directly when users mint or burn USD, or repay debt. */ mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) __unused_legacy_slot; + /** + * @dev Tracks last time a user delegated to this vault. + * + * Needed to validate min delegation time compliance to prevent small scale debt pool frontrunning + */ + mapping(uint128 => uint64) lastDelegationTime; } /** diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index 42c0ab4896..03668a2430 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -353,6 +353,7 @@ contract MarketManagerModule { bytes32 private constant _MARKET_FEATURE_FLAG = "registerMarket"; bytes32 private constant _DEPOSIT_MARKET_FEATURE_FLAG = "depositMarketUsd"; bytes32 private constant _WITHDRAW_MARKET_FEATURE_FLAG = "withdrawMarketUsd"; + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_RATIO = "depositMarketUsd_feeRatio"; bytes32 private constant _CONFIG_WITHDRAW_MARKET_USD_FEE_RATIO = "withdrawMarketUsd_feeRatio"; bytes32 private constant _CONFIG_DEPOSIT_MARKET_USD_FEE_ADDRESS = "depositMarketUsd_feeAddress"; @@ -381,6 +382,7 @@ contract UtilsModule { // @custom:artifact contracts/modules/core/VaultModule.sol:VaultModule contract VaultModule { bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; + bytes32 private constant _TWO_STEPS_DELEGATE_FEATURE_FLAG = "twoStepsDelegateCollateral"; } // @custom:artifact contracts/modules/usd/USDTokenModule.sol:USDTokenModule @@ -397,6 +399,7 @@ library Account { uint64 __slotAvailableForFutureUse; uint128 currentDelegationIntentsEpoch; mapping(address => Collateral.Data) collaterals; + mapping(uint128 => AccountDelegationIntents.Data) delegationIntents; } function load(uint128 id) internal pure returns (Data storage account) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.Account", id)); @@ -409,19 +412,11 @@ library Account { // @custom:artifact contracts/storage/AccountDelegationIntents.sol:AccountDelegationIntents library AccountDelegationIntents { struct Data { - uint128 accountId; - uint128 delegationIntentsEpoch; SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; SetUtil.AddressSet delegatedCollaterals; mapping(address => int256) netDelegatedAmountPerCollateral; } - function load(uint128 accountId, uint128 delegationIntentsEpoch) internal pure returns (Data storage accountDelegationIntents) { - bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.AccountDelegationIntents", accountId, delegationIntentsEpoch)); - assembly { - accountDelegationIntents.slot := s - } - } } // @custom:artifact contracts/storage/AccountRBAC.sol:AccountRBAC @@ -559,7 +554,7 @@ library Market { mapping(uint128 => MarketPoolInfo.Data) pools; DepositedCollateral[] depositedCollateral; mapping(address => uint256) maximumDepositableD18; - uint32 __unusedLegacyStorageSlot; + uint32 minDelegateTime; uint32 undelegateCollateralDelay; uint32 undelegateCollateralWindow; uint32 delegateCollateralDelay; @@ -628,6 +623,9 @@ library OracleManager { // @custom:artifact contracts/storage/Pool.sol:Pool library Pool { + bytes32 private constant _CONFIG_SET_MARKET_MIN_DELEGATE_MAX = "setMarketMinDelegateTime_max"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_DELAY_MIN = "delegateCollateralDelay_min"; + bytes32 private constant _CONFIG_DELEGATE_COLLATERAL_WINDOW_MAX = "delegateCollateralWindow_max"; struct Data { uint128 id; string name; @@ -749,7 +747,7 @@ library VaultEpoch { Distribution.Data accountsDebtDistribution; ScalableMapping.Data collateralAmounts; mapping(uint256 => int256) consolidatedDebtAmountsD18; - mapping(uint128 => uint64) __unused_legacy_slot; + mapping(uint128 => uint64) lastDelegationTime; } } diff --git a/protocol/synthetix/test/common/stakedPool.ts b/protocol/synthetix/test/common/stakedPool.ts index 99fb10194f..9211380ff4 100644 --- a/protocol/synthetix/test/common/stakedPool.ts +++ b/protocol/synthetix/test/common/stakedPool.ts @@ -8,11 +8,16 @@ import { bootstrap } from '../integration/bootstrap'; export const bn = (n: number) => wei(n).toBN(); const POOL_FEATURE_FLAG = ethers.utils.formatBytes32String('createPool'); +const LEGACY_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String('delegateCollateral'); +const TWO_STEPS_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String( + 'twoStepsDelegateCollateral' +); export const createStakedPool = ( r: ReturnType, stakedCollateralPrice: ethers.BigNumber = bn(1), - stakedAmount: ethers.BigNumber = bn(1000) + stakedAmount: ethers.BigNumber = bn(1000), + useLegacyDelegateCollateral: boolean = false ) => { let aggregator: ethers.Contract; @@ -26,6 +31,16 @@ export const createStakedPool = ( .Core.addToFeatureFlagAllowlist(POOL_FEATURE_FLAG, await r.owner().getAddress()); }); + before('set permissions according to mode', async () => { + // set FF + await r + .systems() + .Core.setFeatureFlagAllowAll(LEGACY_DELEGATION_FEATURE_FLAG, useLegacyDelegateCollateral); + await r + .systems() + .Core.setFeatureFlagAllowAll(TWO_STEPS_DELEGATION_FEATURE_FLAG, !useLegacyDelegateCollateral); + }); + before('setup oracle manager node', async () => { const results = await createOracleNode( r.signers()[0], @@ -64,7 +79,8 @@ export const createStakedPool = ( poolId, accountId, staker, - stakedAmount + stakedAmount, + useLegacyDelegateCollateral ); }); diff --git a/protocol/synthetix/test/common/stakers.ts b/protocol/synthetix/test/common/stakers.ts index 455541ea1e..56179af26b 100644 --- a/protocol/synthetix/test/common/stakers.ts +++ b/protocol/synthetix/test/common/stakers.ts @@ -59,7 +59,8 @@ export const stake = async ( poolId: number, accountId: number, user: ethers.Signer, - delegateAmount: ethers.BigNumber = depositAmount + delegateAmount: ethers.BigNumber = depositAmount, + useLegacyDelegateCollateral: boolean = false ) => { const { Core, CollateralMock } = systems; await CollateralMock.mint(await user.getAddress(), delegateAmount.mul(1000)); @@ -73,33 +74,53 @@ export const stake = async ( // stake collateral await Core.connect(user).deposit(accountId, CollateralMock.address, delegateAmount.mul(300)); - // invest in the pool - await Core.connect(user).declareIntentToDelegateCollateral( - accountId, - poolId, - CollateralMock.address, - delegateAmount, - ethers.utils.parseEther('1') - ); + if (useLegacyDelegateCollateral) { + // invest in the pool + await Core.connect(user).delegateCollateral( + accountId, + poolId, + CollateralMock.address, + delegateAmount, + ethers.utils.parseEther('1') + ); + + // also for convenience invest in the 0 pool + await Core.connect(user).delegateCollateral( + accountId, + 0, + CollateralMock.address, + delegateAmount, + ethers.utils.parseEther('1') + ); + } else { + // invest in the pool + await Core.connect(user).declareIntentToDelegateCollateral( + accountId, + poolId, + CollateralMock.address, + delegateAmount, + ethers.utils.parseEther('1') + ); - await Core.connect(user).processIntentToDelegateCollateralByPair( - accountId, - poolId, - CollateralMock.address - ); + await Core.connect(user).processIntentToDelegateCollateralByPair( + accountId, + poolId, + CollateralMock.address + ); - // also for convenience invest in the 0 pool - await Core.connect(user).declareIntentToDelegateCollateral( - accountId, - 0, - CollateralMock.address, - delegateAmount, - ethers.utils.parseEther('1') - ); + // also for convenience invest in the 0 pool + await Core.connect(user).declareIntentToDelegateCollateral( + accountId, + 0, + CollateralMock.address, + delegateAmount, + ethers.utils.parseEther('1') + ); - await Core.connect(user).processIntentToDelegateCollateralByPair( - accountId, - 0, - CollateralMock.address - ); + await Core.connect(user).processIntentToDelegateCollateralByPair( + accountId, + 0, + CollateralMock.address + ); + } }; diff --git a/protocol/synthetix/test/integration/bootstrap.ts b/protocol/synthetix/test/integration/bootstrap.ts index 539cbc7dfe..e19d37760e 100644 --- a/protocol/synthetix/test/integration/bootstrap.ts +++ b/protocol/synthetix/test/integration/bootstrap.ts @@ -58,8 +58,10 @@ export function bootstrap() { }; } -export function bootstrapWithStakedPool() { - return createStakedPool(bootstrap()); +export function bootstrapWithStakedPool(useLegacyMode: boolean = false) { + const r = createStakedPool(bootstrap(), bn(1), bn(1000), useLegacyMode); + + return r; } export function bootstrapWithMockMarketAndPool() { diff --git a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts index 9ffe313db6..8018c1e3c3 100644 --- a/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/MarketManagerModule.test.ts @@ -552,12 +552,12 @@ describe('MarketManagerModule', function () { }); }); - describe('setDelegateCollateralDelay()', () => { + describe('setDelegationCollateralConfiguration()', () => { before(restore); it('only works for market', async () => { await assertRevert( - systems().Core.setDelegateCollateralDelay(marketId(), 86400), + systems().Core.setDelegationCollateralConfiguration(marketId(), 86400, 86400, 86400, 86400), 'Unauthorized', systems().Core ); @@ -566,96 +566,23 @@ describe('MarketManagerModule', function () { describe('success', () => { let tx: ethers.providers.TransactionResponse; before('exec', async () => { - tx = await MockMarket().setDelegateCollateralDelay(60); + tx = await MockMarket().setDelegationCollateralConfiguration(60, 61, 62, 63); }); - it('sets the value', async () => { - assertBn.equal(await systems().Core.getDelegateCollateralDelay(marketId()), 60); - }); - - it('emits', async () => { - await assertEvent(tx, `SetDelegateCollateralDelay(${marketId()}, 60)`, systems().Core); - }); - }); - }); - - describe('setDelegateCollateralWindow()', () => { - before(restore); - - it('only works for market', async () => { - await assertRevert( - systems().Core.setDelegateCollateralWindow(marketId(), 86400), - 'Unauthorized', - systems().Core - ); - }); - - describe('success', () => { - let tx: ethers.providers.TransactionResponse; - before('exec', async () => { - tx = await MockMarket().setDelegateCollateralWindow(70); - }); - - it('sets the value', async () => { - assertBn.equal(await systems().Core.getDelegateCollateralWindow(marketId()), 70); + it('sets the values', async () => { + const config = await systems().Core.getDelegationCollateralConfiguration(marketId()); + assertBn.equal(config.delegateCollateralDelay, 60); + assertBn.equal(config.delegateCollateralWindow, 61); + assertBn.equal(config.undelegateCollateralDelay, 62); + assertBn.equal(config.undelegateCollateralWindow, 63); }); it('emits', async () => { - await assertEvent(tx, `SetDelegateCollateralWindow(${marketId()}, 70)`, systems().Core); - }); - }); - }); - - describe('setUndelegateCollateralDelay()', () => { - before(restore); - - it('only works for market', async () => { - await assertRevert( - systems().Core.setUndelegateCollateralDelay(marketId(), 86400), - 'Unauthorized', - systems().Core - ); - }); - - describe('success', () => { - let tx: ethers.providers.TransactionResponse; - before('exec', async () => { - tx = await MockMarket().setUndelegateCollateralDelay(80); - }); - - it('sets the value', async () => { - assertBn.equal(await systems().Core.getUndelegateCollateralDelay(marketId()), 80); - }); - - it('emits', async () => { - await assertEvent(tx, `SetUndelegateCollateralDelay(${marketId()}, 80)`, systems().Core); - }); - }); - }); - - describe('setUndelegateCollateralWindow()', () => { - before(restore); - - it('only works for market', async () => { - await assertRevert( - systems().Core.setUndelegateCollateralWindow(marketId(), 86400), - 'Unauthorized', - systems().Core - ); - }); - - describe('success', () => { - let tx: ethers.providers.TransactionResponse; - before('exec', async () => { - tx = await MockMarket().setUndelegateCollateralWindow(90); - }); - - it('sets the value', async () => { - assertBn.equal(await systems().Core.getUndelegateCollateralWindow(marketId()), 90); - }); - - it('emits', async () => { - await assertEvent(tx, `SetUndelegateCollateralWindow(${marketId()}, 90)`, systems().Core); + await assertEvent( + tx, + `SetDelegateCollateralConfiguration(${marketId()}, 60, 61, 62, 63)`, + systems().Core + ); }); }); }); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.legacyDelagate.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.legacyDelagate.test.ts new file mode 100644 index 0000000000..12cd411450 --- /dev/null +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.legacyDelagate.test.ts @@ -0,0 +1,959 @@ +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; +import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; +import assert from 'assert/strict'; +import { BigNumber, constants, ethers } from 'ethers'; +import hre from 'hardhat'; +import { bn, bootstrapWithStakedPool } from '../../bootstrap'; +import Permissions from '../../mixins/AccountRBACMixin.permissions'; +import { verifyUsesFeatureFlag } from '../../verifications'; +import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; +import { wei } from '@synthetixio/wei'; + +describe('VaultModule Legacy Delegate', function () { + const { + signers, + systems, + provider, + accountId, + poolId, + depositAmount, + collateralContract, + collateralAddress, + oracleNodeId, + } = bootstrapWithStakedPool(true); + + const MAX_UINT = ethers.constants.MaxUint256; + + let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer; + + let MockMarket: ethers.Contract; + let marketId: BigNumber; + + before('identify signers', async () => { + [owner, user1, user2] = signers(); + }); + + before('give user1 permission to register market', async () => { + await systems() + .Core.connect(owner) + .addToFeatureFlagAllowlist( + ethers.utils.formatBytes32String('registerMarket'), + await user1.getAddress() + ); + }); + + before('deploy and connect fake market', async () => { + const factory = await hre.ethers.getContractFactory('MockMarket'); + + MockMarket = await factory.connect(owner).deploy(); + + marketId = await systems().Core.connect(user1).callStatic.registerMarket(MockMarket.address); + + await systems().Core.connect(user1).registerMarket(MockMarket.address); + + await MockMarket.connect(owner).initialize( + systems().Core.address, + marketId, + ethers.utils.parseEther('1') + ); + + await systems() + .Core.connect(owner) + .setPoolConfiguration(poolId, [ + { + marketId: marketId, + weightD18: ethers.utils.parseEther('1'), + maxDebtShareValueD18: ethers.utils.parseEther('10000000000000000'), + }, + ]); + }); + + before('add second collateral type', async () => { + // add collateral + await ( + await systems().Core.connect(owner).configureCollateral({ + tokenAddress: systems().Collateral2Mock.address, + oracleNodeId: oracleNodeId(), + issuanceRatioD18: '5000000000000000000', + liquidationRatioD18: '1500000000000000000', + liquidationRewardD18: '20000000000000000000', + minDelegationD18: '20000000000000000000', + depositingEnabled: true, + }) + ).wait(); + + await systems() + .Core.connect(owner) + .configureCollateral({ + tokenAddress: await systems().Core.getUsdToken(), + oracleNodeId: ethers.utils.formatBytes32String(''), + issuanceRatioD18: bn(1.5), + liquidationRatioD18: bn(1.1), + liquidationRewardD18: 0, + minDelegationD18: 0, + depositingEnabled: true, + }); + }); + + const restore = snapshotCheckpoint(provider); + + function getExpectedCollateralizationRatio( + collateralAmount: ethers.BigNumberish, + debt: ethers.BigNumberish + ) { + const debtBN = ethers.BigNumber.from(debt); + if (debtBN.isZero()) { + return MAX_UINT; + } + + const collateralBN = ethers.BigNumber.from(collateralAmount); + const decimalBN = ethers.BigNumber.from(10).pow(18); + + return collateralBN.mul(decimalBN).div(debtBN); + } + + // eslint-disable-next-line max-params + function verifyAccountState( + accountId: number, + poolId: number, + collateralAmount: ethers.BigNumberish, + debt: ethers.BigNumberish + ) { + return async () => { + assertBn.equal( + await systems().Core.getPositionCollateral(accountId, poolId, collateralAddress()), + collateralAmount + ); + + assertBn.equal( + await systems().Core.callStatic.getPositionDebt(accountId, poolId, collateralAddress()), + debt + ); + assertBn.equal( + await systems().Core.callStatic.getPositionCollateralRatio( + accountId, + poolId, + collateralAddress() + ), + getExpectedCollateralizationRatio(collateralAmount, debt) + ); + }; + } + + describe('fresh vault', async () => { + const fakeFreshVaultId = 209372; + + before('create empty vault', async () => { + await systems().Core.createPool(fakeFreshVaultId, await user1.getAddress()); + }); + + it('returns 0 debt', async () => { + assertBn.equal( + await systems().Core.callStatic.getVaultDebt(fakeFreshVaultId, collateralAddress()), + 0 + ); + }); + + it('returns 0 collateral', async () => { + assertBn.equal( + ( + await systems().Core.callStatic.getVaultCollateral(fakeFreshVaultId, collateralAddress()) + )[0], + 0 + ); + }); + + it('returns 0 collateral ratio', async () => { + assertBn.equal( + await systems().Core.callStatic.getVaultCollateralRatio( + fakeFreshVaultId, + collateralAddress() + ), + 0 + ); + }); + }); + + describe('delegateCollateral()', async () => { + it( + 'after bootstrap have correct amounts', + verifyAccountState(accountId, poolId, depositAmount, 0) + ); + + it('has max cratio', async function () { + assertBn.equal( + await systems().Core.callStatic.getPositionCollateralRatio( + accountId, + poolId, + collateralAddress() + ), + MAX_UINT + ); + }); + + it('after bootstrap liquidity is delegated all the way back to the market', async () => { + assertBn.gt(await systems().Core.callStatic.getMarketCollateral(marketId), 0); + }); + + it('verifies permission for account', async () => { + await assertRevert( + systems() + .Core.connect(user2) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ), + `PermissionDenied("1", "${Permissions.DELEGATE}", "${await user2.getAddress()}")`, + systems().Core + ); + }); + + it('verifies leverage', async () => { + const leverage = ethers.utils.parseEther('1.1'); + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + leverage + ), + `InvalidLeverage("${leverage}")`, + systems().Core + ); + }); + + it('fails when trying to delegate less than minDelegation amount', async () => { + await assertRevert( + systems().Core.connect(user1).delegateCollateral( + accountId, + 0, // 0 pool is just easy way to test another pool + collateralAddress(), + depositAmount.div(51), + ethers.utils.parseEther('1') + ), + 'InsufficientDelegation("20000000000000000000")', + systems().Core + ); + }); + + it('fails when new collateral amount equals current collateral amount', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + depositAmount, + ethers.utils.parseEther('1') + ), + 'InvalidCollateralAmount()', + systems().Core + ); + }); + + it('fails when pool does not exist', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + 42, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ), + 'PoolNotFound("42")', + systems().Core + ); + }); + + verifyUsesFeatureFlag( + () => systems().Core, + 'delegateCollateral', + () => + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + 42, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ) + ); + + describe('when collateral is disabled by system', async () => { + const restore = snapshotCheckpoint(provider); + after(restore); + + const fakeVaultId = 93729028; + + before('create empty vault', async () => { + await systems().Core.createPool(fakeVaultId, await user1.getAddress()); + }); + + before('disable collateral', async () => { + const beforeConfiguration = + await systems().Core.getCollateralConfiguration(collateralAddress()); + + await systems() + .Core.connect(owner) + .configureCollateral({ ...beforeConfiguration, depositingEnabled: false }); + }); + + it('fails when trying to open delegation position with disabled collateral', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ), + `CollateralDepositDisabled("${collateralAddress()}")`, + systems().Core + ); + }); + }); + + describe('when collateral is disabled by pool owner', async () => { + const restore = snapshotCheckpoint(provider); + after(restore); + + const fakeVaultId = 93729021; + + before('create empty vault', async () => { + await systems().Core.createPool(fakeVaultId, await user1.getAddress()); + }); + + before('enable collateral for the system', async () => { + const beforeConfiguration = + await systems().Core.getCollateralConfiguration(collateralAddress()); + + await systems() + .Core.connect(owner) + .configureCollateral({ ...beforeConfiguration, depositingEnabled: true }); + }); + + // fails when collateral is disabled for the pool by pool owner + before('disable collateral for the pool by the pool owner', async () => { + await systems() + .Core.connect(user1) + .setPoolCollateralConfiguration(fakeVaultId, collateralAddress(), { + collateralLimitD18: bn(10), + issuanceRatioD18: bn(0), + }); + }); + + // fails when collateral is disabled for the pool by pool owner + it('fails when trying to open delegation position with disabled collateral', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ), + `PoolCollateralLimitExceeded("${fakeVaultId}", "${collateralAddress()}", "${depositAmount + .div(50) + .toString()}", "${bn(10).toString()}")`, + systems().Core + ); + }); + + it('collateral is enabled by the pool owner', async () => { + await systems() + .Core.connect(user1) + .setPoolCollateralConfiguration(fakeVaultId, collateralAddress(), { + collateralLimitD18: bn(1000000), + issuanceRatioD18: bn(0), + }); + }); + + it('the delegation works as expected with the enabled collateral', async () => { + await systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + fakeVaultId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ); + }); + }); + + describe('when pool has limited collateral deposit', async () => { + before('set pool limit', async () => { + await systems() + .Core.connect(owner) + .setPoolCollateralConfiguration(poolId, collateralAddress(), { + collateralLimitD18: depositAmount.div(2), + issuanceRatioD18: bn(0), + }); + }); + + it('fails when pool does not allow sufficient deposit amount', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ), + `PoolCollateralLimitExceeded("${poolId}", "${collateralAddress()}", "${depositAmount + .mul(2) + .toString()}", "${depositAmount.div(2).toString()}")`, + systems().Core + ); + }); + }); + + it( + 'user1 has expected initial position', + verifyAccountState(accountId, poolId, depositAmount, 0) + ); + + describe('market debt accumulation', () => { + const startingDebt = ethers.utils.parseEther('100'); + + before('user1 goes into debt', async () => { + await MockMarket.connect(user1).setReportedDebt(startingDebt); + }); + + it('has allocated debt to vault', async () => { + assertBn.equal( + await systems().Core.connect(user2).callStatic.getVaultDebt(poolId, collateralAddress()), + startingDebt + ); + }); + + it( + 'user1 has become indebted', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + + it('vault c-ratio is affected', async () => { + assertBn.equal( + await systems().Core.callStatic.getVaultCollateralRatio(poolId, collateralAddress()), + depositAmount.mul(ethers.utils.parseEther('1')).div(startingDebt) + ); + }); + + describe('second user delegates', async () => { + const user2AccountId = 283847; + + before('set pool limit', async () => { + await systems() + .Core.connect(owner) + .setPoolCollateralConfiguration(poolId, collateralAddress(), { + collateralLimitD18: depositAmount.mul(10), + issuanceRatioD18: bn(0), + }); + }); + + before('second user delegates and mints', async () => { + // user1 has extra collateral available + await collateralContract() + .connect(user1) + .transfer(await user2.getAddress(), depositAmount.mul(2)); + + await systems().Core.connect(user2)['createAccount(uint128)'](user2AccountId); + + await collateralContract() + .connect(user2) + .approve(systems().Core.address, depositAmount.mul(2)); + + await systems() + .Core.connect(user2) + .deposit(user2AccountId, collateralAddress(), depositAmount.mul(2)); + + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 75%, user2 25% + ethers.utils.parseEther('1') + ); + + await systems().Core.connect(user2).mintUsd( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(100) // should be enough collateral to mint this + ); + + await systems() + .Core.connect(user2) + .withdraw(user2AccountId, await systems().Core.getUsdToken(), depositAmount.div(100)); + }); + + // lock enough collateral that the market will *become* capacity locked when the user + // withdraws + const locked = ethers.utils.parseEther('1400'); + + // NOTE: if you are looking at this block and wondering if it would affect your test, + // this is to ensure all below cases are covered with locking. + // when position is increased, it should not be affected by locking + // when a position is decreased, it should only be allowed if the capacity does + // not become locked + before('market locks some capacity', async () => { + await MockMarket.setLocked(locked); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + it( + 'user2 still has correct position', + verifyAccountState(user2AccountId, poolId, depositAmount.div(3), depositAmount.div(100)) + ); + + describe('if one of the markets has a min delegation time', () => { + const restore = snapshotCheckpoint(provider); + + before('set market min delegation time to something high', async () => { + await MockMarket.setMinDelegationTime(86400); + }); + + describe('without time passing', async () => { + it('fails when min delegation timeout not elapsed', async () => { + await assertRevert( + systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(4), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ), + `MinDelegationTimeoutPending("${poolId}",`, + systems().Core + ); + }); + + it('can increase delegation without waiting', async () => { + await systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + }); + + after(restore); + }); + + describe('after time passes', () => { + before('fast forward', async () => { + // for some reason `fastForward` doesn't seem to work with anvil + await fastForwardTo((await getTime(provider())) + 86400, provider()); + }); + + it('works', async () => { + await systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(2), + ethers.utils.parseEther('1') + ); + }); + }); + + after(restore); + }); + + // these exposure tests should be enabled when exposures other + // than 1 are allowed (which might be something we want to do) + describe.skip('increase exposure', async () => { + before('delegate', async () => { + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, 0) + ); + it( + 'user2 still has correct position', + verifyAccountState(user2AccountId, poolId, depositAmount.div(3), depositAmount.div(100)) + ); + }); + + describe.skip('reduce exposure', async () => { + before('delegate', async () => { + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + it( + 'user2 still has correct position', + verifyAccountState(user2AccountId, poolId, depositAmount.div(3), depositAmount.div(100)) + ); + }); + + describe('remove exposure', async () => { + before('delegate', async () => { + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + }); + }); + + describe('increase collateral', async () => { + it('fails when not enough available collateral in account', async () => { + const wanted = depositAmount.mul(3); + const missing = wanted.sub(depositAmount.div(3)); + + await assertRevert( + systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + wanted, + ethers.utils.parseEther('1') + ), + `InsufficientAccountCollateral("${missing}")`, + systems().Core + ); + }); + + describe('when collateral is disabled', async () => { + const restore = snapshotCheckpoint(provider); + after(restore); + + before('disable collatearal', async () => { + const beforeConfiguration = + await systems().Core.getCollateralConfiguration(collateralAddress()); + + await systems() + .Core.connect(owner) + .configureCollateral({ ...beforeConfiguration, depositingEnabled: false }); + }); + + it('fails when trying to open delegation position with disabled collateral', async () => { + await assertRevert( + systems().Core.connect(user2).delegateCollateral( + user2AccountId, + 0, + collateralAddress(), + depositAmount, // user1 50%, user2 50% + ethers.utils.parseEther('1') + ), + `CollateralDepositDisabled("${collateralAddress()}")`, + systems().Core + ); + }); + }); + + describe('success', () => { + before('delegate', async () => { + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount, // user1 50%, user2 50% + ethers.utils.parseEther('1') + ); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + it( + 'user2 position is increased', + verifyAccountState(user2AccountId, poolId, depositAmount, depositAmount.div(100)) + ); + }); + }); + + describe('decrease collateral', async () => { + it('fails when insufficient c-ratio', async () => { + const { issuanceRatioD18 } = + await systems().Core.getCollateralConfiguration(collateralAddress()); + const price = await systems().Core.getCollateralPrice(collateralAddress()); + const deposit = depositAmount.div(50); + const debt = depositAmount.div(100); + + await assertRevert( + systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(50), + ethers.utils.parseEther('1') + ), + `InsufficientCollateralRatio("${deposit}", "${debt}", "${deposit + .mul(price) + .div(debt)}", "${issuanceRatioD18}")`, + systems().Core + ); + }); + + it('fails when reducing to below minDelegation amount', async () => { + await assertRevert( + systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(51), + ethers.utils.parseEther('1') + ), + 'InsufficientDelegation("20000000000000000000")', + systems().Core + ); + }); + + it('fails when market becomes capacity locked', async () => { + // sanity + assert.ok( + !(await systems().Core.connect(user2).callStatic.isMarketCapacityLocked(marketId)) + ); + + await assertRevert( + systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ), + `CapacityLocked("${marketId}")`, + systems().Core + ); + + // allow future tests to work without being locked + await MockMarket.setLocked(ethers.utils.parseEther('500')); + }); + + describe('when collateral is disabled', async () => { + const restore = snapshotCheckpoint(provider); + after(restore); + + before('disable collateral', async () => { + const beforeConfiguration = + await systems().Core.getCollateralConfiguration(collateralAddress()); + + await systems() + .Core.connect(owner) + .configureCollateral({ ...beforeConfiguration, depositingEnabled: false }); + }); + + describe('success', () => { + before('delegate', async () => { + await systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(10), + ethers.utils.parseEther('1') + ); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + it( + 'user2 position is decreased', + verifyAccountState( + user2AccountId, + poolId, + depositAmount.div(10), + depositAmount.div(100) + ) + ); + }); + }); + }); + + describe('remove collateral', async () => { + before('repay debt', async () => { + await systems() + .USD.connect(user2) + .approve(systems().Core.address, constants.MaxUint256.toString()); + + await systems() + .Core.connect(user2) + .deposit(user2AccountId, await systems().Core.getUsdToken(), depositAmount.div(100)); + + await systems() + .Core.connect(user2) + .burnUsd(user2AccountId, poolId, collateralAddress(), depositAmount.div(100)); + }); + + before('delegate', async () => { + await systems() + .Core.connect(user2) + .delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + 0, + ethers.utils.parseEther('1') + ); + }); + + it( + 'user1 still has correct position', + verifyAccountState(accountId, poolId, depositAmount, startingDebt) + ); + it('user2 position is closed', verifyAccountState(user2AccountId, poolId, 0, 0)); + + it('lets user2 re-stake again', async () => { + await systems().Core.connect(user2).delegateCollateral( + user2AccountId, + poolId, + collateralAddress(), + depositAmount.div(3), // user1 75%, user2 25% + ethers.utils.parseEther('1') + ); + }); + }); + }); + }); + + describe('first user leaves', async () => { + before(restore); + before('erase debt', async () => { + await MockMarket.connect(user1).setReportedDebt(0); + }); + + before('undelegate', async () => { + await systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + 0, + ethers.utils.parseEther('1') + ); + }); + + // now the pool is empty + it('exited user1 position', verifyAccountState(accountId, poolId, 0, 0)); + + it('vault is empty', async () => { + assertBn.equal( + (await systems().Core.callStatic.getVaultCollateral(poolId, collateralAddress())).amount, + 0 + ); + assertBn.equal( + await systems().Core.callStatic.getVaultDebt(poolId, collateralAddress()), + 0 + ); + }); + }); + }); + + describe('distribution chain edge cases', async () => { + beforeEach(restore); + it('edge case: double USD printing on market by not fully flushing with 2 collaterals', async () => { + const startingWithdrawable = await systems().Core.getWithdrawableMarketUsd(marketId); + + assertBn.gt(startingWithdrawable, 0); + + // first, mint max debt + await MockMarket.withdrawUsd(startingWithdrawable); + + // sanity + assertBn.equal(await systems().Core.getWithdrawableMarketUsd(marketId), 0); + + // next flush and rebalance (these methods are both write despite appearance) + await systems().Core.getVaultDebt(poolId, systems().Collateral2Mock.address); + await systems().Core.getVaultDebt(poolId, systems().Collateral2Mock.address); + + // finally, we shouldn't be able to mint + await assertRevert( + MockMarket.withdrawUsd(wei(1).toBN()), + 'NotEnoughLiquidity(', + systems().Core + ); + + assertBn.equal(await systems().Core.getWithdrawableMarketUsd(marketId), 0); + }); + + it('edge case: double USD printing on market by not fully flushing with `rebalancePool`', async () => { + const startingWithdrawable = await systems().Core.getWithdrawableMarketUsd(marketId); + + assertBn.gt(startingWithdrawable, 0); + + // first, mint max debt + await MockMarket.withdrawUsd(startingWithdrawable); + + // sanity + assertBn.equal(await systems().Core.getWithdrawableMarketUsd(marketId), 0); + + // next flush and rebalance + // two rebalancePool() required because the debt is accumulated on first call, but not actually assumed by market + // further rebalancePool() calls have no effect, but another call to `withdrawUsd()` can be made and then this repeated. + // NOTE: this attack could also be executed in a pool which has 2 vaults. Just sync only one of the vaults, and the debt from + // the other unsynced vault will not have its debt updated. so the security issue is not exclusive to being + // caused by the addition of this function, just more convenient + await systems().Core.rebalancePool(poolId, ethers.constants.AddressZero); + await systems().Core.rebalancePool(poolId, ethers.constants.AddressZero); + + // finally, we shouldn't be able to mint + await assertRevert( + MockMarket.withdrawUsd(wei(1).toBN()), + 'NotEnoughLiquidity(', + systems().Core + ); + + assertBn.equal(await systems().Core.getWithdrawableMarketUsd(marketId), 0); + }); + }); +}); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index a2f91f5692..d63801d20b 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -179,7 +179,7 @@ describe('VaultModule', function () { }); }); - describe('delegateCollateral()', async () => { + describe('intent delegate', async () => { it( 'after bootstrap have correct amounts', verifyAccountState(accountId, poolId, depositAmount, 0) @@ -301,7 +301,7 @@ describe('VaultModule', function () { verifyUsesFeatureFlag( () => systems().Core, - 'delegateCollateral', + 'twoStepsDelegateCollateral', async () => systems() .Core.connect(user1) @@ -320,6 +320,77 @@ describe('VaultModule', function () { ) ); + describe('when both FF are enabled', async () => { + const LEGACY_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String('delegateCollateral'); + const TWO_STEPS_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String( + 'twoStepsDelegateCollateral' + ); + + const restore = snapshotCheckpoint(provider); + after(restore); + + before('enable both FFs', async () => { + await systems().Core.setFeatureFlagAllowAll(LEGACY_DELEGATION_FEATURE_FLAG, true); + await systems().Core.setFeatureFlagAllowAll(TWO_STEPS_DELEGATION_FEATURE_FLAG, true); + }); + + it('fails when trying to use declareIntentToDelegateCollateral()', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress(), + await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount + ), + ethers.utils.parseEther('1') + ), + 'LegacyAndTwoStepsDelegateCollateralEnabled()', + systems().Core + ); + }); + + it('fails when trying to use processIntentToDelegateCollateralByIntents()', async () => { + await assertRevert( + systems().Core.connect(user2).processIntentToDelegateCollateralByIntents(accountId, [0]), + 'LegacyAndTwoStepsDelegateCollateralEnabled()', + systems().Core + ); + }); + + it('fails when trying to use processIntentToDelegateCollateralByPair()', async () => { + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByPair(accountId, poolId, collateralAddress()), + 'LegacyAndTwoStepsDelegateCollateralEnabled()', + systems().Core + ); + }); + + it('fails when trying to use delegateCollateral() (legacy delegation)', async () => { + await assertRevert( + systems() + .Core.connect(user1) + .delegateCollateral( + accountId, + poolId, + collateralAddress(), + depositAmount, + ethers.utils.parseEther('1') + ), + 'LegacyAndTwoStepsDelegateCollateralEnabled()', + systems().Core + ); + }); + }); + describe('when collateral is disabled by system', async () => { const restore = snapshotCheckpoint(provider); after(restore); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts index 06d6c3a3a1..fd849c4742 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationIntentViews.test.ts @@ -91,10 +91,7 @@ describe('VaultModule Two-step Delegation views', function () { }); before('set initial market window times', async () => { - await MockMarket.setDelegateCollateralDelay(200); - await MockMarket.setDelegateCollateralWindow(200); - await MockMarket.setUndelegateCollateralDelay(200); - await MockMarket.setUndelegateCollateralWindow(200); + await MockMarket.setDelegationCollateralConfiguration(200, 200, 200, 200); }); const intentIds = new Array(); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 8763bb717b..29bae5184f 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -9,7 +9,7 @@ import { bn, bootstrapWithStakedPool } from '../../bootstrap'; import { delegateCollateral, declareDelegateIntent } from '../../../common'; import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; -describe('VaultModule Two-step Delegation', function () { +describe('VaultModule Two-step Delegation timing', function () { const { signers, systems, @@ -104,8 +104,13 @@ describe('VaultModule Two-step Delegation', function () { let intentId: BigNumber; let declareDelegateIntentTime: number; before('set market window times', async () => { - await MockMarket.setDelegateCollateralDelay(100); - await MockMarket.setDelegateCollateralWindow(20); + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 100, + 20, + previousConfiguration[2], + previousConfiguration[3] + ); }); before('declare intent to delegate', async () => { @@ -154,8 +159,13 @@ describe('VaultModule Two-step Delegation', function () { let intentId: BigNumber; let declareDelegateIntentTime: number; before('set market window times', async () => { - await MockMarket.setUndelegateCollateralDelay(100); - await MockMarket.setUndelegateCollateralWindow(20); + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + previousConfiguration[0], + previousConfiguration[1], + 100, + 20 + ); }); before('first delegete some', async () => { @@ -214,6 +224,76 @@ describe('VaultModule Two-step Delegation', function () { }); }); + describe('Delegation Timing failures with global params', async () => { + let intentId: BigNumber; + let declareDelegateIntentTime: number; + before('set global window times', async () => { + await systems() + .Core.connect(owner) + .setConfig( + ethers.utils.formatBytes32String('delegateCollateralDelay_min'), + ethers.utils.hexZeroPad(bn(120).toHexString(), 32) + ); // use 120 as the global min delay + await systems() + .Core.connect(owner) + .setConfig( + ethers.utils.formatBytes32String('delegateCollateralWindow_max'), + ethers.utils.hexZeroPad(bn(10).toHexString(), 32) + ); // use 10 as the global max window + }); + + before('set market window times', async () => { + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 10, + 200, + previousConfiguration[2], + previousConfiguration[3] + ); + }); + + before('declare intent to delegate', async () => { + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + after(restore); + + it('fails to execute a delegation if window is not open (too soon)', async () => { + await fastForwardTo(declareDelegateIntentTime + 115, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentNotReady`, + systems().Core + ); + }); + + it('fails to execute a delegation if window is already closed (too late)', async () => { + await fastForwardTo(declareDelegateIntentTime + 131, provider()); + + await assertRevert( + systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]), + `DelegationIntentExpired`, + systems().Core + ); + }); + }); + describe('Force Delete intents (only system owner)', async () => { let intentId: BigNumber; before('declare intent to delegate', async () => { @@ -292,8 +372,13 @@ describe('VaultModule Two-step Delegation', function () { let intentId: BigNumber; let declareDelegateIntentTime: number; before('set market window times', async () => { - await MockMarket.setDelegateCollateralDelay(100); - await MockMarket.setDelegateCollateralWindow(20); + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 100, + 20, + previousConfiguration[2], + previousConfiguration[3] + ); }); before('declare intent to delegate', async () => { @@ -371,8 +456,13 @@ describe('VaultModule Two-step Delegation', function () { let intentId: BigNumber; let declareDelegateIntentTime: number; before('set market window times', async () => { - await MockMarket.setDelegateCollateralDelay(10000); - await MockMarket.setDelegateCollateralWindow(2000); + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 10000, + 2000, + previousConfiguration[2], + previousConfiguration[3] + ); }); before('declare intent to delegate', async () => { @@ -391,8 +481,13 @@ describe('VaultModule Two-step Delegation', function () { }); before('set market window times', async () => { - await MockMarket.setDelegateCollateralDelay(100); - await MockMarket.setDelegateCollateralWindow(20); + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 100, + 20, + previousConfiguration[2], + previousConfiguration[3] + ); }); it('sanity check. The intent exists', async () => { @@ -419,8 +514,14 @@ describe('VaultModule Two-step Delegation', function () { let declareDelegateIntentTime: number; before(restore); before('set market window times', async () => { - await MockMarket.setDelegateCollateralDelay(150); - // Note: not setting the window size (it means defaults to 0) - forever expiration to execute, immediate expiration to delete + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 150, + 0, + previousConfiguration[2], + previousConfiguration[3] + ); + // Note: not setting the window size to zero - forever expiration to execute, immediate expiration to delete }); before('declare intent to delegate', async () => { @@ -504,15 +605,9 @@ describe('VaultModule Two-step Delegation', function () { }); before('set both market window times', async () => { - await MockMarket.setDelegateCollateralDelay(100); - await MockMarket.setUndelegateCollateralDelay(100); - await MockMarket.setDelegateCollateralWindow(0); - await MockMarket.setUndelegateCollateralWindow(0); - - await SecondMockMarket.setDelegateCollateralDelay(200); - await SecondMockMarket.setUndelegateCollateralDelay(200); - await SecondMockMarket.setDelegateCollateralWindow(20); - await SecondMockMarket.setUndelegateCollateralWindow(20); + await MockMarket.setDelegationCollateralConfiguration(100, 0, 100, 0); + + await SecondMockMarket.setDelegationCollateralConfiguration(200, 20, 200, 20); }); before('declare intent to delegate', async () => { From 4061decd5b1faef769aff81a67f846d83e8f2c22 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 26 Jun 2024 14:22:26 -0300 Subject: [PATCH 39/50] fix bfp tests --- markets/bfp-market/test/bootstrap.ts | 2 +- .../integration/modules/OrderModule.test.ts | 9 ++------- ...PerpMarketFactoryModule.utilisation.test.ts | 18 ++++-------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/markets/bfp-market/test/bootstrap.ts b/markets/bfp-market/test/bootstrap.ts index 2a03e9d072..16c567cb66 100644 --- a/markets/bfp-market/test/bootstrap.ts +++ b/markets/bfp-market/test/bootstrap.ts @@ -137,7 +137,7 @@ export const bootstrap = (args: GeneratedBootstrap) => { const stakedCollateralAmount = wei(50_000_000).div(stakedCollateralPrice).toBN(); // Create a pool which makes `args.markets.length` with all equal weighting. - const stakedPool = createStakedPool(core, stakedCollateralPrice, stakedCollateralAmount); + const stakedPool = createStakedPool(core, stakedCollateralPrice, stakedCollateralAmount, true); let ethOracleNodeId: string; let ethOracleAgg: AggregatorV3Mock; diff --git a/markets/bfp-market/test/integration/modules/OrderModule.test.ts b/markets/bfp-market/test/integration/modules/OrderModule.test.ts index 648b208d2e..b604c928d0 100644 --- a/markets/bfp-market/test/integration/modules/OrderModule.test.ts +++ b/markets/bfp-market/test/integration/modules/OrderModule.test.ts @@ -37,7 +37,6 @@ import { import { ethers } from 'ethers'; import { calcFillPrice } from '../../calculations'; import { shuffle } from 'lodash'; -import { delegateCollateral } from '@synthetixio/main/test/common'; describe('OrderModule', () => { const bs = bootstrap(genBootstrap()); @@ -45,7 +44,6 @@ describe('OrderModule', () => { systems, restore, provider, - owner, keeper, collateralsWithoutSusd, markets, @@ -1481,7 +1479,7 @@ describe('OrderModule', () => { }); it('should accurately account for utilization when holding for a long time', async () => { - const { BfpMarketProxy } = systems(); + const { BfpMarketProxy, Core } = systems(); const { collateral, collateralDepositAmount, trader, marketId, market } = await depositMargin( bs, @@ -1511,10 +1509,7 @@ describe('OrderModule', () => { const { stakedAmount, staker, stakerAccountId, collateral: stakedCollateral, id } = pool(); // Unstake 50% of the delegated amount on the core side, this should lead to a increased utilization rate. const newDelegated = wei(stakedAmount).mul(0.5).toBN(); - await delegateCollateral( - systems, - owner(), - staker(), + await Core.connect(staker()).delegateCollateral( stakerAccountId, id, stakedCollateral().address, diff --git a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts index 5db176fba8..ab3f43f75c 100644 --- a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts +++ b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts @@ -14,7 +14,6 @@ import { withExplicitEvmMine, } from '../../helpers'; import { calcUtilization, calcUtilizationRate } from '../../calculations'; -import { delegateCollateral } from '@synthetixio/main/test/common'; describe('PerpMarketFactoryModule Utilization', () => { const bs = bootstrap(genBootstrap()); @@ -144,10 +143,7 @@ describe('PerpMarketFactoryModule Utilization', () => { stakedCollateral().address ); const stakedCollateralAddress = stakedCollateral().address; - await delegateCollateral( - systems, - owner(), - staker(), + await Core.connect(staker()).delegateCollateral( stakerAccountId, poolId, stakedCollateralAddress, @@ -217,10 +213,7 @@ describe('PerpMarketFactoryModule Utilization', () => { stakedCollateral().address ); const stakedCollateralAddress = stakedCollateral().address; - await delegateCollateral( - systems, - owner(), - staker(), + await Core.connect(staker()).delegateCollateral( stakerAccountId, poolId, stakedCollateralAddress, @@ -370,7 +363,7 @@ describe('PerpMarketFactoryModule Utilization', () => { }); describe('getUtilizationDigest', async () => { it('should return utilization data', async () => { - const { BfpMarketProxy } = systems(); + const { BfpMarketProxy, Core } = systems(); const market = genOneOf(markets()); const { marketId, trader, collateral, collateralDepositAmount } = await depositMargin( @@ -405,10 +398,7 @@ describe('PerpMarketFactoryModule Utilization', () => { await fastForwardBySec(provider(), genNumber(1, SECONDS_ONE_DAY)); // Decrease amount of staked collateral on the core side. const stakedCollateralAddress = stakedCollateral().address; - await delegateCollateral( - systems, - owner(), - staker(), + await Core.connect(staker()).delegateCollateral( stakerAccountId, poolId, stakedCollateralAddress, From f9ba8dac68fb26d24299dd9fb0154b09baa46fb2 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Wed, 26 Jun 2024 14:26:37 -0300 Subject: [PATCH 40/50] more fixes on bfp (roll back changes) --- .../PerpMarketFactoryModule.utilisation.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts index ab3f43f75c..376471a8be 100644 --- a/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts +++ b/markets/bfp-market/test/integration/modules/PerpMarketFactoryModule.utilisation.test.ts @@ -398,12 +398,16 @@ describe('PerpMarketFactoryModule Utilization', () => { await fastForwardBySec(provider(), genNumber(1, SECONDS_ONE_DAY)); // Decrease amount of staked collateral on the core side. const stakedCollateralAddress = stakedCollateral().address; - await Core.connect(staker()).delegateCollateral( - stakerAccountId, - poolId, - stakedCollateralAddress, - wei(stakedAmount).mul(0.9).toBN(), - bn(1) + await withExplicitEvmMine( + () => + Core.connect(staker()).delegateCollateral( + stakerAccountId, + poolId, + stakedCollateralAddress, + wei(stakedAmount).mul(0.9).toBN(), + bn(1) + ), + provider() ); const utilizationDigest2 = await BfpMarketProxy.getUtilizationDigest(marketId); From cc0bd1256dd34af08c0a3e6d52294a5b2ddb7f21 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 27 Jun 2024 13:41:08 -0300 Subject: [PATCH 41/50] add missing empty line --- protocol/synthetix/contracts/modules/core/PoolModule.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol/synthetix/contracts/modules/core/PoolModule.sol b/protocol/synthetix/contracts/modules/core/PoolModule.sol index 08bf8a8cbd..f032b17f34 100644 --- a/protocol/synthetix/contracts/modules/core/PoolModule.sol +++ b/protocol/synthetix/contracts/modules/core/PoolModule.sol @@ -149,6 +149,7 @@ contract PoolModule is IPoolModule { Pool.Data storage pool = Pool.loadExisting(poolId); Pool.onlyPoolOwner(poolId, ERC2771Context._msgSender()); pool.requireMinDelegationTimeElapsed(pool.lastConfigurationTime); + // Update each market's pro-rata liquidity and collect accumulated debt into the pool's debt distribution. // Note: This follows the same pattern as Pool.recalculateVaultCollateral(), // where we need to distribute the debt, adjust the market configurations and distribute again. From 62de7741063d295bf12a2c940c7cf6a015d37c3a Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 27 Jun 2024 16:31:31 -0300 Subject: [PATCH 42/50] add more info to view --- protocol/synthetix/contracts/interfaces/IVaultModule.sol | 6 +++++- protocol/synthetix/contracts/modules/core/VaultModule.sol | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index 83529225d1..b42c7cc773 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -272,6 +272,8 @@ interface IVaultModule { * @return deltaCollateralAmountD18 The delta amount of collateral delegated in the position, denominated with 18 decimals of precision. * @return leverage The new leverage amount used in the position, denominated with 18 decimals of precision. * @return declarationTime The time at which the intent was declared. + * @return processingStartTime The time at which the intent execution window starts. + * @return processingEndTime The time at which the intent execution window ends. */ function getAccountIntent( uint128 accountId, @@ -284,7 +286,9 @@ interface IVaultModule { address collateralType, int256 deltaCollateralAmountD18, uint256 leverage, - uint32 declarationTime + uint32 declarationTime, + uint32 processingStartTime, + uint32 processingEndTime ); /** diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index c840f4c1a5..d0db133ccc 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -419,7 +419,7 @@ contract VaultModule is IVaultModule { function getAccountIntent( uint128 accountId, uint256 intentId - ) external view override returns (uint128, address, int256, uint256, uint32) { + ) external view override returns (uint128, address, int256, uint256, uint32, uint32, uint32) { DelegationIntent.Data storage intent = Account .load(accountId) .getDelegationIntents() @@ -429,7 +429,9 @@ contract VaultModule is IVaultModule { intent.collateralType, intent.deltaCollateralAmountD18, intent.leverage, - intent.declarationTime + intent.declarationTime, + intent.processingStartTime(), + intent.processingEndTime() ); } From 9a4495534e585b290c27febb84671ed5c8ade5bd Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 27 Jun 2024 16:33:00 -0300 Subject: [PATCH 43/50] fix wrong sign + a small refactor --- .../contracts/storage/DelegationIntent.sol | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 72fb9b5410..9b1991553c 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -97,37 +97,17 @@ library DelegationIntent { } function processingStartTime(Data storage self) internal view returns (uint32) { - (uint32 requiredDelayTime, ) = Pool - .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); - return self.declarationTime + requiredDelayTime; + (uint32 _processingStartTime, ) = getProcessingWindow(self); + return _processingStartTime; } function processingEndTime(Data storage self) internal view returns (uint32) { - (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool - .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); - - // Apply default (forever) window time if not set - if (requiredWindowTime == 0) { - requiredWindowTime = 86400 * 360; // 1 year - } - - return self.declarationTime + requiredDelayTime + requiredWindowTime; + (, uint32 _processingEndTime) = getProcessingWindow(self); + return _processingEndTime; } function checkIsExecutable(Data storage self) internal view { - (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool - .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); - - // Apply default (forever) window time if not set - if (requiredWindowTime == 0) { - requiredWindowTime = 86400 * 360; // 1 year - } - - uint32 _processingStartTime = self.declarationTime + requiredDelayTime; - uint32 _processingEndTime = _processingStartTime + requiredWindowTime; + (uint32 _processingStartTime, uint32 _processingEndTime) = getProcessingWindow(self); if (block.timestamp < _processingStartTime) revert IVaultModule.DelegationIntentNotReady( @@ -139,9 +119,21 @@ library DelegationIntent { } function isExecutable(Data storage self) internal view returns (bool) { + (uint32 _processingStartTime, uint32 _processingEndTime) = getProcessingWindow(self); + + return block.timestamp >= _processingStartTime && block.timestamp < _processingEndTime; + } + + function intentExpired(Data storage self) internal view returns (bool) { + (, uint32 _processingEndTime) = getProcessingWindow(self); + + return block.timestamp >= _processingEndTime; + } + + function getProcessingWindow(Data storage self) internal view returns (uint32, uint32) { (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 > 0); + .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); // Apply default (forever) window time if not set if (requiredWindowTime == 0) { @@ -151,16 +143,6 @@ library DelegationIntent { uint32 _processingStartTime = self.declarationTime + requiredDelayTime; uint32 _processingEndTime = _processingStartTime + requiredWindowTime; - return block.timestamp >= _processingStartTime && block.timestamp < _processingEndTime; - } - - function intentExpired(Data storage self) internal view returns (bool) { - (uint32 requiredDelayTime, uint32 requiredWindowTime) = Pool - .loadExisting(self.poolId) - .getRequiredDelegationDelayAndWindow(self.deltaCollateralAmountD18 < 0); - - // Note: here we don't apply the forever defaul if window time is not set to allow the intent to expire. If it's zero it means is not configured, then it can expire immediately. - uint32 _processingEndTime = self.declarationTime + requiredDelayTime + requiredWindowTime; - return block.timestamp >= _processingEndTime; + return (_processingStartTime, _processingEndTime); } } From 6272a49c91a9835837171d000753111d31adf75d Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Thu, 27 Jun 2024 16:33:06 -0300 Subject: [PATCH 44/50] fix test --- .../core/VaultModuleDelegationTiming.test.ts | 147 +++++++++++------- 1 file changed, 89 insertions(+), 58 deletions(-) diff --git a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts index 29bae5184f..04f904e45a 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModuleDelegationTiming.test.ts @@ -67,17 +67,16 @@ describe('VaultModule Two-step Delegation timing', function () { before('add second collateral type', async () => { // add collateral - await ( - await systems().Core.connect(owner).configureCollateral({ - tokenAddress: systems().Collateral2Mock.address, - oracleNodeId: oracleNodeId(), - issuanceRatioD18: '5000000000000000000', - liquidationRatioD18: '1500000000000000000', - liquidationRewardD18: '20000000000000000000', - minDelegationD18: '20000000000000000000', - depositingEnabled: true, - }) - ).wait(); + + await systems().Core.connect(owner).configureCollateral({ + tokenAddress: systems().Collateral2Mock.address, + oracleNodeId: oracleNodeId(), + issuanceRatioD18: '5000000000000000000', + liquidationRatioD18: '1500000000000000000', + liquidationRewardD18: '20000000000000000000', + minDelegationD18: '20000000000000000000', + depositingEnabled: true, + }); await systems() .Core.connect(owner) @@ -103,6 +102,7 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Delegation Timing failures', async () => { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); before('set market window times', async () => { const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); await MockMarket.setDelegationCollateralConfiguration( @@ -128,28 +128,30 @@ describe('VaultModule Two-step Delegation timing', function () { declareDelegateIntentTime = await getTime(provider()); }); - after(restore); - - it('fails to execute a delegation if window is not open (too soon)', async () => { + it('skips the execution of a delegation if window is not open (too soon)', async () => { await fastForwardTo(declareDelegateIntentTime + 95, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentNotReady`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentSkipped(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); - it('fails to execute a delegation if window is already closed (too late)', async () => { + it('removes a delegation if window is already closed (too late)', async () => { await fastForwardTo(declareDelegateIntentTime + 121, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentExpired`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentRemoved(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); @@ -158,6 +160,8 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Un-Delegation Timing failures', async () => { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); + before('set market window times', async () => { const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); await MockMarket.setDelegationCollateralConfiguration( @@ -197,28 +201,30 @@ describe('VaultModule Two-step Delegation timing', function () { declareDelegateIntentTime = await getTime(provider()); }); - after(restore); - - it('fails to execute an un-delegation if window is not open (too soon)', async () => { + it('skips the execution of an un-delegation if window is not open (too soon)', async () => { await fastForwardTo(declareDelegateIntentTime + 95, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentNotReady`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentSkipped(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); - it('fails to execute an un-delegation if window is already closed (too late)', async () => { + it('removes an un-delegation if window is already closed (too late)', async () => { await fastForwardTo(declareDelegateIntentTime + 121, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentExpired`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentRemoved(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); @@ -227,18 +233,20 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Delegation Timing failures with global params', async () => { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); + before('set global window times', async () => { await systems() .Core.connect(owner) .setConfig( ethers.utils.formatBytes32String('delegateCollateralDelay_min'), - ethers.utils.hexZeroPad(bn(120).toHexString(), 32) + ethers.utils.hexZeroPad(ethers.BigNumber.from(120).toHexString(), 32) ); // use 120 as the global min delay await systems() .Core.connect(owner) .setConfig( ethers.utils.formatBytes32String('delegateCollateralWindow_max'), - ethers.utils.hexZeroPad(bn(10).toHexString(), 32) + ethers.utils.hexZeroPad(ethers.BigNumber.from(10).toHexString(), 32) ); // use 10 as the global max window }); @@ -267,28 +275,30 @@ describe('VaultModule Two-step Delegation timing', function () { declareDelegateIntentTime = await getTime(provider()); }); - after(restore); - - it('fails to execute a delegation if window is not open (too soon)', async () => { + it('skips the execution of a delegation if window is not open (too soon)', async () => { await fastForwardTo(declareDelegateIntentTime + 115, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentNotReady`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentSkipped(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); - it('fails to execute a delegation if window is already closed (too late)', async () => { + it('removes a delegation if window is already closed (too late)', async () => { await fastForwardTo(declareDelegateIntentTime + 131, provider()); - await assertRevert( - systems() - .Core.connect(user2) - .processIntentToDelegateCollateralByIntents(accountId, [intentId]), - `DelegationIntentExpired`, + const tx = await systems() + .Core.connect(user2) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + await assertEvent( + tx, + `DelegationIntentRemoved(${intentId}, ${accountId}, ${poolId}, "${collateralAddress()}")`, systems().Core ); }); @@ -296,6 +306,8 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Force Delete intents (only system owner)', async () => { let intentId: BigNumber; + before(restore); + before('declare intent to delegate', async () => { intentId = await declareDelegateIntent( systems, @@ -371,6 +383,8 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Self Delete intents (only account owner)', async () => { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); + before('set market window times', async () => { const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); await MockMarket.setDelegationCollateralConfiguration( @@ -455,6 +469,8 @@ describe('VaultModule Two-step Delegation timing', function () { describe('Edge case - Self delete after configuration change', async () => { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); + before('set market window times', async () => { const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); await MockMarket.setDelegationCollateralConfiguration( @@ -541,7 +557,8 @@ describe('VaultModule Two-step Delegation timing', function () { it('sanity check 1. The intent exists', async () => { const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); - assertBn.equal(intent[0], accountId); + assertBn.equal(intent[0], poolId); + assert.equal(intent[1], collateralAddress()); }); it('fails to delete before window starts', async () => { @@ -551,11 +568,23 @@ describe('VaultModule Two-step Delegation timing', function () { it('sanity check 2. The intent exists', async () => { const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); - assertBn.equal(intent[0], accountId); + assertBn.equal(intent[0], poolId); + assert.equal(intent[1], collateralAddress()); + }); + + it('fails to delete after the window starts', async () => { + await fastForwardTo(declareDelegateIntentTime + 200, provider()); + await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); + }); + + it('sanity check 3. The intent exists', async () => { + const intent = await systems().Core.connect(user1).getAccountIntent(accountId, intentId); + assertBn.equal(intent[0], poolId); + assert.equal(intent[1], collateralAddress()); }); it('can force delete all expired account intents', async () => { - await fastForwardTo(declareDelegateIntentTime + 155, provider()); + await fastForwardTo(declareDelegateIntentTime + 150 + 86400 * 360 + 1, provider()); await systems().Core.connect(user1).deleteAllExpiredIntents(accountId); }); @@ -576,6 +605,8 @@ describe('VaultModule Two-step Delegation timing', function () { let intentId: BigNumber; let declareDelegateIntentTime: number; + before(restore); + before('deploy and connect a second fake market', async () => { const factory = await hre.ethers.getContractFactory('MockMarket'); From ac3ad55d9794db54d10f485ee8b48e4899658fcf Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 1 Jul 2024 11:46:25 -0300 Subject: [PATCH 45/50] rename FFs --- .../synthetix/contracts/modules/core/VaultModule.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index d0db133ccc..bb9ab52fd3 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -41,7 +41,8 @@ contract VaultModule is IVaultModule { using Account for Account.Data; bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; - bytes32 private constant _TWO_STEPS_DELEGATE_FEATURE_FLAG = "twoStepsDelegateCollateral"; + bytes32 private constant _DECLARE_DELEGATE_FEATURE_FLAG = "declareIntentToDelegateColl"; + bytes32 private constant _PROCESS_DELEGATE_FEATURE_FLAG = "processIntentToDelegateColl"; /** * @inheritdoc IVaultModule @@ -55,7 +56,7 @@ contract VaultModule is IVaultModule { ) external override { FeatureFlag.ensureAccessToFeature(_DELEGATE_FEATURE_FLAG); Account.loadAccountAndValidatePermission(accountId, AccountRBAC._DELEGATE_PERMISSION); - if (FeatureFlag.hasAccess(_TWO_STEPS_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { + if (FeatureFlag.hasAccess(_DECLARE_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { revert LegacyAndTwoStepsDelegateCollateralEnabled(); } @@ -86,7 +87,7 @@ contract VaultModule is IVaultModule { uint256 leverage ) external override returns (uint256 intentId) { // Ensure the caller is authorized to represent the account. - FeatureFlag.ensureAccessToFeature(_TWO_STEPS_DELEGATE_FEATURE_FLAG); + FeatureFlag.ensureAccessToFeature(_DECLARE_DELEGATE_FEATURE_FLAG); Account.Data storage account = Account.loadAccountAndValidatePermission( accountId, AccountRBAC._DELEGATE_PERMISSION @@ -188,7 +189,7 @@ contract VaultModule is IVaultModule { uint128 accountId, uint256[] memory intentIds ) public override { - FeatureFlag.ensureAccessToFeature(_TWO_STEPS_DELEGATE_FEATURE_FLAG); + FeatureFlag.ensureAccessToFeature(_PROCESS_DELEGATE_FEATURE_FLAG); if (FeatureFlag.hasAccess(_DELEGATE_FEATURE_FLAG, ERC2771Context._msgSender())) { revert LegacyAndTwoStepsDelegateCollateralEnabled(); } From c11d354e76a03a4f3b94428d67c95120582ce62d Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Mon, 1 Jul 2024 12:19:02 -0300 Subject: [PATCH 46/50] fix tests --- protocol/synthetix/test/common/stakedPool.ts | 12 +++++++++--- .../integration/modules/core/VaultModule.test.ts | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/protocol/synthetix/test/common/stakedPool.ts b/protocol/synthetix/test/common/stakedPool.ts index 9211380ff4..043b188f88 100644 --- a/protocol/synthetix/test/common/stakedPool.ts +++ b/protocol/synthetix/test/common/stakedPool.ts @@ -9,8 +9,11 @@ export const bn = (n: number) => wei(n).toBN(); const POOL_FEATURE_FLAG = ethers.utils.formatBytes32String('createPool'); const LEGACY_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String('delegateCollateral'); -const TWO_STEPS_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String( - 'twoStepsDelegateCollateral' +const DECLARE_DELEGATE_FEATURE_FLAG = ethers.utils.formatBytes32String( + 'declareIntentToDelegateColl' +); +const PROCESS_DELEGATE_FEATURE_FLAG = ethers.utils.formatBytes32String( + 'processIntentToDelegateColl' ); export const createStakedPool = ( @@ -38,7 +41,10 @@ export const createStakedPool = ( .Core.setFeatureFlagAllowAll(LEGACY_DELEGATION_FEATURE_FLAG, useLegacyDelegateCollateral); await r .systems() - .Core.setFeatureFlagAllowAll(TWO_STEPS_DELEGATION_FEATURE_FLAG, !useLegacyDelegateCollateral); + .Core.setFeatureFlagAllowAll(DECLARE_DELEGATE_FEATURE_FLAG, !useLegacyDelegateCollateral); + await r + .systems() + .Core.setFeatureFlagAllowAll(PROCESS_DELEGATE_FEATURE_FLAG, !useLegacyDelegateCollateral); }); before('setup oracle manager node', async () => { diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index d63801d20b..b53c20f7ec 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -301,7 +301,7 @@ describe('VaultModule', function () { verifyUsesFeatureFlag( () => systems().Core, - 'twoStepsDelegateCollateral', + 'declareIntentToDelegateColl', async () => systems() .Core.connect(user1) @@ -322,8 +322,8 @@ describe('VaultModule', function () { describe('when both FF are enabled', async () => { const LEGACY_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String('delegateCollateral'); - const TWO_STEPS_DELEGATION_FEATURE_FLAG = ethers.utils.formatBytes32String( - 'twoStepsDelegateCollateral' + const DECLARE_DELEGATE_FEATURE_FLAG = ethers.utils.formatBytes32String( + 'declareIntentToDelegateColl' ); const restore = snapshotCheckpoint(provider); @@ -331,7 +331,7 @@ describe('VaultModule', function () { before('enable both FFs', async () => { await systems().Core.setFeatureFlagAllowAll(LEGACY_DELEGATION_FEATURE_FLAG, true); - await systems().Core.setFeatureFlagAllowAll(TWO_STEPS_DELEGATION_FEATURE_FLAG, true); + await systems().Core.setFeatureFlagAllowAll(DECLARE_DELEGATE_FEATURE_FLAG, true); }); it('fails when trying to use declareIntentToDelegateCollateral()', async () => { From 0a2d08e6df0e0199151926f9d3ca4b4780e6cfc2 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 2 Jul 2024 21:30:43 -0300 Subject: [PATCH 47/50] push storage dump --- protocol/synthetix/storage.dump.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index 03668a2430..62e46fc3fa 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -382,7 +382,8 @@ contract UtilsModule { // @custom:artifact contracts/modules/core/VaultModule.sol:VaultModule contract VaultModule { bytes32 private constant _DELEGATE_FEATURE_FLAG = "delegateCollateral"; - bytes32 private constant _TWO_STEPS_DELEGATE_FEATURE_FLAG = "twoStepsDelegateCollateral"; + bytes32 private constant _DECLARE_DELEGATE_FEATURE_FLAG = "declareIntentToDelegateColl"; + bytes32 private constant _PROCESS_DELEGATE_FEATURE_FLAG = "processIntentToDelegateColl"; } // @custom:artifact contracts/modules/usd/USDTokenModule.sol:USDTokenModule From 4ff64dbe73b39348a816a79fb3d0b9d100d81605 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Fri, 19 Jul 2024 14:23:41 -0300 Subject: [PATCH 48/50] Reduce delegationIntent footprint --- .../contracts/modules/core/VaultModule.sol | 26 ++++++++----------- .../contracts/storage/DelegationIntent.sol | 7 ++--- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index bb9ab52fd3..9f3a2d83a0 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -156,7 +156,6 @@ contract VaultModule is IVaultModule { // Create a new delegation intent. intentId = DelegationIntent.nextId(); DelegationIntent.Data storage intent = DelegationIntent.load(intentId); - intent.id = intentId; intent.accountId = accountId; intent.poolId = poolId; intent.collateralType = collateralType; @@ -199,15 +198,16 @@ contract VaultModule is IVaultModule { .getDelegationIntents(); for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - if (!accountIntents.isInCurrentEpoch(intent.id)) { - revert DelegationIntentNotInCurrentEpoch(intent.id); + uint256 intentId = intentIds[i]; + DelegationIntent.Data storage intent = DelegationIntent.load(intentId); + if (!accountIntents.isInCurrentEpoch(intentId)) { + revert DelegationIntentNotInCurrentEpoch(intentId); } if (!intent.isExecutable()) { // emit an Skipped event emit DelegationIntentSkipped( - intent.id, + intentId, accountId, intent.poolId, intent.collateralType @@ -217,7 +217,7 @@ contract VaultModule is IVaultModule { if (intent.intentExpired()) { accountIntents.removeIntent(intent); emit DelegationIntentRemoved( - intent.id, + intentId, accountId, intent.poolId, intent.collateralType @@ -242,16 +242,11 @@ contract VaultModule is IVaultModule { // Remove the intent. accountIntents.removeIntent(intent); - emit DelegationIntentRemoved( - intent.id, - accountId, - intent.poolId, - intent.collateralType - ); + emit DelegationIntentRemoved(intentId, accountId, intent.poolId, intent.collateralType); // emit an event emit DelegationIntentProcessed( - intent.id, + intentId, accountId, intent.poolId, intent.collateralType @@ -313,12 +308,13 @@ contract VaultModule is IVaultModule { .load(accountId) .getDelegationIntents(); for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + uint256 intentId = intentIds[i]; + DelegationIntent.Data storage intent = DelegationIntent.load(intentId); if (intent.accountId != accountId) { revert InvalidDelegationIntent(); } if (!intent.intentExpired()) { - revert DelegationIntentNotExpired(intent.id); + revert DelegationIntentNotExpired(intentId); } accountIntents.removeIntent(intent); } diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 9b1991553c..13ba078e95 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -31,10 +31,6 @@ library DelegationIntent { * The intent can be processed only between processingStartTime and processingEndTime. */ struct Data { - /** - * @notice An incrementing id (nonce) to ensure the uniqueness of the intent and prevent replay attacks - */ - uint256 id; /** * @notice The ID of the account that has an outstanding intent to delegate a new amount of collateral to */ @@ -82,7 +78,8 @@ library DelegationIntent { function loadValid(uint256 id) internal view returns (Data storage delegationIntent) { delegationIntent = load(id); - if (delegationIntent.id != id) { + // Notice, using declarationTime as a proxy for existence to reduce the struct by one slot removing the id + if (delegationIntent.declarationTime == 0) { revert IVaultModule.DelegationIntentNotExists(); } } From cd8a7f97dc1021fe291b581a48f9313affc51f10 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Fri, 19 Jul 2024 14:45:16 -0300 Subject: [PATCH 49/50] fix missing references to intent.id --- markets/bfp-market/storage.dump.sol | 1 - markets/perps-market/storage.dump.sol | 1 - .../contracts/modules/core/VaultModule.sol | 13 +++++----- .../storage/AccountDelegationIntents.sol | 24 ++++++++++++------- protocol/synthetix/storage.dump.sol | 1 - 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/markets/bfp-market/storage.dump.sol b/markets/bfp-market/storage.dump.sol index 84fd1740fc..1e16d1b88c 100644 --- a/markets/bfp-market/storage.dump.sol +++ b/markets/bfp-market/storage.dump.sol @@ -212,7 +212,6 @@ library Config { library DelegationIntent { bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; struct Data { - uint256 id; uint128 accountId; uint128 poolId; address collateralType; diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index fbcb16d994..ade8a33876 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -211,7 +211,6 @@ library Config { library DelegationIntent { bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; struct Data { - uint256 id; uint128 accountId; uint128 poolId; address collateralType; diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 9f3a2d83a0..a095b52247 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -164,7 +164,7 @@ contract VaultModule is IVaultModule { intent.declarationTime = block.timestamp.to32(); // Add intent to the account's delegation intents. - accountIntents.addIntent(intent); + accountIntents.addIntent(intent, intentId); // emit an event emit DelegationIntentDeclared( @@ -215,7 +215,7 @@ contract VaultModule is IVaultModule { // If expired, remove the intent. if (intent.intentExpired()) { - accountIntents.removeIntent(intent); + accountIntents.removeIntent(intent, intentId); emit DelegationIntentRemoved( intentId, accountId, @@ -241,7 +241,7 @@ contract VaultModule is IVaultModule { ); // Remove the intent. - accountIntents.removeIntent(intent); + accountIntents.removeIntent(intent, intentId); emit DelegationIntentRemoved(intentId, accountId, intent.poolId, intent.collateralType); // emit an event @@ -285,8 +285,9 @@ contract VaultModule is IVaultModule { .load(accountId) .getDelegationIntents(); for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - accountIntents.removeIntent(intent); + uint256 intentId = intentIds[i]; + DelegationIntent.Data storage intent = DelegationIntent.load(intentId); + accountIntents.removeIntent(intent, intentId); } } @@ -316,7 +317,7 @@ contract VaultModule is IVaultModule { if (!intent.intentExpired()) { revert DelegationIntentNotExpired(intentId); } - accountIntents.removeIntent(intent); + accountIntents.removeIntent(intent, intentId); } } diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index 40290175f1..eb99d655b7 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -28,15 +28,19 @@ library AccountDelegationIntents { mapping(address => int256) netDelegatedAmountPerCollateral; // collateralType => net delegatedCollateralAmount } - function addIntent(Data storage self, DelegationIntent.Data storage delegationIntent) internal { - self.intentsId.add(delegationIntent.id); + function addIntent( + Data storage self, + DelegationIntent.Data storage delegationIntent, + uint256 intentId + ) internal { + self.intentsId.add(intentId); self .intentsByPair[ keccak256( abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) ) ] - .add(delegationIntent.id); + .add(intentId); self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent .deltaCollateralAmountD18; @@ -48,20 +52,21 @@ library AccountDelegationIntents { function removeIntent( Data storage self, - DelegationIntent.Data storage delegationIntent + DelegationIntent.Data storage delegationIntent, + uint256 intentId ) internal { - if (!self.intentsId.contains(delegationIntent.id)) { + if (!self.intentsId.contains(intentId)) { return; } - self.intentsId.remove(delegationIntent.id); + self.intentsId.remove(intentId); self .intentsByPair[ keccak256( abi.encodePacked(delegationIntent.poolId, delegationIntent.collateralType) ) ] - .remove(delegationIntent.id); + .remove(intentId); self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent .deltaCollateralAmountD18; @@ -100,9 +105,10 @@ library AccountDelegationIntents { function cleanAllExpiredIntents(Data storage self) internal { uint256[] memory intentIds = self.intentsId.values(); for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + uint256 intentId = intentIds[i]; + DelegationIntent.Data storage intent = DelegationIntent.load(intentId); if (intent.intentExpired()) { - removeIntent(self, intent); + removeIntent(self, intent, intentId); } } } diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index 7cf2a339f6..1761c4ebf9 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -503,7 +503,6 @@ library CrossChain { library DelegationIntent { bytes32 private constant _ATOMIC_VALUE_LATEST_ID = "delegateIntent_idAsNonce"; struct Data { - uint256 id; uint128 accountId; uint128 poolId; address collateralType; From f960f8166fcf1ae83870d666197e0f485e472b51 Mon Sep 17 00:00:00 2001 From: Leonardo Massazza Date: Tue, 13 Aug 2024 11:47:01 -0300 Subject: [PATCH 50/50] fix test --- .../test/integration/modules/core/RewardsManagerModule.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/synthetix/test/integration/modules/core/RewardsManagerModule.test.ts b/protocol/synthetix/test/integration/modules/core/RewardsManagerModule.test.ts index bbb0f77893..0619631b28 100644 --- a/protocol/synthetix/test/integration/modules/core/RewardsManagerModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/RewardsManagerModule.test.ts @@ -17,7 +17,7 @@ import { verifyUsesFeatureFlag } from '../../verifications'; describe('RewardsManagerModule', function () { this.timeout(120000); const { provider, signers, systems, poolId, collateralAddress, accountId } = - bootstrapWithStakedPool(); + bootstrapWithStakedPool(true); let owner: ethers.Signer, user1: ethers.Signer, user2: ethers.Signer;