diff --git a/markets/bfp-market/storage.dump.sol b/markets/bfp-market/storage.dump.sol index 0c14278ffe..bb29f5a0a8 100644 --- a/markets/bfp-market/storage.dump.sol +++ b/markets/bfp-market/storage.dump.sol @@ -137,7 +137,8 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; SetUtil.AddressSet delegatedCollaterals; - mapping(address => int256) netDelegatedAmountPerCollateral; + mapping(address => uint256) delegatedAmountPerCollateral; + mapping(address => uint256) unDelegatedAmountPerCollateral; } } @@ -220,6 +221,7 @@ library DelegationIntent { int256 deltaCollateralAmountD18; uint256 leverage; uint32 declarationTime; + uint128 epochId; } function load(uint256 id) internal pure returns (Data storage delegationIntent) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index c0bab4a01a..d2ca37b9d5 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -136,7 +136,8 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; SetUtil.AddressSet delegatedCollaterals; - mapping(address => int256) netDelegatedAmountPerCollateral; + mapping(address => uint256) delegatedAmountPerCollateral; + mapping(address => uint256) unDelegatedAmountPerCollateral; } } @@ -219,6 +220,7 @@ library DelegationIntent { int256 deltaCollateralAmountD18; uint256 leverage; uint32 declarationTime; + uint128 epochId; } function load(uint256 id) internal pure returns (Data storage delegationIntent) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); diff --git a/protocol/synthetix/cannonfile.test.toml b/protocol/synthetix/cannonfile.test.toml index 5e39a024df..08e6b37773 100644 --- a/protocol/synthetix/cannonfile.test.toml +++ b/protocol/synthetix/cannonfile.test.toml @@ -83,6 +83,7 @@ contracts = [ "RewardsManagerModule", "UtilsModule", "VaultModule", + "VaultIntentViewsModule", "TestableAccountStorage", "TestableAccountRBACStorage", "TestableCollateralStorage", diff --git a/protocol/synthetix/cannonfile.toml b/protocol/synthetix/cannonfile.toml index 8a9ebc9f81..3973442446 100644 --- a/protocol/synthetix/cannonfile.toml +++ b/protocol/synthetix/cannonfile.toml @@ -91,6 +91,9 @@ artifact = "contracts/modules/core/UtilsModule.sol:UtilsModule" [contract.VaultModule] artifact = "contracts/modules/core/VaultModule.sol:VaultModule" +[contract.VaultIntentViewsModule] +artifact = "contracts/modules/core/VaultIntentViewsModule.sol:VaultIntentViewsModule" + [contract.AccountTokenModule] artifact = "contracts/modules/account/AccountTokenModule.sol:AccountTokenModule" @@ -118,6 +121,7 @@ contracts = [ "RewardsManagerModule", "UtilsModule", "VaultModule", + "VaultIntentViewsModule", ] [contract.InitialCoreProxy] diff --git a/protocol/synthetix/contracts/interfaces/ICollateralModule.sol b/protocol/synthetix/contracts/interfaces/ICollateralModule.sol index a7a358a8a1..338801b7b7 100644 --- a/protocol/synthetix/contracts/interfaces/ICollateralModule.sol +++ b/protocol/synthetix/contracts/interfaces/ICollateralModule.sol @@ -102,11 +102,20 @@ interface ICollateralModule { * @return totalDeposited The total collateral deposited in the account, denominated with 18 decimals of precision. * @return totalAssigned The amount of collateral in the account that is delegated to pools, denominated with 18 decimals of precision. * @return totalLocked The amount of collateral in the account that cannot currently be undelegated from a pool, denominated with 18 decimals of precision. + * @return totalPendingToDelegate The amount of collateral in the account that cannot currently be undelegated from a pool because is intended to be delegated, denominated with 18 decimals of precision. */ function getAccountCollateral( uint128 accountId, address collateralType - ) external view returns (uint256 totalDeposited, uint256 totalAssigned, uint256 totalLocked); + ) + external + view + returns ( + uint256 totalDeposited, + uint256 totalAssigned, + uint256 totalLocked, + uint256 totalPendingToDelegate + ); /** * @notice Returns the amount of collateral of type `collateralType` deposited with account `accountId` that can be withdrawn or delegated to pools. diff --git a/protocol/synthetix/contracts/interfaces/IVaultIntentViewsModule.sol b/protocol/synthetix/contracts/interfaces/IVaultIntentViewsModule.sol new file mode 100644 index 0000000000..195ba30ebe --- /dev/null +++ b/protocol/synthetix/contracts/interfaces/IVaultIntentViewsModule.sol @@ -0,0 +1,109 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +/** + * @title Views for Delegation Intents for Vault. + */ +interface IVaultIntentViewsModule { + /** + * @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. + * @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, + uint256 intentId + ) + external + view + returns ( + uint128 poolId, + address collateralType, + int256 deltaCollateralAmountD18, + uint256 leverage, + uint32 declarationTime, + uint32 processingStartTime, + uint32 processingEndTime + ); + + /** + * @notice Returns the total 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 delegatedPerCollateral The total amount of collateral intended to be delegated to the vault by the account, denominated with 18 decimals of precision. + */ + function getIntentDelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view returns (uint256 delegatedPerCollateral); + + /** + * @notice Returns the total amount of collateral intended to be undelegated to the vault by the account. + * @param accountId The id of the account owning the intents. + * @param collateralType The address of the collateral. + * @return undelegatedPerCollateral The total amount of collateral intended to be undelegated to the vault by the account, denominated with 18 decimals of precision. + */ + function getIntentUndelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view returns (uint256 undelegatedPerCollateral); + + /** + * @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 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); +} diff --git a/protocol/synthetix/contracts/interfaces/IVaultModule.sol b/protocol/synthetix/contracts/interfaces/IVaultModule.sol index b42c7cc773..c2674e37bd 100644 --- a/protocol/synthetix/contracts/interfaces/IVaultModule.sol +++ b/protocol/synthetix/contracts/interfaces/IVaultModule.sol @@ -57,7 +57,7 @@ interface IVaultModule { */ error ExceedingUndelegateAmount( int256 deltaCollateralAmountD18, - int256 cachedDeltaCollateralAmountD18, + uint256 cachedDeltaCollateralAmountD18, int256 totalDeltaCollateralAmountD18, uint256 currentCollateralAmount ); @@ -263,58 +263,6 @@ interface IVaultModule { */ 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. - * @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, - uint256 intentId - ) - external - view - returns ( - uint128 poolId, - address collateralType, - int256 deltaCollateralAmountD18, - uint256 leverage, - uint32 declarationTime, - uint32 processingStartTime, - uint32 processingEndTime - ); - - /** - * @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 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 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. @@ -333,45 +281,6 @@ interface IVaultModule { 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. - * @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/AccountModule.sol b/protocol/synthetix/contracts/modules/core/AccountModule.sol index 0dc853603f..204d8fb672 100644 --- a/protocol/synthetix/contracts/modules/core/AccountModule.sol +++ b/protocol/synthetix/contracts/modules/core/AccountModule.sol @@ -102,6 +102,9 @@ contract AccountModule is IAccountModule { account.rbac.revokeAllPermissions(permissionedAddresses[i]); } + // clean all pending intents + account.cleanAllIntents(); + account.rbac.setOwner(to); } diff --git a/protocol/synthetix/contracts/modules/core/CollateralModule.sol b/protocol/synthetix/contracts/modules/core/CollateralModule.sol index c8ba90d742..54068603f0 100644 --- a/protocol/synthetix/contracts/modules/core/CollateralModule.sol +++ b/protocol/synthetix/contracts/modules/core/CollateralModule.sol @@ -82,14 +82,18 @@ contract CollateralModule is ICollateralModule { .load(collateralType) .convertTokenToSystemAmount(tokenAmount); - (uint256 totalDeposited, uint256 totalAssigned, uint256 totalLocked) = account - .getCollateralTotals(collateralType); + ( + uint256 totalDeposited, + uint256 totalAssigned, + uint256 totalLocked, + uint256 pendingToDelegate + ) = account.getCollateralTotals(collateralType); // The amount that cannot be withdrawn from the protocol is the max of either // locked collateral or delegated collateral. uint256 unavailableCollateral = totalLocked > totalAssigned ? totalLocked : totalAssigned; - uint256 availableForWithdrawal = totalDeposited - unavailableCollateral; + uint256 availableForWithdrawal = totalDeposited - unavailableCollateral - pendingToDelegate; if (tokenAmountD18 > availableForWithdrawal) { revert InsufficientAccountCollateral(tokenAmountD18); } @@ -111,7 +115,12 @@ contract CollateralModule is ICollateralModule { external view override - returns (uint256 totalDeposited, uint256 totalAssigned, uint256 totalLocked) + returns ( + uint256 totalDeposited, + uint256 totalAssigned, + uint256 totalLocked, + uint256 totalPendingToDelegate + ) { return Account.load(accountId).getCollateralTotals(collateralType); } @@ -212,7 +221,7 @@ contract CollateralModule is ICollateralModule { AccountRBAC._ADMIN_PERMISSION ); - (uint256 totalDeposited, , uint256 totalLocked) = account.getCollateralTotals( + (uint256 totalDeposited, , uint256 totalLocked, ) = account.getCollateralTotals( collateralType ); diff --git a/protocol/synthetix/contracts/modules/core/VaultIntentViewsModule.sol b/protocol/synthetix/contracts/modules/core/VaultIntentViewsModule.sol new file mode 100644 index 0000000000..7a425b6641 --- /dev/null +++ b/protocol/synthetix/contracts/modules/core/VaultIntentViewsModule.sol @@ -0,0 +1,159 @@ +//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"; + +import "../../storage/Account.sol"; +import "../../storage/Pool.sol"; +import "../../storage/AccountDelegationIntents.sol"; + +import "../../interfaces/IVaultIntentViewsModule.sol"; + +/** + * @title Views for Delegation Intents for Vault. + * @dev See IVaultIntentViewsModule. + */ +contract VaultIntentViewsModule is IVaultIntentViewsModule { + using SetUtil for SetUtil.UintSet; + using DecimalMath for uint256; + using Pool for Pool.Data; + using Vault for Vault.Data; + using VaultEpoch for VaultEpoch.Data; + using Collateral for Collateral.Data; + using CollateralConfiguration for CollateralConfiguration.Data; + using SafeCastU256 for uint256; + using SafeCastI256 for int256; + using AccountDelegationIntents for AccountDelegationIntents.Data; + using DelegationIntent for DelegationIntent.Data; + using Account for Account.Data; + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getAccountIntent( + uint128 accountId, + uint256 intentId + ) external view override returns (uint128, address, int256, uint256, uint32, uint32, uint32) { + DelegationIntent.Data storage intent = Account + .load(accountId) + .getDelegationIntents() + .getIntent(intentId); + return ( + intent.poolId, + intent.collateralType, + intent.deltaCollateralAmountD18, + intent.leverage, + intent.declarationTime, + intent.processingStartTime(), + intent.processingEndTime() + ); + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getAccountIntentIds( + uint128 accountId + ) external view override returns (uint256[] memory) { + return Account.load(accountId).getDelegationIntents().intentsId.values(); + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getAccountExpiredIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view override returns (uint256[] memory expiredIntents, uint256 foundItems) { + uint256[] memory allIntents = Account + .load(accountId) + .getDelegationIntents() + .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[foundItems] = allIntents[i]; + foundItems++; + } + } + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getAccountExecutableIntentIds( + uint128 accountId, + uint256 maxProcessableIntent + ) external view override returns (uint256[] memory executableIntents, uint256 foundItems) { + uint128 accountEpochId = Account.load(accountId).currentDelegationIntentsEpoch; + uint256[] memory allIntents = Account + .load(accountId) + .getDelegationIntents() + .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(accountEpochId)) { + executableIntents[foundItems] = allIntents[i]; + foundItems++; + } + } + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getIntentDelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view override returns (uint256) { + return + Account.load(accountId).getDelegationIntents().delegatedAmountPerCollateral[ + collateralType + ]; + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getIntentUndelegatedPerCollateral( + uint128 accountId, + address collateralType + ) external view override returns (uint256) { + return + Account.load(accountId).getDelegationIntents().unDelegatedAmountPerCollateral[ + collateralType + ]; + } + + /** + * @inheritdoc IVaultIntentViewsModule + */ + function getExecutableDelegationAccumulated( + uint128 accountId, + uint128 poolId, + address collateralType + ) external view override returns (int256 accumulatedIntentDelta) { + uint256[] memory intentIds = Account.load(accountId).getDelegationIntents().intentIdsByPair( + poolId, + collateralType + ); + + for (uint256 i = 0; i < intentIds.length; i++) { + DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); + if (!intent.intentExpired()) { + accumulatedIntentDelta += intent.deltaCollateralAmountD18; + } + } + } +} diff --git a/protocol/synthetix/contracts/modules/core/VaultModule.sol b/protocol/synthetix/contracts/modules/core/VaultModule.sol index 53dd96bb5a..86d2f7fc6b 100644 --- a/protocol/synthetix/contracts/modules/core/VaultModule.sol +++ b/protocol/synthetix/contracts/modules/core/VaultModule.sol @@ -20,21 +20,13 @@ import "../../interfaces/IVaultModule.sol"; */ contract VaultModule is IVaultModule { using SetUtil for SetUtil.UintSet; - using SetUtil for SetUtil.Bytes32Set; - using SetUtil for SetUtil.AddressSet; using DecimalMath for uint256; using Pool for Pool.Data; using Vault for Vault.Data; using VaultEpoch for VaultEpoch.Data; using Collateral for Collateral.Data; using CollateralConfiguration for CollateralConfiguration.Data; - using AccountRBAC for AccountRBAC.Data; - using Distribution for Distribution.Data; - using CollateralConfiguration for CollateralConfiguration.Data; - using ScalableMapping for ScalableMapping.Data; - using SafeCastU128 for uint128; using SafeCastU256 for uint256; - using SafeCastI128 for int128; using SafeCastI256 for int256; using AccountDelegationIntents for AccountDelegationIntents.Data; using DelegationIntent for DelegationIntent.Data; @@ -112,19 +104,28 @@ 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()) { + + uint256 accumulatedDelegatedDelta = deltaCollateralAmountD18 > 0 + ? accountIntents.delegatedAmountPerCollateral[collateralType] + + deltaCollateralAmountD18.toUint() + : accountIntents.delegatedAmountPerCollateral[collateralType]; + uint256 accumulatedUnDelegatedDelta = deltaCollateralAmountD18 < 0 + ? accountIntents.unDelegatedAmountPerCollateral[collateralType] + + (-1 * deltaCollateralAmountD18).toUint() + : accountIntents.unDelegatedAmountPerCollateral[collateralType]; + + if (deltaCollateralAmountD18 < 0 && currentCollateralAmount < accumulatedUnDelegatedDelta) { revert ExceedingUndelegateAmount( deltaCollateralAmountD18, - accountIntents.netDelegatedAmountPerCollateral[collateralType], - accumulatedDelta, + accountIntents.unDelegatedAmountPerCollateral[collateralType], + accumulatedUnDelegatedDelta.toInt() * -1, currentCollateralAmount ); } - uint256 newCollateralAmountD18 = (currentCollateralAmount.toInt() + accumulatedDelta) - .toUint(); + uint256 newCollateralAmountD18 = deltaCollateralAmountD18 > 0 + ? currentCollateralAmount + accumulatedDelegatedDelta + : currentCollateralAmount - accumulatedUnDelegatedDelta; // Each collateral type may specify a minimum collateral amount that can be delegated. // See CollateralConfiguration.minDelegationD18. @@ -162,6 +163,7 @@ contract VaultModule is IVaultModule { intent.deltaCollateralAmountD18 = deltaCollateralAmountD18; intent.leverage = leverage; intent.declarationTime = block.timestamp.to32(); + intent.epochId = account.currentDelegationIntentsEpoch; // Add intent to the account's delegation intents. accountIntents.addIntent(intent, intentId); @@ -197,6 +199,8 @@ contract VaultModule is IVaultModule { .load(accountId) .getDelegationIntents(); + uint128 accountEpochId = Account.load(accountId).currentDelegationIntentsEpoch; + for (uint256 i = 0; i < intentIds.length; i++) { uint256 intentId = intentIds[i]; DelegationIntent.Data storage intent = DelegationIntent.load(intentId); @@ -204,7 +208,7 @@ contract VaultModule is IVaultModule { revert DelegationIntentNotInCurrentEpoch(intentId); } - if (!intent.isExecutable()) { + if (!intent.isExecutable(accountEpochId)) { // emit an Skipped event emit DelegationIntentSkipped( intentId, @@ -411,119 +415,6 @@ contract VaultModule is IVaultModule { return Pool.loadExisting(poolId).currentVaultDebt(collateralType); } - /** - * @inheritdoc IVaultModule - */ - function getAccountIntent( - uint128 accountId, - uint256 intentId - ) external view override returns (uint128, address, int256, uint256, uint32, uint32, uint32) { - DelegationIntent.Data storage intent = Account - .load(accountId) - .getDelegationIntents() - .getIntent(intentId); - return ( - intent.poolId, - intent.collateralType, - intent.deltaCollateralAmountD18, - intent.leverage, - intent.declarationTime, - intent.processingStartTime(), - intent.processingEndTime() - ); - } - - /** - * @inheritdoc IVaultModule - */ - function getAccountIntentIds( - uint128 accountId - ) external view override returns (uint256[] memory) { - return Account.load(accountId).getDelegationIntents().intentsId.values(); - } - - /** - * @inheritdoc IVaultModule - */ - function getAccountExpiredIntentIds( - uint128 accountId, - uint256 maxProcessableIntent - ) external view override returns (uint256[] memory expiredIntents, uint256 foundItems) { - uint256[] memory allIntents = Account - .load(accountId) - .getDelegationIntents() - .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[foundItems] = allIntents[i]; - foundItems++; - } - } - } - - /** - * @inheritdoc IVaultModule - */ - function getAccountExecutableIntentIds( - uint128 accountId, - uint256 maxProcessableIntent - ) external view override returns (uint256[] memory executableIntents, uint256 foundItems) { - uint256[] memory allIntents = Account - .load(accountId) - .getDelegationIntents() - .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[foundItems] = allIntents[i]; - foundItems++; - } - } - } - - /** - * @inheritdoc IVaultModule - */ - function getNetDelegatedPerCollateral( - uint128 accountId, - address collateralType - ) external view override returns (int256) { - return - Account.load(accountId).getDelegationIntents().netDelegatedAmountPerCollateral[ - collateralType - ]; - } - - /** - * @inheritdoc IVaultModule - */ - function getExecutableDelegationAccumulated( - uint128 accountId, - uint128 poolId, - address collateralType - ) external view override returns (int256 accumulatedIntentDelta) { - uint256[] memory intentIds = Account.load(accountId).getDelegationIntents().intentIdsByPair( - poolId, - collateralType - ); - accumulatedIntentDelta = 0; - for (uint256 i = 0; i < intentIds.length; i++) { - DelegationIntent.Data storage intent = DelegationIntent.load(intentIds[i]); - if (!intent.intentExpired()) { - accumulatedIntentDelta += intent.deltaCollateralAmountD18; - } - } - } - /** * @inheritdoc IVaultModule */ diff --git a/protocol/synthetix/contracts/storage/Account.sol b/protocol/synthetix/contracts/storage/Account.sol index d9c398a4c0..256c0132b7 100644 --- a/protocol/synthetix/contracts/storage/Account.sol +++ b/protocol/synthetix/contracts/storage/Account.sol @@ -112,15 +112,23 @@ library Account { ) internal view - returns (uint256 totalDepositedD18, uint256 totalAssignedD18, uint256 totalLockedD18) + returns ( + uint256 totalDepositedD18, + uint256 totalAssignedD18, + uint256 totalLockedD18, + uint256 totalIntendedToDelegateD18 + ) { totalAssignedD18 = getAssignedCollateral(self, collateralType); totalDepositedD18 = totalAssignedD18 + self.collaterals[collateralType].amountAvailableForDelegationD18; totalLockedD18 = self.collaterals[collateralType].getTotalLocked(); + totalIntendedToDelegateD18 = getDelegationIntents(self).delegatedAmountPerCollateral[ + collateralType + ]; - return (totalDepositedD18, totalAssignedD18, totalLockedD18); + return (totalDepositedD18, totalAssignedD18, totalLockedD18, totalIntendedToDelegateD18); } /** diff --git a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol index eb99d655b7..b2c749571a 100644 --- a/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol +++ b/protocol/synthetix/contracts/storage/AccountDelegationIntents.sol @@ -8,7 +8,7 @@ import "../interfaces/IVaultModule.sol"; import "./Account.sol"; /** - * @title Represents a delegation (or undelegation) intent. + * @title Contains the delegation intents for an account on an specific epoch. */ library AccountDelegationIntents { using SafeCastI256 for int256; @@ -25,7 +25,8 @@ library AccountDelegationIntents { // accounting for the intents collateral delegated // Per Collateral SetUtil.AddressSet delegatedCollaterals; - mapping(address => int256) netDelegatedAmountPerCollateral; // collateralType => net delegatedCollateralAmount + mapping(address => uint256) delegatedAmountPerCollateral; // collateralType => sum of delegations (delegatedCollateralAmount) + mapping(address => uint256) unDelegatedAmountPerCollateral; // collateralType => sum of un-delegations (delegatedCollateralAmount) } function addIntent( @@ -42,8 +43,14 @@ library AccountDelegationIntents { ] .add(intentId); - self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent - .deltaCollateralAmountD18; + if (delegationIntent.deltaCollateralAmountD18 > 0) { + self.delegatedAmountPerCollateral[delegationIntent.collateralType] += delegationIntent + .deltaCollateralAmountD18 + .toUint(); + } else { + self.unDelegatedAmountPerCollateral[delegationIntent.collateralType] += (-1 * + delegationIntent.deltaCollateralAmountD18).toUint(); + } if (!self.delegatedCollaterals.contains(delegationIntent.collateralType)) { self.delegatedCollaterals.add(delegationIntent.collateralType); @@ -68,8 +75,14 @@ library AccountDelegationIntents { ] .remove(intentId); - self.netDelegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent - .deltaCollateralAmountD18; + if (delegationIntent.deltaCollateralAmountD18 > 0) { + self.delegatedAmountPerCollateral[delegationIntent.collateralType] -= delegationIntent + .deltaCollateralAmountD18 + .toUint(); + } else { + self.unDelegatedAmountPerCollateral[delegationIntent.collateralType] -= (-1 * + delegationIntent.deltaCollateralAmountD18).toUint(); + } } function getIntent( @@ -94,8 +107,7 @@ library AccountDelegationIntents { } 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 + // verifies the intent is in the current epoch return self.intentsId.contains(intentId); } diff --git a/protocol/synthetix/contracts/storage/DelegationIntent.sol b/protocol/synthetix/contracts/storage/DelegationIntent.sol index 13ba078e95..8c136be4bd 100644 --- a/protocol/synthetix/contracts/storage/DelegationIntent.sol +++ b/protocol/synthetix/contracts/storage/DelegationIntent.sol @@ -60,6 +60,10 @@ library DelegationIntent { * @notice The timestamp at which the intent was declared */ uint32 declarationTime; + /** + * @notice Id of the epoch where this intent was created. Used to validate if the epoch is still valid + */ + uint128 epochId; } /** @@ -103,22 +107,13 @@ library DelegationIntent { return _processingEndTime; } - function checkIsExecutable(Data storage self) internal view { - (uint32 _processingStartTime, uint32 _processingEndTime) = getProcessingWindow(self); - - if (block.timestamp < _processingStartTime) - revert IVaultModule.DelegationIntentNotReady( - self.declarationTime, - _processingStartTime - ); - if (block.timestamp >= _processingEndTime) - revert IVaultModule.DelegationIntentExpired(self.declarationTime, _processingEndTime); - } - - function isExecutable(Data storage self) internal view returns (bool) { + function isExecutable(Data storage self, uint128 currentEpochId) internal view returns (bool) { (uint32 _processingStartTime, uint32 _processingEndTime) = getProcessingWindow(self); - return block.timestamp >= _processingStartTime && block.timestamp < _processingEndTime; + return + currentEpochId == self.epochId && + block.timestamp >= _processingStartTime && + block.timestamp < _processingEndTime; } function intentExpired(Data storage self) internal view returns (bool) { diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index 1ac3bbabb3..93064f6f0a 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -411,7 +411,8 @@ library AccountDelegationIntents { SetUtil.UintSet intentsId; mapping(bytes32 => SetUtil.UintSet) intentsByPair; SetUtil.AddressSet delegatedCollaterals; - mapping(address => int256) netDelegatedAmountPerCollateral; + mapping(address => uint256) delegatedAmountPerCollateral; + mapping(address => uint256) unDelegatedAmountPerCollateral; } } @@ -511,6 +512,7 @@ library DelegationIntent { int256 deltaCollateralAmountD18; uint256 leverage; uint32 declarationTime; + uint128 epochId; } function load(uint256 id) internal pure returns (Data storage delegationIntent) { bytes32 s = keccak256(abi.encode("io.synthetix.synthetix.DelegationIntent", id)); diff --git a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts index b53c20f7ec..c273bbdfd1 100644 --- a/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/VaultModule.test.ts @@ -13,6 +13,7 @@ import { expectedToDeltaDelegatedCollateral, } from '../../../common'; import { wei } from '@synthetixio/wei'; +import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; describe('VaultModule', function () { const { @@ -1068,4 +1069,228 @@ describe('VaultModule', function () { assertBn.equal(await systems().Core.getWithdrawableMarketUsd(marketId), 0); }); }); + + describe('transient locked collateral (flashloan)', async () => { + let intentId: BigNumber; + let declareDelegateIntentTime: number; + let preTotalDeposited: BigNumber; + let preTotalAssigned: BigNumber; + let toWithdraw: BigNumber; + + before(restore); + + before('set market window times', async () => { + const previousConfiguration = await MockMarket.getDelegationCollateralConfiguration(); + await MockMarket.setDelegationCollateralConfiguration( + 100, + 20, + previousConfiguration[2], + previousConfiguration[3] + ); + }); + + before('declare intent to delegate', async () => { + const collateralsPre = await systems() + .Core.connect(owner) + .getAccountCollateral(accountId, collateralAddress()); + preTotalAssigned = collateralsPre.totalAssigned; + preTotalDeposited = collateralsPre.totalDeposited; + toWithdraw = preTotalDeposited.sub(preTotalAssigned); + + intentId = await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + declareDelegateIntentTime = await getTime(provider()); + }); + + const restoreToLocked = snapshotCheckpoint(provider); + + it('intended to delegate is transient locked', async () => { + const collaterals = await systems() + .Core.connect(owner) + .getAccountCollateral(accountId, collateralAddress()); + + assertBn.equal(collaterals.totalPendingToDelegate, depositAmount.mul(1)); + }); + + it('fails to withraw when collateral is reserved for the intent to delegate', async () => { + await assertRevert( + systems().Core.connect(user1).withdraw(accountId, collateralAddress(), toWithdraw), + 'InsufficientAccountCollateral("10000000000000000000")', + systems().Core + ); + }); + + describe('when the intent is processed and undelegated', async () => { + before(restoreToLocked); + + before('process the intent and undelegate the collateral', async () => { + await fastForwardTo(declareDelegateIntentTime + 110, provider()); + + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + + // Undelegate the collateral + await delegateCollateral( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(1), + ethers.utils.parseEther('1') + ); + }); + + it('allows to withdraw the collateral', async () => { + await systems().Core.connect(user1).withdraw(accountId, collateralAddress(), toWithdraw); + }); + }); + + describe('when the intent is expired and removed', async () => { + before(restoreToLocked); + + before('remove the expired intent', async () => { + await fastForwardTo(declareDelegateIntentTime + 130, provider()); + + await systems() + .Core.connect(user1) + .processIntentToDelegateCollateralByIntents(accountId, [intentId]); + }); + + it('allows to withdraw the collateral', async () => { + await systems().Core.connect(user1).withdraw(accountId, collateralAddress(), toWithdraw); + }); + }); + }); + + describe('undelegate what is delegated', async () => { + let intentToDelegateDelta: BigNumber; + + let prePositionCollateral: BigNumber; + + before(restore); + + before('declare intent to delegate', async () => { + prePositionCollateral = await systems().Core.getPositionCollateral( + accountId, + poolId, + collateralAddress() + ); + + intentToDelegateDelta = await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2) + ); + + await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(2), + ethers.utils.parseEther('1') + ); + + // At this point the account has + // 1x depositAmount already delegated + // 1x depositAmount pending to delegate + }); + + const restoreToLocked = snapshotCheckpoint(provider); + + it('describes checkpoint', () => { + assert.ok(true); + }); + + describe('when an intent is pending, the collateral is transiently locked', () => { + before(restoreToLocked); + + it('intended to delegate is transient locked', async () => { + const collaterals = await systems() + .Core.connect(owner) + .getAccountCollateral(accountId, collateralAddress()); + + assertBn.equal(collaterals.totalPendingToDelegate, depositAmount.mul(1)); + }); + + it('fails to undelegate what is pending in a single undelegation', async () => { + // Tries to undelegate what was delegated + what is pending + const delta = prePositionCollateral.add(intentToDelegateDelta).mul(-1); + await assertRevert( + systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress(), + delta, + ethers.utils.parseEther('1') + ), + 'ExceedingUndelegateAmount', + systems().Core + ); + }); + }); + + describe('fails to undelegate what is pending on a multiple shots', async () => { + before(restoreToLocked); + + before('add a previous undelegation intent', async () => { + // set the first undelegation (allowed, since is what is already delegated) + // notice, since we are using a fixed expected value based on current delegated, + // setting the expected amount to 0 will make an undelegation of 1x depositAmount + await declareDelegateIntent( + systems, + owner, + user1, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(0), + ethers.utils.parseEther('1') + ); + }); + + it('fails to set the second intent to delegate', async () => { + const delta = await expectedToDeltaDelegatedCollateral( + systems, + accountId, + poolId, + collateralAddress(), + depositAmount.mul(0) + ); + + // exceeds the amount that is delegated (still pending) + await assertRevert( + systems() + .Core.connect(user1) + .declareIntentToDelegateCollateral( + accountId, + poolId, + collateralAddress(), + delta, + ethers.utils.parseEther('1') + ), + 'ExceedingUndelegateAmount', + systems().Core + ); + }); + }); + }); });