@@ -10,6 +10,7 @@ import (
1010 "github.com/dydxprotocol/v4-chain/protocol/testutil/constants"
1111 assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types"
1212 clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types"
13+ sendingtypes "github.com/dydxprotocol/v4-chain/protocol/x/sending/types"
1314 satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
1415 "github.com/stretchr/testify/require"
1516)
@@ -264,3 +265,279 @@ func TestOrderPlacementFailsWithLeverageConfigured(t *testing.T) {
264265 require .False (t , resp .IsOK (), "Expected Alice's CheckTx to fail due to leverage. Response: %+v" , resp )
265266 }
266267}
268+
269+ func TestWithdrawalWithLeverage (t * testing.T ) {
270+ tApp := testapp .NewTestAppBuilder (t ).Build ()
271+ ctx := tApp .InitChain ()
272+
273+ // Use CheckTx to set leverage (this goes through ante handler which persists it)
274+ leverageMsg := & clobtypes.MsgUpdateLeverage {
275+ SubaccountId : & constants .Alice_Num0 ,
276+ ClobPairLeverage : []* clobtypes.LeverageEntry {
277+ {
278+ ClobPairId : 0 ,
279+ Leverage : 5 ,
280+ },
281+ },
282+ }
283+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (ctx , tApp .App , * leverageMsg ) {
284+ resp := tApp .CheckTx (checkTx )
285+ require .True (t , resp .IsOK (), "Expected leverage CheckTx to succeed. Response: %+v" , resp )
286+ }
287+
288+ ctx = tApp .AdvanceToBlock (2 , testapp.AdvanceToBlockOptions {})
289+
290+ // Verify leverage was set immediately (ante handler sets it)
291+ aliceLeverageMap , found := tApp .App .SubaccountsKeeper .GetLeverage (ctx , & constants .Alice_Num0 )
292+ require .True (t , found , "Alice's leverage should be set" )
293+ require .Equal (t , uint32 (5 ), aliceLeverageMap [0 ], "Alice's leverage for perpetual 0 should be 5" )
294+
295+ // Use orders large enough to have meaningful margin requirements
296+ // Both Alice and Bob trade 1,000,000 quantums (0.0001 BTC) at price 5,000,000,000 subticks ($50,000/BTC)
297+ // This creates a $5 notional position which will have measurable IMR
298+ // Quantums must be multiple of StepBaseQuantums = 10
299+ // Subticks must be multiple of SubticksPerTick = 10000
300+ aliceOrder := clobtypes.Order {
301+ OrderId : clobtypes.OrderId {
302+ SubaccountId : constants .Alice_Num0 ,
303+ ClientId : 0 ,
304+ OrderFlags : clobtypes .OrderIdFlags_ShortTerm ,
305+ ClobPairId : 0 ,
306+ },
307+ Side : clobtypes .Order_SIDE_BUY ,
308+ Quantums : 1_000_000 , // 0.0001 BTC
309+ Subticks : 5_000_000_000 , // $50,000 per BTC
310+ GoodTilOneof : & clobtypes.Order_GoodTilBlock {
311+ GoodTilBlock : 20 ,
312+ },
313+ }
314+
315+ bobOrder := clobtypes.Order {
316+ OrderId : clobtypes.OrderId {
317+ SubaccountId : constants .Bob_Num0 ,
318+ ClientId : 0 ,
319+ OrderFlags : clobtypes .OrderIdFlags_ShortTerm ,
320+ ClobPairId : 0 ,
321+ },
322+ Side : clobtypes .Order_SIDE_BUY ,
323+ Quantums : 1_000_000 , // 0.0001 BTC
324+ Subticks : 5_000_000_000 , // $50,000 per BTC
325+ GoodTilOneof : & clobtypes.Order_GoodTilBlock {
326+ GoodTilBlock : 20 ,
327+ },
328+ }
329+
330+ carlSellOrder := clobtypes.Order {
331+ OrderId : clobtypes.OrderId {
332+ SubaccountId : constants .Carl_Num0 ,
333+ ClientId : 0 ,
334+ OrderFlags : clobtypes .OrderIdFlags_ShortTerm ,
335+ ClobPairId : 0 ,
336+ },
337+ Side : clobtypes .Order_SIDE_SELL ,
338+ Quantums : 2_000_000 , // 0.0002 BTC
339+ Subticks : 5_000_000_000 , // $50,000 per BTC
340+ GoodTilOneof : & clobtypes.Order_GoodTilBlock {
341+ GoodTilBlock : 20 ,
342+ },
343+ }
344+
345+ // Place all orders
346+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
347+ ctx ,
348+ tApp .App ,
349+ * clobtypes .NewMsgPlaceOrder (carlSellOrder ),
350+ ) {
351+ resp := tApp .CheckTx (checkTx )
352+ require .True (t , resp .IsOK (), "Expected Carl's order to succeed. Response: %+v" , resp )
353+ }
354+
355+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
356+ ctx ,
357+ tApp .App ,
358+ * clobtypes .NewMsgPlaceOrder (bobOrder ),
359+ ) {
360+ resp := tApp .CheckTx (checkTx )
361+ require .True (t , resp .IsOK (), "Expected Bob's order to succeed. Response: %+v" , resp )
362+ }
363+
364+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
365+ ctx ,
366+ tApp .App ,
367+ * clobtypes .NewMsgPlaceOrder (aliceOrder ),
368+ ) {
369+ resp := tApp .CheckTx (checkTx )
370+ require .True (t , resp .IsOK (), "Expected Alice's order to succeed. Response: %+v" , resp )
371+ }
372+
373+ // Advance to next block which matches the orders
374+ ctx = tApp .AdvanceToBlock (3 , testapp.AdvanceToBlockOptions {})
375+
376+ // Get final subaccount states
377+ aliceAfter := tApp .App .SubaccountsKeeper .GetSubaccount (ctx , constants .Alice_Num0 )
378+ bobAfter := tApp .App .SubaccountsKeeper .GetSubaccount (ctx , constants .Bob_Num0 )
379+
380+ // Both should have positions now
381+ require .Len (t , aliceAfter .PerpetualPositions , 1 , "Alice should have 1 perpetual position" )
382+ require .Len (t , bobAfter .PerpetualPositions , 1 , "Bob should have 1 perpetual position" )
383+
384+ // Verify the positions are equal in size but opposite in direction
385+ require .Equal (t , uint64 (1_000_000 ), new (big.Int ).Abs (aliceAfter .PerpetualPositions [0 ].GetBigQuantums ()).Uint64 ())
386+ require .Equal (t , uint64 (1_000_000 ), new (big.Int ).Abs (bobAfter .PerpetualPositions [0 ].GetBigQuantums ()).Uint64 ())
387+
388+ // Get actual risk calculations to verify leverage is being applied
389+ aliceRisk , err := tApp .App .SubaccountsKeeper .GetNetCollateralAndMarginRequirements (
390+ ctx ,
391+ satypes.Update {SubaccountId : constants .Alice_Num0 },
392+ )
393+ require .NoError (t , err )
394+
395+ bobRisk , err := tApp .App .SubaccountsKeeper .GetNetCollateralAndMarginRequirements (
396+ ctx ,
397+ satypes.Update {SubaccountId : constants .Bob_Num0 },
398+ )
399+ require .NoError (t , err )
400+
401+ // Alice's IMR should be higher due to lower leverage (more conservative)
402+ require .True (t , aliceRisk .IMR .Cmp (bobRisk .IMR ) > 0 ,
403+ "Alice's IMR (%s) should be higher than Bob's IMR (%s) due to lower leverage setting (5x vs 20x)" ,
404+ aliceRisk .IMR .String (), bobRisk .IMR .String ())
405+
406+ // Calculate available collateral for new positions: NC - IMR
407+ aliceAvailable := new (big.Int ).Sub (aliceRisk .NC , aliceRisk .IMR )
408+ bobAvailable := new (big.Int ).Sub (bobRisk .NC , bobRisk .IMR )
409+
410+ // Verify Bob has more available collateral than Alice due to lower leverage requirements
411+ require .True (t , bobAvailable .Cmp (aliceAvailable ) > 0 ,
412+ "Bob's available collateral (%s) should be greater than Alice's (%s) due to lower IMR from higher leverage" ,
413+ bobAvailable .String (), aliceAvailable .String ())
414+
415+ // Let's try to withdraw bob's available collateral from both subaccounts
416+
417+ bobWithdrawal := & sendingtypes.MsgWithdrawFromSubaccount {
418+ Sender : constants .Bob_Num0 ,
419+ Recipient : constants .BobAccAddress .String (),
420+ AssetId : constants .Usdc .Id ,
421+ Quantums : bobAvailable .Uint64 (),
422+ }
423+
424+ for _ , checkTx := range testapp .MustMakeCheckTxsWithSdkMsg (
425+ ctx ,
426+ tApp .App ,
427+ testapp.MustMakeCheckTxOptions {
428+ AccAddressForSigning : constants .Bob_Num0 .Owner ,
429+ Gas : constants .TestGasLimit ,
430+ FeeAmt : constants .TestFeeCoins_5Cents ,
431+ },
432+ bobWithdrawal ,
433+ ) {
434+ resp := tApp .CheckTx (checkTx )
435+ require .True (t , resp .IsOK (), "Expected Bob's withdrawal to succeed. Response: %+v" , resp )
436+ }
437+
438+ aliceWithdrawal := & sendingtypes.MsgWithdrawFromSubaccount {
439+ Sender : constants .Alice_Num0 ,
440+ Recipient : constants .AliceAccAddress .String (),
441+ AssetId : constants .Usdc .Id ,
442+ Quantums : bobAvailable .Uint64 (), // using bob's available collateral which is greater than alice's
443+ }
444+
445+ for _ , checkTx := range testapp .MustMakeCheckTxsWithSdkMsg (
446+ ctx ,
447+ tApp .App ,
448+ testapp.MustMakeCheckTxOptions {
449+ AccAddressForSigning : constants .Alice_Num0 .Owner ,
450+ Gas : constants .TestGasLimit * 10 ,
451+ FeeAmt : constants .TestFeeCoins_5Cents ,
452+ },
453+ aliceWithdrawal ,
454+ ) {
455+ resp := tApp .CheckTx (checkTx )
456+ require .False (t , resp .IsOK (), "Expected Alice's withdrawal to fail. Response: %+v" , resp )
457+ }
458+ }
459+
460+ func TestUpdateLeverageWithExistingPosition (t * testing.T ) {
461+ tApp := testapp .NewTestAppBuilder (t ).Build ()
462+ ctx := tApp .InitChain ()
463+
464+ // Place orders for both Alice and Bob that would require the entire margin if leverage was unchanged
465+ orderSize := dtypes .NewIntFromBigInt (big .NewInt (5_500_000_000_000_000 ))
466+
467+ // Use the same price and clob pair as in the other test
468+ price := uint64 (2_000_000_000 )
469+
470+ // Bob's order should succeed
471+ bobOrder := & clobtypes.Order {
472+ OrderId : clobtypes.OrderId {
473+ SubaccountId : constants .Bob_Num0 ,
474+ ClientId : 0 ,
475+ OrderFlags : clobtypes .OrderIdFlags_LongTerm ,
476+ ClobPairId : 0 ,
477+ },
478+ Side : clobtypes .Order_SIDE_SELL ,
479+ Quantums : orderSize .BigInt ().Uint64 (),
480+ Subticks : price ,
481+ GoodTilOneof : & clobtypes.Order_GoodTilBlockTime {
482+ GoodTilBlockTime : uint32 (ctx .BlockTime ().Unix () + 100 ),
483+ },
484+ }
485+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
486+ ctx ,
487+ tApp .App ,
488+ * clobtypes .NewMsgPlaceOrder (* bobOrder ),
489+ ) {
490+ resp := tApp .CheckTx (checkTx )
491+ require .True (t , resp .IsOK (), "Expected Bob's CheckTx to succeed. Response: %+v" , resp )
492+ }
493+
494+ bobSubaccount := tApp .App .SubaccountsKeeper .GetSubaccount (ctx , constants .Bob_Num0 )
495+ require .True (t , bobSubaccount .AssetPositions != nil , "Bob should have a subaccount" )
496+
497+ // Alice's order should fail due to leverage config
498+ aliceOrder := & clobtypes.Order {
499+ OrderId : clobtypes.OrderId {
500+ SubaccountId : constants .Alice_Num0 ,
501+ ClientId : 0 ,
502+ OrderFlags : clobtypes .OrderIdFlags_LongTerm ,
503+ ClobPairId : 0 ,
504+ },
505+ Side : clobtypes .Order_SIDE_BUY ,
506+ Quantums : orderSize .BigInt ().Uint64 (),
507+ Subticks : price ,
508+ GoodTilOneof : & clobtypes.Order_GoodTilBlockTime {
509+ GoodTilBlockTime : uint32 (ctx .BlockTime ().Unix () + 100 ),
510+ },
511+ }
512+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
513+ ctx ,
514+ tApp .App ,
515+ * clobtypes .NewMsgPlaceOrder (* aliceOrder ),
516+ ) {
517+ resp := tApp .CheckTx (checkTx )
518+ require .True (t , resp .IsOK (), "Expected Alice's CheckTx to succeed. Response: %+v" , resp )
519+ }
520+
521+ // Advance to next block which matches the orders
522+ ctx = tApp .AdvanceToBlock (2 , testapp.AdvanceToBlockOptions {})
523+
524+ // Configure leverage for Alice with an existing bitcoin position
525+ // This should make the account fail against the IMR check
526+ aliceLeverage := & clobtypes.MsgUpdateLeverage {
527+ SubaccountId : & constants .Alice_Num0 ,
528+ ClobPairLeverage : []* clobtypes.LeverageEntry {
529+ {
530+ ClobPairId : 0 ,
531+ Leverage : 2 ,
532+ },
533+ },
534+ }
535+ for _ , checkTx := range testapp .MustMakeCheckTxsWithClobMsg (
536+ ctx ,
537+ tApp .App ,
538+ * aliceLeverage ,
539+ ) {
540+ resp := tApp .CheckTx (checkTx )
541+ require .False (t , resp .IsOK (), "Expected Alice's CheckTx to fail. Response: %+v" , resp )
542+ }
543+ }
0 commit comments