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