diff --git a/protocol/x/vault/keeper/deposit.go b/protocol/x/vault/keeper/deposit.go index ca7b1d7323..60b2b64f44 100644 --- a/protocol/x/vault/keeper/deposit.go +++ b/protocol/x/vault/keeper/deposit.go @@ -65,7 +65,7 @@ func (k Keeper) MintShares( ) (mintedShares *big.Int, err error) { // Quantums to deposit should be positive. if quantumsToDeposit.Sign() <= 0 { - return nil, types.ErrInvalidDepositAmount + return nil, types.ErrInvalidQuoteQuantums } // Get existing TotalShares of the vault. existingTotalShares := k.GetTotalShares(ctx).NumShares.BigInt() diff --git a/protocol/x/vault/keeper/deposit_test.go b/protocol/x/vault/keeper/deposit_test.go index 7acbe3e6eb..8084a571e9 100644 --- a/protocol/x/vault/keeper/deposit_test.go +++ b/protocol/x/vault/keeper/deposit_test.go @@ -123,14 +123,14 @@ func TestMintShares(t *testing.T) { totalShares: big.NewInt(1), owner: constants.AliceAccAddress.String(), quantumsToDeposit: big.NewInt(0), - expectedErr: vaulttypes.ErrInvalidDepositAmount, + expectedErr: vaulttypes.ErrInvalidQuoteQuantums, }, "Equity 0, TotalShares 0, Deposit -1": { equity: big.NewInt(0), totalShares: big.NewInt(0), owner: constants.AliceAccAddress.String(), quantumsToDeposit: big.NewInt(-1), - expectedErr: vaulttypes.ErrInvalidDepositAmount, + expectedErr: vaulttypes.ErrInvalidQuoteQuantums, }, "Equity 1000, TotalShares 1, Deposit 100": { equity: big.NewInt(1_000), diff --git a/protocol/x/vault/keeper/msg_server_allocate_to_vault.go b/protocol/x/vault/keeper/msg_server_allocate_to_vault.go index d7ff60f66c..491103ddf4 100644 --- a/protocol/x/vault/keeper/msg_server_allocate_to_vault.go +++ b/protocol/x/vault/keeper/msg_server_allocate_to_vault.go @@ -8,6 +8,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + sendingtypes "github.com/dydxprotocol/v4-chain/protocol/x/sending/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -53,12 +54,14 @@ func (k msgServer) AllocateToVault( } // Transfer from main vault to the specified vault. - if err := k.Keeper.subaccountsKeeper.TransferFundsFromSubaccountToSubaccount( + if err := k.Keeper.sendingKeeper.ProcessTransfer( ctx, - types.MegavaultMainSubaccount, - *msg.VaultId.ToSubaccountId(), - assetstypes.AssetUsdc.Id, - msg.QuoteQuantums.BigInt(), + &sendingtypes.Transfer{ + Sender: types.MegavaultMainSubaccount, + Recipient: *msg.VaultId.ToSubaccountId(), + AssetId: assetstypes.AssetUsdc.Id, + Amount: msg.QuoteQuantums.BigInt().Uint64(), // validated to be positive above. + }, ); err != nil { return nil, err } diff --git a/protocol/x/vault/keeper/msg_server_allocate_to_vault_test.go b/protocol/x/vault/keeper/msg_server_allocate_to_vault_test.go index b6279a183c..cb22fa2ee2 100644 --- a/protocol/x/vault/keeper/msg_server_allocate_to_vault_test.go +++ b/protocol/x/vault/keeper/msg_server_allocate_to_vault_test.go @@ -1,18 +1,21 @@ package keeper_test import ( + "bytes" + "math" + "math/big" "testing" + abcitypes "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "github.com/dydxprotocol/v4-chain/protocol/dtypes" - "github.com/dydxprotocol/v4-chain/protocol/lib" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" - "github.com/dydxprotocol/v4-chain/protocol/x/vault/keeper" vaulttypes "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -28,10 +31,16 @@ func TestMsgAllocateToVault(t *testing.T) { vaultParams *vaulttypes.VaultParams // Msg. msg *vaulttypes.MsgAllocateToVault - // Expected error. - expectedErr string + // Signer of above msg. + signer string + // A string that CheckTx response should contain, if any. + checkTxResponseContains string + // Whether CheckTx fails. + checkTxFails bool + // Whether DeliverTx fails. + deliverTxFails bool }{ - "Success - Gov Authority, Allocate 50 to Vault Clob 0, Existing vault params": { + "Success - Allocate 50 to Vault Clob 0, Existing vault params": { operator: constants.AliceAccAddress.String(), mainVaultQuantums: 100, subVaultQuantums: 0, @@ -39,33 +48,66 @@ func TestMsgAllocateToVault(t *testing.T) { Status: vaulttypes.VaultStatus_VAULT_STATUS_QUOTING, }, msg: &vaulttypes.MsgAllocateToVault{ - Authority: lib.GovModuleAddress.String(), + Authority: constants.AliceAccAddress.String(), VaultId: constants.Vault_Clob0, QuoteQuantums: dtypes.NewInt(50), }, + signer: constants.AliceAccAddress.String(), }, - "Success - Gov Authority, Allocate 77 to Vault Clob 1, Non-existent Vault Params": { - operator: constants.AliceAccAddress.String(), + "Success - Allocate 77 to Vault Clob 1, Non-existent Vault Params": { + operator: constants.BobAccAddress.String(), mainVaultQuantums: 100, subVaultQuantums: 15, msg: &vaulttypes.MsgAllocateToVault{ - Authority: lib.GovModuleAddress.String(), + Authority: constants.BobAccAddress.String(), VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(77), }, + signer: constants.BobAccAddress.String(), + }, + "Failure - Operator Authority, allocating more than max uint64 quantums": { + operator: constants.CarlAccAddress.String(), + mainVaultQuantums: 100, + subVaultQuantums: 15, + msg: &vaulttypes.MsgAllocateToVault{ + Authority: constants.CarlAccAddress.String(), + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewIntFromBigInt( + new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + ), + }, + checkTxResponseContains: "QuoteQuantums must be positive and less than 2^64", + checkTxFails: true, + signer: constants.CarlAccAddress.String(), }, - "Success - Operator Authority, Allocate all to Vault Clob 1, Existing vault params": { + "Failure - Operator Authority, allocating zero quantums": { operator: constants.AliceAccAddress.String(), mainVaultQuantums: 100, subVaultQuantums: 15, - vaultParams: &vaulttypes.VaultParams{ - Status: vaulttypes.VaultStatus_VAULT_STATUS_CLOSE_ONLY, + msg: &vaulttypes.MsgAllocateToVault{ + Authority: constants.AliceAccAddress.String(), + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(0), }, + checkTxResponseContains: "QuoteQuantums must be positive", + checkTxFails: true, + signer: constants.AliceAccAddress.String(), + }, + "Failure - Operator Authority, allocating negative quantums": { + operator: constants.AliceAccAddress.String(), + mainVaultQuantums: 100, + subVaultQuantums: 15, msg: &vaulttypes.MsgAllocateToVault{ Authority: constants.AliceAccAddress.String(), - VaultId: constants.Vault_Clob1, - QuoteQuantums: dtypes.NewInt(100), + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(-1), }, + checkTxResponseContains: "QuoteQuantums must be positive", + checkTxFails: true, + signer: constants.AliceAccAddress.String(), }, "Failure - Operator Authority, Insufficient quantums to allocate to Vault Clob 0, Existing vault params": { operator: constants.AliceAccAddress.String(), @@ -79,7 +121,9 @@ func TestMsgAllocateToVault(t *testing.T) { VaultId: constants.Vault_Clob0, QuoteQuantums: dtypes.NewInt(101), }, - expectedErr: "failed to apply subaccount updates", + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Operator Authority, No corresponding clob pair": { operator: constants.AliceAccAddress.String(), @@ -93,7 +137,9 @@ func TestMsgAllocateToVault(t *testing.T) { }, QuoteQuantums: dtypes.NewInt(1), }, - expectedErr: vaulttypes.ErrClobPairNotFound.Error(), + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Invalid Authority, Non-existent Vault Params": { operator: constants.BobAccAddress.String(), @@ -104,7 +150,9 @@ func TestMsgAllocateToVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(77), }, - expectedErr: vaulttypes.ErrInvalidAuthority.Error(), + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Empty Authority, Existing vault params": { operator: constants.BobAccAddress.String(), @@ -115,7 +163,9 @@ func TestMsgAllocateToVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(77), }, - expectedErr: vaulttypes.ErrInvalidAuthority.Error(), + signer: constants.BobAccAddress.String(), + checkTxResponseContains: vaulttypes.ErrInvalidAuthority.Error(), + checkTxFails: true, }, } @@ -170,18 +220,60 @@ func TestMsgAllocateToVault(t *testing.T) { }).Build() ctx := tApp.InitChain() k := tApp.App.VaultKeeper - ms := keeper.NewMsgServerImpl(k) - // Allocate to vault. - _, err := ms.AllocateToVault(ctx, tc.msg) + // Invoke CheckTx. + CheckTx_MsgAllocateToVault := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: tc.signer, + Gas: constants.TestGasLimit, + FeeAmt: constants.TestFeeCoins_5Cents, + }, + tc.msg, + ) + checkTxResp := tApp.CheckTx(CheckTx_MsgAllocateToVault) + + // Check that CheckTx response log contains expected string, if any. + if tc.checkTxResponseContains != "" { + require.Contains(t, checkTxResp.Log, tc.checkTxResponseContains) + } + // Check that CheckTx succeeds or errors out as expected. + if tc.checkTxFails { + require.Conditionf(t, checkTxResp.IsErr, "Expected CheckTx to error. Response: %+v", checkTxResp) + return + } + require.Conditionf(t, checkTxResp.IsOK, "Expected CheckTx to succeed. Response: %+v", checkTxResp) + + // Advance to next block (and check that DeliverTx is as expected). + nextBlock := uint32(ctx.BlockHeight()) + 1 + if tc.deliverTxFails { + // Check that DeliverTx fails on `msgDepositToMegavault`. + ctx = tApp.AdvanceToBlock(nextBlock, testapp.AdvanceToBlockOptions{ + ValidateFinalizeBlock: func( + context sdktypes.Context, + request abcitypes.RequestFinalizeBlock, + response abcitypes.ResponseFinalizeBlock, + ) (haltChain bool) { + for i, tx := range request.Txs { + if bytes.Equal(tx, CheckTx_MsgAllocateToVault.Tx) { + require.True(t, response.TxResults[i].IsErr()) + } else { + require.True(t, response.TxResults[i].IsOK()) + } + } + return false + }, + }) + } else { + ctx = tApp.AdvanceToBlock(nextBlock, testapp.AdvanceToBlockOptions{}) + } // Check expectations. mainVault := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, vaulttypes.MegavaultMainSubaccount) subVault := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, *tc.msg.VaultId.ToSubaccountId()) require.Len(t, subVault.AssetPositions, 1) - if tc.expectedErr != "" { - require.ErrorContains(t, err, tc.expectedErr) - + if tc.deliverTxFails { // Verify that main vault and sub vault balances are unchanged. require.Len(t, mainVault.AssetPositions, 1) require.Equal( @@ -204,8 +296,6 @@ func TestMsgAllocateToVault(t *testing.T) { require.False(t, exists) } } else { - require.NoError(t, err) - // Verify that main vault and sub vault balances are updated. expectedMainVaultQuantums := tc.mainVaultQuantums - tc.msg.QuoteQuantums.BigInt().Uint64() if expectedMainVaultQuantums == 0 { diff --git a/protocol/x/vault/keeper/msg_server_deposit_to_megavault_test.go b/protocol/x/vault/keeper/msg_server_deposit_to_megavault_test.go index 54f8a9f50a..2e38c4012b 100644 --- a/protocol/x/vault/keeper/msg_server_deposit_to_megavault_test.go +++ b/protocol/x/vault/keeper/msg_server_deposit_to_megavault_test.go @@ -211,7 +211,7 @@ func TestMsgDepositToMegavault(t *testing.T) { depositAmount: big.NewInt(0), msgSigner: constants.Alice_Num0.Owner, checkTxFails: true, - checkTxResponseContains: "Deposit amount is invalid", + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), expectedOwnerShares: nil, }, { @@ -219,7 +219,7 @@ func TestMsgDepositToMegavault(t *testing.T) { depositAmount: big.NewInt(-1), msgSigner: constants.Bob_Num0.Owner, checkTxFails: true, - checkTxResponseContains: "Deposit amount is invalid", + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), expectedOwnerShares: nil, }, { @@ -230,7 +230,7 @@ func TestMsgDepositToMegavault(t *testing.T) { ), msgSigner: constants.Bob_Num0.Owner, checkTxFails: true, - checkTxResponseContains: "Deposit amount is invalid", + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), expectedOwnerShares: nil, }, }, diff --git a/protocol/x/vault/keeper/msg_server_retrieve_from_vault.go b/protocol/x/vault/keeper/msg_server_retrieve_from_vault.go index d4586eda70..72ebe3fb1d 100644 --- a/protocol/x/vault/keeper/msg_server_retrieve_from_vault.go +++ b/protocol/x/vault/keeper/msg_server_retrieve_from_vault.go @@ -7,6 +7,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" + sendingtypes "github.com/dydxprotocol/v4-chain/protocol/x/sending/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -33,12 +34,14 @@ func (k msgServer) RetrieveFromVault( } // Transfer from specified vault to main vault. - if err := k.Keeper.subaccountsKeeper.TransferFundsFromSubaccountToSubaccount( + if err := k.Keeper.sendingKeeper.ProcessTransfer( ctx, - *msg.VaultId.ToSubaccountId(), - types.MegavaultMainSubaccount, - assetstypes.AssetUsdc.Id, - msg.QuoteQuantums.BigInt(), + &sendingtypes.Transfer{ + Sender: *msg.VaultId.ToSubaccountId(), + Recipient: types.MegavaultMainSubaccount, + AssetId: assetstypes.AssetUsdc.Id, + Amount: msg.QuoteQuantums.BigInt().Uint64(), // validated to be positive above. + }, ); err != nil { return nil, err } diff --git a/protocol/x/vault/keeper/msg_server_retrieve_from_vault_test.go b/protocol/x/vault/keeper/msg_server_retrieve_from_vault_test.go index a5082e55f6..2847ed00cb 100644 --- a/protocol/x/vault/keeper/msg_server_retrieve_from_vault_test.go +++ b/protocol/x/vault/keeper/msg_server_retrieve_from_vault_test.go @@ -1,18 +1,21 @@ package keeper_test import ( + "bytes" + "math" + "math/big" "testing" + abcitypes "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "github.com/dydxprotocol/v4-chain/protocol/dtypes" - "github.com/dydxprotocol/v4-chain/protocol/lib" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" - "github.com/dydxprotocol/v4-chain/protocol/x/vault/keeper" vaulttypes "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -30,23 +33,65 @@ func TestMsgRetrieveFromVault(t *testing.T) { vaultParams *vaulttypes.VaultParams // Msg. msg *vaulttypes.MsgRetrieveFromVault - // Expected error. - expectedErr string + // Signer of above msg. + signer string + // A string that CheckTx response should contain, if any. + checkTxResponseContains string + // Whether CheckTx fails. + checkTxFails bool + // Whether DeliverTx fails. + deliverTxFails bool }{ - "Success - Gov Authority, Retrieve 50 From Vault Clob 0": { - operator: constants.AliceAccAddress.String(), + "Success - Retrieve 50 From Vault Clob 0": { + operator: constants.DaveAccAddress.String(), mainVaultQuantums: 100, subVaultQuantums: 200, vaultParams: &vaulttypes.VaultParams{ Status: vaulttypes.VaultStatus_VAULT_STATUS_QUOTING, }, msg: &vaulttypes.MsgRetrieveFromVault{ - Authority: lib.GovModuleAddress.String(), + Authority: constants.DaveAccAddress.String(), VaultId: constants.Vault_Clob0, QuoteQuantums: dtypes.NewInt(50), }, + signer: constants.DaveAccAddress.String(), + }, + "Success - Retrieve all from Vault Clob 1": { + operator: constants.BobAccAddress.String(), + mainVaultQuantums: 0, + subVaultQuantums: 3_500_000, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_CLOSE_ONLY, + }, + msg: &vaulttypes.MsgRetrieveFromVault{ + Authority: constants.BobAccAddress.String(), + VaultId: constants.Vault_Clob1, + QuoteQuantums: dtypes.NewInt(3_500_000), + }, + signer: constants.BobAccAddress.String(), }, - "Success - Operator Authority, Retrieve all from Vault Clob 1": { + "Failure - Operator Authority, Retrieve more than max uint64 quantums from Vault Clob 1": { + operator: constants.AliceAccAddress.String(), + mainVaultQuantums: 0, + subVaultQuantums: 3_500_000, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_CLOSE_ONLY, + }, + msg: &vaulttypes.MsgRetrieveFromVault{ + Authority: constants.AliceAccAddress.String(), + VaultId: constants.Vault_Clob1, + QuoteQuantums: dtypes.NewIntFromBigInt( + new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + ), + }, + signer: constants.AliceAccAddress.String(), + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), + checkTxFails: true, + }, + "Failure - Operator Authority, Retrieve zero quantums from Vault Clob 1": { operator: constants.AliceAccAddress.String(), mainVaultQuantums: 0, subVaultQuantums: 3_500_000, @@ -56,8 +101,27 @@ func TestMsgRetrieveFromVault(t *testing.T) { msg: &vaulttypes.MsgRetrieveFromVault{ Authority: constants.AliceAccAddress.String(), VaultId: constants.Vault_Clob1, - QuoteQuantums: dtypes.NewInt(3_500_000), + QuoteQuantums: dtypes.NewInt(0), }, + signer: constants.AliceAccAddress.String(), + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), + checkTxFails: true, + }, + "Failure - Operator Authority, Retrieve negative quantums from Vault Clob 1": { + operator: constants.AliceAccAddress.String(), + mainVaultQuantums: 0, + subVaultQuantums: 3_500_000, + vaultParams: &vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_CLOSE_ONLY, + }, + msg: &vaulttypes.MsgRetrieveFromVault{ + Authority: constants.AliceAccAddress.String(), + VaultId: constants.Vault_Clob1, + QuoteQuantums: dtypes.NewInt(-1), + }, + signer: constants.AliceAccAddress.String(), + checkTxResponseContains: vaulttypes.ErrInvalidQuoteQuantums.Error(), + checkTxFails: true, }, "Failure - Insufficient quantums to retrieve from Vault Clob 0 with no open position": { operator: constants.AliceAccAddress.String(), @@ -71,7 +135,9 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob0, QuoteQuantums: dtypes.NewInt(27), }, - expectedErr: "failed to apply subaccount updates", + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Success - Retrieval from vault with open position exactly meets initial margin requirement": { operator: constants.AliceAccAddress.String(), @@ -90,6 +156,7 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(1_925_000), }, + signer: constants.AliceAccAddress.String(), }, "Failure - Retrieval from vault with open position would result in undercollateralization": { operator: constants.AliceAccAddress.String(), @@ -108,7 +175,9 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(1_925_001), }, - expectedErr: satypes.NewlyUndercollateralized.String(), + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Retrieve from non-existent vault": { operator: constants.AliceAccAddress.String(), @@ -119,7 +188,9 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob0, QuoteQuantums: dtypes.NewInt(10), }, - expectedErr: vaulttypes.ErrVaultParamsNotFound.Error(), + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Invalid Authority": { operator: constants.BobAccAddress.String(), @@ -130,7 +201,9 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(10), }, - expectedErr: vaulttypes.ErrInvalidAuthority.Error(), + signer: constants.AliceAccAddress.String(), + checkTxFails: false, + deliverTxFails: true, }, "Failure - Empty Authority": { operator: constants.BobAccAddress.String(), @@ -141,7 +214,9 @@ func TestMsgRetrieveFromVault(t *testing.T) { VaultId: constants.Vault_Clob1, QuoteQuantums: dtypes.NewInt(10), }, - expectedErr: vaulttypes.ErrInvalidAuthority.Error(), + signer: constants.BobAccAddress.String(), + checkTxResponseContains: vaulttypes.ErrInvalidAuthority.Error(), + checkTxFails: true, }, } @@ -201,20 +276,61 @@ func TestMsgRetrieveFromVault(t *testing.T) { return genesis }).Build() ctx := tApp.InitChain() - k := tApp.App.VaultKeeper - ms := keeper.NewMsgServerImpl(k) - // Retrieve from vault. - _, err := ms.RetrieveFromVault(ctx, tc.msg) + // Invoke CheckTx. + CheckTx_MsgRetrieveFromVault := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: tc.signer, + Gas: constants.TestGasLimit, + FeeAmt: constants.TestFeeCoins_5Cents, + }, + tc.msg, + ) + checkTxResp := tApp.CheckTx(CheckTx_MsgRetrieveFromVault) + + // Check that CheckTx response log contains expected string, if any. + if tc.checkTxResponseContains != "" { + require.Contains(t, checkTxResp.Log, tc.checkTxResponseContains) + } + // Check that CheckTx succeeds or errors out as expected. + if tc.checkTxFails { + require.Conditionf(t, checkTxResp.IsErr, "Expected CheckTx to error. Response: %+v", checkTxResp) + return + } + require.Conditionf(t, checkTxResp.IsOK, "Expected CheckTx to succeed. Response: %+v", checkTxResp) + + // Advance to next block (and check that DeliverTx is as expected). + nextBlock := uint32(ctx.BlockHeight()) + 1 + if tc.deliverTxFails { + // Check that DeliverTx fails on `msgDepositToMegavault`. + ctx = tApp.AdvanceToBlock(nextBlock, testapp.AdvanceToBlockOptions{ + ValidateFinalizeBlock: func( + context sdktypes.Context, + request abcitypes.RequestFinalizeBlock, + response abcitypes.ResponseFinalizeBlock, + ) (haltChain bool) { + for i, tx := range request.Txs { + if bytes.Equal(tx, CheckTx_MsgRetrieveFromVault.Tx) { + require.True(t, response.TxResults[i].IsErr()) + } else { + require.True(t, response.TxResults[i].IsOK()) + } + } + return false + }, + }) + } else { + ctx = tApp.AdvanceToBlock(nextBlock, testapp.AdvanceToBlockOptions{}) + } // Check expectations. mainVault := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, vaulttypes.MegavaultMainSubaccount) subVault := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, *tc.msg.VaultId.ToSubaccountId()) require.Len(t, mainVault.AssetPositions, 1) - if tc.expectedErr != "" { - require.ErrorContains(t, err, tc.expectedErr) - + if tc.deliverTxFails { // Verify that main vault and sub vault balances are unchanged. require.Len(t, subVault.AssetPositions, 1) require.Equal( @@ -228,8 +344,6 @@ func TestMsgRetrieveFromVault(t *testing.T) { subVault.AssetPositions[0].Quantums.BigInt().Uint64(), ) } else { - require.NoError(t, err) - // Verify that main vault and sub vault balances are updated. expectedSubVaultQuantums := tc.subVaultQuantums - tc.msg.QuoteQuantums.BigInt().Uint64() if expectedSubVaultQuantums == 0 { 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 1470c4ff1d..6f9ae542ae 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 @@ -2,6 +2,7 @@ package keeper_test import ( "bytes" + "math" "math/big" "testing" @@ -36,7 +37,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { tests := map[string]struct { /* --- Setup --- */ // Quote quantums that main vault has. - mainVaultBalance uint64 + mainVaultBalance *big.Int // Total shares before withdrawal. totalShares uint64 // Owner address. @@ -53,6 +54,10 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { minQuoteQuantums int64 /* --- Expectations --- */ + // A string that CheckTx response contains, if any. + checkTxResponseContains string + // Whether CheckTx should fail. + checkTxFails bool // Whether DeliverTx should fail. deliverTxFails bool // Quote quantums that should be redeemed. @@ -63,7 +68,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedOwnerShares uint64 }{ "Success: Withdraw some unlocked shares (5% of total), No sub-vaults, Redeemed quantums = Min quantums": { - mainVaultBalance: 100, + mainVaultBalance: big.NewInt(100), totalShares: 200, owner: constants.AliceAccAddress.String(), ownerTotalShares: 50, @@ -76,7 +81,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedOwnerShares: 40, // 50 - 10 }, "Success: Withdraw all unlocked shares (8% of total), No sub-vaults, Redeemed quantums > Min quantums": { - mainVaultBalance: 1_234, + mainVaultBalance: big.NewInt(1_234), totalShares: 500, owner: constants.BobAccAddress.String(), ownerTotalShares: 47, @@ -89,7 +94,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedOwnerShares: 7, // 47 - 40 }, "Success: Withdraw all shares (100% of total), No sub-vaults, Redeemed quantums = Min quantums": { - mainVaultBalance: 654_321, + mainVaultBalance: big.NewInt(654_321), totalShares: 787_565, owner: constants.CarlAccAddress.String(), ownerTotalShares: 787_565, @@ -101,21 +106,21 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedTotalShares: 0, expectedOwnerShares: 0, }, - "Success: Withdraw some unlocked shares (1% of total), No sub-vaults, Redeemed quantums rounds down to 0": { - mainVaultBalance: 99, + "Failure: Withdraw some unlocked shares (1% of total), No sub-vaults, Redeemed quantums rounds down to 0": { + mainVaultBalance: big.NewInt(99), totalShares: 200, owner: constants.AliceAccAddress.String(), ownerTotalShares: 10, ownerLockedShares: 5, sharesToWithdraw: 2, - minQuoteQuantums: -1, + minQuoteQuantums: 0, deliverTxFails: true, redeemedQuoteQuantums: 0, // 99 * 2 / 200 = 0.99 ~= 0 (rounded down) expectedTotalShares: 200, // unchanged expectedOwnerShares: 10, // unchanged }, "Failure: Withdraw more than locked shares": { - mainVaultBalance: 100, + mainVaultBalance: big.NewInt(100), totalShares: 500, owner: constants.AliceAccAddress.String(), ownerTotalShares: 100, @@ -127,31 +132,33 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedOwnerShares: 100, // unchanged }, "Failure: Withdraw zero shares": { - mainVaultBalance: 100, - totalShares: 500, - owner: constants.AliceAccAddress.String(), - ownerTotalShares: 100, - ownerLockedShares: 20, - sharesToWithdraw: 0, - minQuoteQuantums: 1, - deliverTxFails: true, - expectedTotalShares: 500, // unchanged - expectedOwnerShares: 100, // unchanged + mainVaultBalance: big.NewInt(100), + totalShares: 500, + owner: constants.AliceAccAddress.String(), + ownerTotalShares: 100, + ownerLockedShares: 20, + sharesToWithdraw: 0, + minQuoteQuantums: 1, + checkTxResponseContains: vaulttypes.ErrNonPositiveShares.Error(), + checkTxFails: true, + expectedTotalShares: 500, // unchanged + expectedOwnerShares: 100, // unchanged }, "Failure: Withdraw negative shares": { - mainVaultBalance: 100, - totalShares: 500, - owner: constants.AliceAccAddress.String(), - ownerTotalShares: 100, - ownerLockedShares: 20, - sharesToWithdraw: -1, - minQuoteQuantums: 1, - deliverTxFails: true, - expectedTotalShares: 500, // unchanged - expectedOwnerShares: 100, // unchanged + mainVaultBalance: big.NewInt(100), + totalShares: 500, + owner: constants.AliceAccAddress.String(), + ownerTotalShares: 100, + ownerLockedShares: 20, + sharesToWithdraw: -1, + minQuoteQuantums: 1, + checkTxResponseContains: vaulttypes.ErrNonPositiveShares.Error(), + checkTxFails: true, + expectedTotalShares: 500, // unchanged + expectedOwnerShares: 100, // unchanged }, "Failure: Withdraw some unlocked shares (8% of total), one sub-vault, Redeemed quantums < Min quantums": { - mainVaultBalance: 1_234, + mainVaultBalance: big.NewInt(1_234), totalShares: 500, owner: constants.BobAccAddress.String(), ownerTotalShares: 47, @@ -180,7 +187,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { }, "Success: Withdraw some unlocked shares (0.4444% of total), 888_888 quantums in main vault, " + "one quoting sub-vault with negative equity": { - mainVaultBalance: 888_888, + mainVaultBalance: big.NewInt(888_888), totalShares: 1_000_000, owner: constants.AliceAccAddress.String(), ownerTotalShares: 9999, @@ -201,7 +208,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { }, }, sharesToWithdraw: 4444, - minQuoteQuantums: -1, + minQuoteQuantums: 123, deliverTxFails: false, redeemedQuoteQuantums: 3_950, // 888_888 * 4444 / 1_000_000 ~= 3950 (sub-vault is skipped) expectedTotalShares: 995_556, // 1_000_000 - 4444 @@ -209,7 +216,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { }, "Success: Withdraw some unlocked shares (~0.67% of total), 0 quantums in main vault, " + "one quoting sub-vault with 0 leverage": { - mainVaultBalance: 0, + mainVaultBalance: big.NewInt(0), totalShares: 987_654, owner: constants.AliceAccAddress.String(), ownerTotalShares: 9999, @@ -238,7 +245,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { }, "Success: Withdraw some unlocked shares (10% of total), 500 quantums in main vault, " + "one stand-by sub-vault with 0 leverage, one close-only sub-vault with 1.5 leverage": { - mainVaultBalance: 500, + mainVaultBalance: big.NewInt(500), totalShares: 1_000, owner: constants.AliceAccAddress.String(), ownerTotalShares: 120, @@ -295,7 +302,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { }, "Success: Withdraw all shares (100% of total), 500 quantums in main vault, " + "one close-only sub-vault with 1.5 leverage": { - mainVaultBalance: 500, + mainVaultBalance: big.NewInt(500), totalShares: 1_000, owner: constants.AliceAccAddress.String(), ownerTotalShares: 1_000, @@ -327,6 +334,22 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { expectedTotalShares: 0, expectedOwnerShares: 0, }, + "Failure: Withdraw more than max uint64": { + mainVaultBalance: new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + totalShares: 155, + owner: constants.CarlAccAddress.String(), + ownerTotalShares: 155, + ownerLockedShares: 0, + sharesToWithdraw: 155, + minQuoteQuantums: 1, + // fails as owner redeems more than max uint64 quote quantums. + deliverTxFails: true, + expectedTotalShares: 155, // unchanged + expectedOwnerShares: 155, // unchanged + }, } for name, tc := range tests { @@ -343,7 +366,7 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { AssetPositions: []*satypes.AssetPosition{ { AssetId: assetstypes.AssetUsdc.Id, - Quantums: dtypes.NewIntFromUint64(tc.mainVaultBalance), + Quantums: dtypes.NewIntFromBigInt(tc.mainVaultBalance), }, }, }, @@ -479,6 +502,15 @@ func TestMsgWithdrawFromMegavault(t *testing.T) { &msgWithdrawFromMegavault, ) checkTxResp := tApp.CheckTx(CheckTx_MsgWithdrawFromMegavault) + // Check that CheckTx response log contains expected string, if any. + if tc.checkTxResponseContains != "" { + require.Contains(t, checkTxResp.Log, tc.checkTxResponseContains) + } + // Check that CheckTx succeeds or errors out as expected. + if tc.checkTxFails { + require.Conditionf(t, checkTxResp.IsErr, "Expected CheckTx to error. Response: %+v", checkTxResp) + return + } require.Conditionf(t, checkTxResp.IsOK, "Expected CheckTx to succeed. Response: %+v", checkTxResp) // Advance to next block (and check that DeliverTx is as expected). diff --git a/protocol/x/vault/keeper/withdraw.go b/protocol/x/vault/keeper/withdraw.go index 3ac55b56c3..7db9cff6df 100644 --- a/protocol/x/vault/keeper/withdraw.go +++ b/protocol/x/vault/keeper/withdraw.go @@ -11,6 +11,7 @@ import ( assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + sendingtypes "github.com/dydxprotocol/v4-chain/protocol/x/sending/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -225,12 +226,25 @@ func (k Keeper) WithdrawFromMegavault( redeemedFromSubVault.Mul(redeemedFromSubVault, new(big.Rat).Sub(lib.BigRat1(), slippage)) quantumsToTransfer := new(big.Int).Quo(redeemedFromSubVault.Num(), redeemedFromSubVault.Denom()) - err = k.subaccountsKeeper.TransferFundsFromSubaccountToSubaccount( + if quantumsToTransfer.Sign() <= 0 || !quantumsToTransfer.IsUint64() { + log.InfoLog( + ctx, + "Megavault withdrawal: quantums to transfer is invalid. Skipping this vault", + "Vault ID", + vaultId, + "Quantums", + quantumsToTransfer, + ) + continue + } + err = k.sendingKeeper.ProcessTransfer( ctx, - *vaultId.ToSubaccountId(), - types.MegavaultMainSubaccount, - assetstypes.AssetUsdc.Id, - quantumsToTransfer, + &sendingtypes.Transfer{ + Sender: *vaultId.ToSubaccountId(), + Recipient: types.MegavaultMainSubaccount, + AssetId: assetstypes.AssetUsdc.Id, + Amount: quantumsToTransfer.Uint64(), // validated above. + }, ) if err != nil { log.ErrorLogWithError( @@ -250,8 +264,9 @@ func (k Keeper) WithdrawFromMegavault( megavaultEquity.Add(megavaultEquity, equity) } - // 4. Return error if redeemed quantums are non-positive or less than min quantums. - if redeemedQuoteQuantums.Sign() <= 0 || redeemedQuoteQuantums.Cmp(minQuoteQuantums) < 0 { + // 4. Return error if redeemed quantums is invalid. + if redeemedQuoteQuantums.Sign() <= 0 || !redeemedQuoteQuantums.IsUint64() || + redeemedQuoteQuantums.Cmp(minQuoteQuantums) < 0 { return nil, errorsmod.Wrapf( types.ErrInsufficientRedeemedQuoteQuantums, "redeemed quote quantums: %s, min quote quantums: %s", @@ -261,12 +276,14 @@ func (k Keeper) WithdrawFromMegavault( } // 5. Transfer from main vault to destination subaccount. - err = k.subaccountsKeeper.TransferFundsFromSubaccountToSubaccount( + err = k.sendingKeeper.ProcessTransfer( ctx, - types.MegavaultMainSubaccount, - toSubaccount, - assetstypes.AssetUsdc.Id, - redeemedQuoteQuantums, + &sendingtypes.Transfer{ + Sender: types.MegavaultMainSubaccount, + Recipient: toSubaccount, + AssetId: assetstypes.AssetUsdc.Id, + Amount: redeemedQuoteQuantums.Uint64(), // validated above. + }, ) if err != nil { log.ErrorLogWithError( diff --git a/protocol/x/vault/types/errors.go b/protocol/x/vault/types/errors.go index b7972d6ad2..4b4d541dec 100644 --- a/protocol/x/vault/types/errors.go +++ b/protocol/x/vault/types/errors.go @@ -20,10 +20,10 @@ var ( 3, "MarketParam not found", ) - ErrInvalidDepositAmount = errorsmod.Register( + ErrInvalidQuoteQuantums = errorsmod.Register( ModuleName, 4, - "Deposit amount is invalid", + "QuoteQuantums must be positive and less than 2^64", ) ErrNonPositiveEquity = errorsmod.Register( ModuleName, @@ -150,4 +150,9 @@ var ( 29, "Cannot deactivate vaults with positive equity", ) + ErrNonPositiveShares = errorsmod.Register( + ModuleName, + 30, + "Shares must be positive", + ) ) diff --git a/protocol/x/vault/types/expected_keepers.go b/protocol/x/vault/types/expected_keepers.go index 643d8b012e..85c1138716 100644 --- a/protocol/x/vault/types/expected_keepers.go +++ b/protocol/x/vault/types/expected_keepers.go @@ -111,11 +111,4 @@ type SubaccountsKeeper interface { ctx sdk.Context, id satypes.SubaccountId, ) satypes.Subaccount - TransferFundsFromSubaccountToSubaccount( - ctx sdk.Context, - senderSubaccountId satypes.SubaccountId, - recipientSubaccountId satypes.SubaccountId, - assetId uint32, - quantums *big.Int, - ) error } diff --git a/protocol/x/vault/types/msg_allocate_to_vault.go b/protocol/x/vault/types/msg_allocate_to_vault.go new file mode 100644 index 0000000000..b1ab5c7b1c --- /dev/null +++ b/protocol/x/vault/types/msg_allocate_to_vault.go @@ -0,0 +1,22 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/types" +) + +var _ types.Msg = &MsgAllocateToVault{} + +// ValidateBasic performs stateless validation on a MsgAllocateToVault. +func (msg *MsgAllocateToVault) ValidateBasic() error { + if msg.Authority == "" { + return ErrInvalidAuthority + } + + // Validate that quote quantums is positive and an uint64. + quoteQuantums := msg.QuoteQuantums.BigInt() + if quoteQuantums.Sign() <= 0 || !quoteQuantums.IsUint64() { + return ErrInvalidQuoteQuantums + } + + return nil +} diff --git a/protocol/x/vault/types/msg_allocate_to_vault_test.go b/protocol/x/vault/types/msg_allocate_to_vault_test.go new file mode 100644 index 0000000000..89d3af9386 --- /dev/null +++ b/protocol/x/vault/types/msg_allocate_to_vault_test.go @@ -0,0 +1,82 @@ +package types_test + +import ( + "math" + "math/big" + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" + "github.com/stretchr/testify/require" +) + +func TestMsgAllocateToVault_ValidateBasic(t *testing.T) { + tests := map[string]struct { + msg types.MsgAllocateToVault + expectedErr error + }{ + "Success": { + msg: types.MsgAllocateToVault{ + Authority: constants.AliceAccAddress.String(), + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(1), + }, + }, + "Success: max uint64 quote quantums": { + msg: types.MsgAllocateToVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewIntFromUint64(math.MaxUint64), + }, + }, + "Failure: quote quantums greater than max uint64": { + msg: types.MsgAllocateToVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewIntFromBigInt( + new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + ), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: zero quote quantums": { + msg: types.MsgAllocateToVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: negative quote quantums": { + msg: types.MsgAllocateToVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(-1), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: empty authority": { + msg: types.MsgAllocateToVault{ + Authority: "", + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrInvalidAuthority, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expectedErr) + } + }) + } +} diff --git a/protocol/x/vault/types/msg_deposit_to_megavault.go b/protocol/x/vault/types/msg_deposit_to_megavault.go index e16673ef55..d285b8f78b 100644 --- a/protocol/x/vault/types/msg_deposit_to_megavault.go +++ b/protocol/x/vault/types/msg_deposit_to_megavault.go @@ -21,7 +21,10 @@ func (msg *MsgDepositToMegavault) ValidateBasic() error { // Validate that quote quantums is positive and an uint64. quoteQuantums := msg.QuoteQuantums.BigInt() if quoteQuantums.Sign() <= 0 || !quoteQuantums.IsUint64() { - return errors.Wrap(ErrInvalidDepositAmount, "quote quantums must be strictly positive and less than 2^64") + return errors.Wrap( + ErrInvalidQuoteQuantums, + "quote quantums must be strictly positive and less than 2^64", + ) } return nil diff --git a/protocol/x/vault/types/msg_deposit_to_megavault_test.go b/protocol/x/vault/types/msg_deposit_to_megavault_test.go index 8373f3f686..5d983bea3a 100644 --- a/protocol/x/vault/types/msg_deposit_to_megavault_test.go +++ b/protocol/x/vault/types/msg_deposit_to_megavault_test.go @@ -39,21 +39,21 @@ func TestMsgDepositToMegavault_ValidateBasic(t *testing.T) { ), ), }, - expectedErr: "Deposit amount is invalid", + expectedErr: types.ErrInvalidQuoteQuantums.Error(), }, "Failure: zero quote quantums": { msg: types.MsgDepositToMegavault{ SubaccountId: &constants.Alice_Num0, QuoteQuantums: dtypes.NewInt(0), }, - expectedErr: "Deposit amount is invalid", + expectedErr: types.ErrInvalidQuoteQuantums.Error(), }, "Failure: negative quote quantums": { msg: types.MsgDepositToMegavault{ SubaccountId: &constants.Alice_Num0, QuoteQuantums: dtypes.NewInt(-1), }, - expectedErr: "Deposit amount is invalid", + expectedErr: types.ErrInvalidQuoteQuantums.Error(), }, "Failure: invalid subaccount owner": { msg: types.MsgDepositToMegavault{ diff --git a/protocol/x/vault/types/msg_retrieve_from_vault.go b/protocol/x/vault/types/msg_retrieve_from_vault.go new file mode 100644 index 0000000000..da8241e627 --- /dev/null +++ b/protocol/x/vault/types/msg_retrieve_from_vault.go @@ -0,0 +1,22 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/types" +) + +var _ types.Msg = &MsgRetrieveFromVault{} + +// ValidateBasic performs stateless validation on a MsgRetrieveFromVault. +func (msg *MsgRetrieveFromVault) ValidateBasic() error { + if msg.Authority == "" { + return ErrInvalidAuthority + } + + // Validate that quote quantums is positive and an uint64. + quoteQuantums := msg.QuoteQuantums.BigInt() + if quoteQuantums.Sign() <= 0 || !quoteQuantums.IsUint64() { + return ErrInvalidQuoteQuantums + } + + return nil +} diff --git a/protocol/x/vault/types/msg_retrieve_from_vault_test.go b/protocol/x/vault/types/msg_retrieve_from_vault_test.go new file mode 100644 index 0000000000..ed021d5fc3 --- /dev/null +++ b/protocol/x/vault/types/msg_retrieve_from_vault_test.go @@ -0,0 +1,82 @@ +package types_test + +import ( + "math" + "math/big" + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" + "github.com/stretchr/testify/require" +) + +func TestMsgRetrieveFromVault_ValidateBasic(t *testing.T) { + tests := map[string]struct { + msg types.MsgRetrieveFromVault + expectedErr error + }{ + "Success": { + msg: types.MsgRetrieveFromVault{ + Authority: constants.AliceAccAddress.String(), + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(1), + }, + }, + "Success: max uint64 quote quantums": { + msg: types.MsgRetrieveFromVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewIntFromUint64(math.MaxUint64), + }, + }, + "Failure: quote quantums greater than max uint64": { + msg: types.MsgRetrieveFromVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewIntFromBigInt( + new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + ), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: zero quote quantums": { + msg: types.MsgRetrieveFromVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: negative quote quantums": { + msg: types.MsgRetrieveFromVault{ + Authority: constants.GovAuthority, + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(-1), + }, + expectedErr: types.ErrInvalidQuoteQuantums, + }, + "Failure: empty authority": { + msg: types.MsgRetrieveFromVault{ + Authority: "", + VaultId: constants.Vault_Clob0, + QuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrInvalidAuthority, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expectedErr) + } + }) + } +} diff --git a/protocol/x/vault/types/msg_withdraw_from_megavault.go b/protocol/x/vault/types/msg_withdraw_from_megavault.go new file mode 100644 index 0000000000..eec71b002b --- /dev/null +++ b/protocol/x/vault/types/msg_withdraw_from_megavault.go @@ -0,0 +1,32 @@ +package types + +import ( + "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/types" +) + +var _ types.Msg = &MsgWithdrawFromMegavault{} + +// ValidateBasic performs stateless validation on a MsgWithdrawFromMegavault. +func (msg *MsgWithdrawFromMegavault) ValidateBasic() error { + // Validate subaccount to withdraw to. + if err := msg.SubaccountId.Validate(); err != nil { + return err + } + + // Validate that shares is positive. + if msg.Shares.NumShares.Sign() <= 0 { + return ErrNonPositiveShares + } + + // Validate that min quote quantums is non-negative and an uint64. + quoteQuantums := msg.MinQuoteQuantums.BigInt() + if quoteQuantums.Sign() < 0 || !quoteQuantums.IsUint64() { + return errors.Wrap( + ErrInvalidQuoteQuantums, + "min quote quantums must be non-negative and less than 2^64", + ) + } + + return nil +} diff --git a/protocol/x/vault/types/msg_withdraw_from_megavault_test.go b/protocol/x/vault/types/msg_withdraw_from_megavault_test.go new file mode 100644 index 0000000000..696831dfc8 --- /dev/null +++ b/protocol/x/vault/types/msg_withdraw_from_megavault_test.go @@ -0,0 +1,117 @@ +package types_test + +import ( + "math" + "math/big" + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + "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/types" + "github.com/stretchr/testify/require" +) + +func TestMsgWithdrawFromMegavault_ValidateBasic(t *testing.T) { + tests := map[string]struct { + msg types.MsgWithdrawFromMegavault + expectedErr string + }{ + "Success": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewInt(1), + }, + }, + "Success: zero quote quantums": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewInt(0), + }, + }, + "Success: max uint64 quote quantums": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewIntFromUint64(math.MaxUint64), + }, + }, + "Failure: quote quantums greater than max uint64": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewIntFromBigInt( + new(big.Int).Add( + new(big.Int).SetUint64(math.MaxUint64), + new(big.Int).SetUint64(1), + ), + ), + }, + expectedErr: types.ErrInvalidQuoteQuantums.Error(), + }, + "Failure: negative quote quantums": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewInt(-1), + }, + expectedErr: types.ErrInvalidQuoteQuantums.Error(), + }, + "Failure: zero shares": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(0), + }, + MinQuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrNonPositiveShares.Error(), + }, + "Failure: negative shares": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: constants.Alice_Num0, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(-1), + }, + MinQuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: types.ErrNonPositiveShares.Error(), + }, + "Failure: invalid subaccount owner": { + msg: types.MsgWithdrawFromMegavault{ + SubaccountId: satypes.SubaccountId{ + Owner: "invalid-owner", + Number: 0, + }, + Shares: types.NumShares{ + NumShares: dtypes.NewInt(1), + }, + MinQuoteQuantums: dtypes.NewInt(0), + }, + expectedErr: "subaccount id owner is an invalid address", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.expectedErr) + } + }) + } +}