diff --git a/protocol/x/vault/keeper/msg_server_withdraw_from_megavault_test.go b/protocol/x/vault/keeper/msg_server_withdraw_from_megavault_test.go index 93463c6cda..1470c4ff1d 100644 --- a/protocol/x/vault/keeper/msg_server_withdraw_from_megavault_test.go +++ b/protocol/x/vault/keeper/msg_server_withdraw_from_megavault_test.go @@ -160,7 +160,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { { id: constants.Vault_Clob0, params: vaulttypes.VaultParams{ - Status: vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + Status: vaulttypes.VaultStatus_VAULT_STATUS_STAND_BY, }, assetQuoteQuantums: big.NewInt(400), positionBaseQuantums: big.NewInt(0), diff --git a/protocol/x/vault/keeper/orders_test.go b/protocol/x/vault/keeper/orders_test.go index 3d86f78dc0..880f8ab774 100644 --- a/protocol/x/vault/keeper/orders_test.go +++ b/protocol/x/vault/keeper/orders_test.go @@ -66,7 +66,7 @@ func TestRefreshAllVaultOrders(t *testing.T) { }, activationThresholdQuoteQuantums: big.NewInt(1_000_000_000), }, - "Two Vaults, One Stand-By, One Deactivated, Both Above Activation Threshold": { + "Two Vaults, One Stand-By, One Deactivated, One Above Activation Threshold": { vaultIds: []vaulttypes.VaultId{ constants.Vault_Clob0, constants.Vault_Clob1, @@ -77,7 +77,7 @@ func TestRefreshAllVaultOrders(t *testing.T) { }, assetQuantums: []*big.Int{ big.NewInt(1_000_000_000), // 1,000 USDC - big.NewInt(1_000_000_001), + big.NewInt(0), }, activationThresholdQuoteQuantums: big.NewInt(1_000_000_000), }, diff --git a/protocol/x/vault/keeper/params.go b/protocol/x/vault/keeper/params.go index 3e6273138b..d34f2be675 100644 --- a/protocol/x/vault/keeper/params.go +++ b/protocol/x/vault/keeper/params.go @@ -68,6 +68,16 @@ func (k Keeper) SetVaultParams( return err } + if vaultParams.Status == types.VaultStatus_VAULT_STATUS_DEACTIVATED { + vaultEquity, err := k.GetVaultEquity(ctx, vaultId) + if err != nil { + return err + } + if vaultEquity.Sign() > 0 { + return types.ErrDeactivatePositiveEquityVault + } + } + b := k.cdc.MustMarshal(&vaultParams) store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.VaultParamsKeyPrefix)) store.Set(vaultId.ToStateKey(), b) diff --git a/protocol/x/vault/keeper/params_test.go b/protocol/x/vault/keeper/params_test.go index 97783d1d4c..7e65060510 100644 --- a/protocol/x/vault/keeper/params_test.go +++ b/protocol/x/vault/keeper/params_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "testing" + "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" "github.com/dydxprotocol/v4-chain/protocol/dtypes" @@ -13,8 +14,9 @@ import ( v1 "github.com/dydxprotocol/v4-chain/protocol/indexer/protocol/v1" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/keeper" - "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" + vaulttypes "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" "github.com/stretchr/testify/require" ) @@ -25,10 +27,10 @@ func TestGetSetDefaultQuotingParams(t *testing.T) { // Params should have default values at genesis. params := k.GetDefaultQuotingParams(ctx) - require.Equal(t, types.DefaultQuotingParams(), params) + require.Equal(t, vaulttypes.DefaultQuotingParams(), params) // Set new params and get. - newParams := types.QuotingParams{ + newParams := vaulttypes.QuotingParams{ Layers: 3, SpreadMinPpm: 4_000, SpreadBufferPpm: 2_000, @@ -42,7 +44,7 @@ func TestGetSetDefaultQuotingParams(t *testing.T) { require.Equal(t, newParams, k.GetDefaultQuotingParams(ctx)) // Set invalid params and get. - invalidParams := types.QuotingParams{ + invalidParams := vaulttypes.QuotingParams{ Layers: 3, SpreadMinPpm: 4_000, SpreadBufferPpm: 2_000, @@ -59,9 +61,15 @@ func TestGetSetDefaultQuotingParams(t *testing.T) { func TestGetSetVaultParams(t *testing.T) { tests := map[string]struct { // Vault id. - vaultId types.VaultId + vaultId vaulttypes.VaultId + // Existing vault params, if any. + existingVaultParams *vaulttypes.VaultParams + // Asset quote quantums that vault has. + assetQuoteQuantums int64 + // Position base quantums that vault has. + positionBaseQuantums int64 // Vault params to set. - vaultParams *types.VaultParams + vaultParams *vaulttypes.VaultParams // Expected on-chain indexer events expectedIndexerEvents []*indexerevents.UpsertVaultEventV1 // Expected error. @@ -89,24 +97,64 @@ func TestGetSetVaultParams(t *testing.T) { }, }, }, - "Success - Non-existent Vault Params": { - vaultId: constants.Vault_Clob1, - vaultParams: nil, + "Success - Deactivate a vault with zero equity": { + vaultId: constants.Vault_Clob1, + existingVaultParams: &constants.VaultParams, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + }, + expectedIndexerEvents: []*indexerevents.UpsertVaultEventV1{ + { + Address: constants.Vault_Clob1.ToModuleAccountAddress(), + ClobPairId: constants.Vault_Clob1.Number, + Status: v1.VaultStatusToIndexerVaultStatus( + vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + ), + }, + }, + }, + "Success - Deactivate a vault with negative equity": { + vaultId: constants.Vault_Clob1, + existingVaultParams: &constants.VaultParams, + assetQuoteQuantums: 0, + positionBaseQuantums: -1, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + }, + expectedIndexerEvents: []*indexerevents.UpsertVaultEventV1{ + { + Address: constants.Vault_Clob1.ToModuleAccountAddress(), + ClobPairId: constants.Vault_Clob1.Number, + Status: v1.VaultStatusToIndexerVaultStatus( + vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + ), + }, + }, + }, + "Failure - Deactivate a vault with positive equity": { + vaultId: constants.Vault_Clob1, + existingVaultParams: &constants.VaultParams, + assetQuoteQuantums: 0, + positionBaseQuantums: 1, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + }, + expectedErr: vaulttypes.ErrDeactivatePositiveEquityVault, }, "Failure - Unspecified Status": { vaultId: constants.Vault_Clob0, - vaultParams: &types.VaultParams{ + vaultParams: &vaulttypes.VaultParams{ QuotingParams: &constants.QuotingParams, }, - expectedErr: types.ErrUnspecifiedVaultStatus, + expectedErr: vaulttypes.ErrUnspecifiedVaultStatus, }, "Failure - Invalid Quoting Params": { vaultId: constants.Vault_Clob0, - vaultParams: &types.VaultParams{ - Status: types.VaultStatus_VAULT_STATUS_STAND_BY, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_STAND_BY, QuotingParams: &constants.InvalidQuotingParams, }, - expectedErr: types.ErrInvalidOrderExpirationSeconds, + expectedErr: vaulttypes.ErrInvalidOrderExpirationSeconds, }, } @@ -116,21 +164,68 @@ func TestGetSetVaultParams(t *testing.T) { appOpts := map[string]interface{}{ indexer.MsgSenderInstanceForTest: msgSender, } - tApp := testapp.NewTestAppBuilder(t).WithAppOptions(appOpts).Build() + tApp := testapp.NewTestAppBuilder(t).WithAppOptions(appOpts). + WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *vaulttypes.GenesisState) { + if tc.existingVaultParams != nil { + genesisState.Vaults = []vaulttypes.Vault{ + { + VaultId: tc.vaultId, + VaultParams: *tc.existingVaultParams, + }, + } + } + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + assetPositions := []*satypes.AssetPosition{} + if tc.assetQuoteQuantums != 0 { + assetPositions = append(assetPositions, &satypes.AssetPosition{ + AssetId: constants.Usdc.GetId(), + Quantums: dtypes.NewInt(tc.assetQuoteQuantums), + }) + } + perpPositions := []*satypes.PerpetualPosition{} + if tc.positionBaseQuantums != 0 { + perpPositions = append(perpPositions, &satypes.PerpetualPosition{ + PerpetualId: tc.vaultId.Number, + Quantums: dtypes.NewInt(tc.positionBaseQuantums), + }) + } + genesisState.Subaccounts = []satypes.Subaccount{ + { + Id: tc.vaultId.ToSubaccountId(), + AssetPositions: assetPositions, + PerpetualPositions: perpPositions, + }, + } + }, + ) + return genesis + }).Build() ctx := tApp.InitChain() k := tApp.App.VaultKeeper - if tc.vaultParams == nil { + if tc.existingVaultParams == nil { _, exists := k.GetVaultParams(ctx, tc.vaultId) require.False(t, exists) - return } err := k.SetVaultParams(ctx, tc.vaultId, *tc.vaultParams) if tc.expectedErr != nil { require.ErrorIs(t, err, tc.expectedErr) - _, exists := k.GetVaultParams(ctx, tc.vaultId) - require.False(t, exists) + v, exists := k.GetVaultParams(ctx, tc.vaultId) + if tc.existingVaultParams == nil { + require.False(t, exists) + } else { + require.True(t, exists) + require.Equal(t, *tc.existingVaultParams, v) + } } else { require.NoError(t, err) p, exists := k.GetVaultParams(ctx, tc.vaultId) @@ -150,17 +245,17 @@ func TestGetVaultQuotingParams(t *testing.T) { tests := map[string]struct { /* Setup */ // Vault id. - vaultId types.VaultId + vaultId vaulttypes.VaultId // Vault params to set. - vaultParams *types.VaultParams + vaultParams *vaulttypes.VaultParams /* Expectations */ // Whether quoting params should be default. shouldBeDefault bool }{ "Default Quoting Params": { vaultId: constants.Vault_Clob0, - vaultParams: &types.VaultParams{ - Status: types.VaultStatus_VAULT_STATUS_CLOSE_ONLY, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_CLOSE_ONLY, }, shouldBeDefault: true, }, @@ -187,7 +282,7 @@ func TestGetVaultQuotingParams(t *testing.T) { p, exists := k.GetVaultQuotingParams(ctx, tc.vaultId) require.True(t, exists) if tc.shouldBeDefault { - require.Equal(t, types.DefaultQuotingParams(), p) + require.Equal(t, vaulttypes.DefaultQuotingParams(), p) } else { require.Equal(t, *tc.vaultParams.QuotingParams, p) } @@ -208,14 +303,14 @@ func TestGetSetOperatorParams(t *testing.T) { params := k.GetOperatorParams(ctx) require.Equal( t, - types.OperatorParams{ + vaulttypes.OperatorParams{ Operator: constants.GovAuthority, }, params, ) // Set operator to Alice. - newParams := types.OperatorParams{ + newParams := vaulttypes.OperatorParams{ Operator: constants.AliceAccAddress.String(), } err := k.SetOperatorParams(ctx, newParams) @@ -223,7 +318,7 @@ func TestGetSetOperatorParams(t *testing.T) { require.Equal(t, newParams, k.GetOperatorParams(ctx)) // Set invalid operator and get. - invalidParams := types.OperatorParams{ + invalidParams := vaulttypes.OperatorParams{ Operator: "", } err = k.SetOperatorParams(ctx, invalidParams) diff --git a/protocol/x/vault/keeper/vault_test.go b/protocol/x/vault/keeper/vault_test.go index f467e9b70f..76078fab3f 100644 --- a/protocol/x/vault/keeper/vault_test.go +++ b/protocol/x/vault/keeper/vault_test.go @@ -37,7 +37,7 @@ func TestDecommissionNonPositiveEquityVaults(t *testing.T) { }, statuses: []vaulttypes.VaultStatus{ vaulttypes.VaultStatus_VAULT_STATUS_QUOTING, - vaulttypes.VaultStatus_VAULT_STATUS_DEACTIVATED, + vaulttypes.VaultStatus_VAULT_STATUS_STAND_BY, }, equities: []*big.Int{ big.NewInt(1), @@ -464,7 +464,7 @@ func TestGetMegavaultEquity(t *testing.T) { }, expectedMegavaultEquity: big.NewInt(1_345), }, - "Megavault subaccount with 1000 equity, One quoting vault with 345 equity, One deactivated vault with 5 equity,": { + "Megavault subaccount with 1000 equity, One quoting vault with 345 equity, One deactivated vault with -5 equity,": { megavaultSaEquity: big.NewInt(1_000), vaults: []vaulttypes.Vault{ { @@ -482,9 +482,9 @@ func TestGetMegavaultEquity(t *testing.T) { }, vaultEquities: []*big.Int{ big.NewInt(345), - big.NewInt(5), + big.NewInt(-5), }, - expectedMegavaultEquity: big.NewInt(1_350), + expectedMegavaultEquity: big.NewInt(1_345), }, } for name, tc := range tests { diff --git a/protocol/x/vault/types/errors.go b/protocol/x/vault/types/errors.go index b68028ece2..b7972d6ad2 100644 --- a/protocol/x/vault/types/errors.go +++ b/protocol/x/vault/types/errors.go @@ -145,4 +145,9 @@ var ( 28, "Insufficient redeemed quote quantums", ) + ErrDeactivatePositiveEquityVault = errorsmod.Register( + ModuleName, + 29, + "Cannot deactivate vaults with positive equity", + ) )