Skip to content

Commit

Permalink
[TRA-139] Add logic to transfer collateral when open/close isolated p…
Browse files Browse the repository at this point in the history
…erpetual position. (#1200)
  • Loading branch information
vincentwschau authored Mar 22, 2024
1 parent 1788384 commit 52a5783
Show file tree
Hide file tree
Showing 9 changed files with 905 additions and 30 deletions.
2 changes: 1 addition & 1 deletion protocol/testutil/constants/positions.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ var (
// Long position for arbitrary isolated market
PerpetualPosition_OneISOLong = satypes.PerpetualPosition{
PerpetualId: 3,
Quantums: dtypes.NewInt(100_000_000),
Quantums: dtypes.NewInt(1_000_000_000),
FundingIndex: dtypes.NewInt(0),
}
PerpetualPosition_OneISO2Long = satypes.PerpetualPosition{
Expand Down
231 changes: 224 additions & 7 deletions protocol/x/subaccounts/keeper/isolated_subaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package keeper

import (
"math"
"math/big"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dydxprotocol/v4-chain/protocol/lib"
assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types"
perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types"
"github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
)
Expand All @@ -22,7 +24,7 @@ import (
// caused a failure, if any.
func (k Keeper) checkIsolatedSubaccountConstraints(
ctx sdk.Context,
settledUpdates []settledUpdate,
settledUpdates []SettledUpdate,
perpetuals []perptypes.Perpetual,
) (
success bool,
Expand All @@ -31,11 +33,7 @@ func (k Keeper) checkIsolatedSubaccountConstraints(
) {
success = true
successPerUpdate = make([]types.UpdateResult, len(settledUpdates))
var perpIdToMarketType = make(map[uint32]perptypes.PerpetualMarketType)

for _, perpetual := range perpetuals {
perpIdToMarketType[perpetual.GetId()] = perpetual.Params.MarketType
}
perpIdToMarketType := getPerpIdToMarketTypeMap(perpetuals)

for i, u := range settledUpdates {
result, err := isValidIsolatedPerpetualUpdates(u, perpIdToMarketType)
Expand Down Expand Up @@ -63,7 +61,7 @@ func (k Keeper) checkIsolatedSubaccountConstraints(
// - a subaccount with no positions cannot be updated to have positions in multiple isolated
// perpetuals or a combination of isolated and non-isolated perpetuals
func isValidIsolatedPerpetualUpdates(
settledUpdate settledUpdate,
settledUpdate SettledUpdate,
perpIdToMarketType map[uint32]perptypes.PerpetualMarketType,
) (types.UpdateResult, error) {
// If there are no perpetual updates, then this update does not violate constraints for isolated
Expand Down Expand Up @@ -140,3 +138,222 @@ func isValidIsolatedPerpetualUpdates(

return types.Success, nil
}

// GetIsolatedPerpetualStateTransition computes whether an isolated perpetual position will be
// opened or closed for a subaccount.
// This function assumes that the subaccount is valid under isolated perpetual constraints.
// The input `settledUpdate` must have an updated subaccount (`settledUpdate.SettledSubaccount`),
// so all the updates must have been applied already to the subaccount.
func GetIsolatedPerpetualStateTransition(
settledUpdateWithUpdatedSubaccount SettledUpdate,
perpetuals []perptypes.Perpetual,
) (*types.IsolatedPerpetualPositionStateTransition, error) {
perpIdToMarketType := getPerpIdToMarketTypeMap(perpetuals)
// This subaccount needs to have had the updates in the `settledUpdate` already applied to it.
updatedSubaccount := settledUpdateWithUpdatedSubaccount.SettledSubaccount
// If there are no perpetual updates, then no perpetual position could have been opened or closed
// on the subaccount.
if len(settledUpdateWithUpdatedSubaccount.PerpetualUpdates) == 0 {
return nil, nil
}

// If there are more than 1 valid perpetual update, or more than 1 valid perpetual position on the
// subaccount, it is not isolated to an isolated perpetual, and so no isolated perpetual position
// could have been opened or closed.
if len(settledUpdateWithUpdatedSubaccount.PerpetualUpdates) > 1 ||
len(updatedSubaccount.PerpetualPositions) > 1 {
return nil, nil
}

// Now, from the above checks, we know there is only a single perpetual update and 0 or 1 perpetual
// positions.
perpetualUpdate := settledUpdateWithUpdatedSubaccount.PerpetualUpdates[0]
marketType, exists := perpIdToMarketType[perpetualUpdate.PerpetualId]
if !exists {
return nil, errorsmod.Wrap(
perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualUpdate.PerpetualId),
)
}
// If the perpetual update is not for an isolated perpetual, no isolated perpetual position is
// being opened or closed.
if marketType != perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED {
return nil, nil
}

// If the updated subaccount does not have any perpetual positions, then an isolated perpetual
// position must have been closed due to the perpetual update.
if len(updatedSubaccount.PerpetualPositions) == 0 {
return &types.IsolatedPerpetualPositionStateTransition{
SubaccountId: updatedSubaccount.Id,
PerpetualId: perpetualUpdate.PerpetualId,
QuoteQuantums: updatedSubaccount.GetUsdcPosition(),
Transition: types.Closed,
}, nil
}

// After the above checks, the subaccount must have only a single perpetual position, which is for
// the same isolated perpetual as the perpetual update.
perpetualPosition := updatedSubaccount.PerpetualPositions[0]
// If the size of the update and the position are the same, the perpetual update must have opened
// the position.
if perpetualUpdate.GetBigQuantums().Cmp(perpetualPosition.GetBigQuantums()) == 0 {
if len(settledUpdateWithUpdatedSubaccount.AssetUpdates) != 1 {
return nil, errorsmod.Wrapf(
types.ErrFailedToUpdateSubaccounts,
"Subaccount with id %v opened perpteual position with perpetual id %d with invalid number of"+
" changes to asset positions (%d), should only be 1 asset update",
updatedSubaccount.Id,
perpetualUpdate.PerpetualId,
len(settledUpdateWithUpdatedSubaccount.AssetUpdates),
)
}
if settledUpdateWithUpdatedSubaccount.AssetUpdates[0].AssetId != assettypes.AssetUsdc.Id {
return nil, errorsmod.Wrapf(
types.ErrFailedToUpdateSubaccounts,
"Subaccount with id %v opened perpteual position with perpetual id %d without a change to the"+
" quote currency's asset position.",
updatedSubaccount.Id,
perpetualUpdate.PerpetualId,
)
}
// Collateral equal to the quote currency asset position before the update was applied needs to be transferred.
// Subtract the delta from the updated subaccount's quote currency asset position size to get the size
// of the quote currency asset position.
quoteQuantumsBeforeUpdate := new(big.Int).Sub(
updatedSubaccount.GetUsdcPosition(),
settledUpdateWithUpdatedSubaccount.AssetUpdates[0].GetBigQuantums(),
)
return &types.IsolatedPerpetualPositionStateTransition{
SubaccountId: updatedSubaccount.Id,
PerpetualId: perpetualUpdate.PerpetualId,
QuoteQuantums: quoteQuantumsBeforeUpdate,
Transition: types.Opened,
}, nil
}

// The isolated perpetual position changed size but was not opened or closed.
return nil, nil
}

// transferCollateralForIsolatedPerpetual transfers collateral between an isolated collateral pool
// and the cross-perpetual collateral pool based on whether an isolated perpetual position was
// opened or closed in a subaccount.
// Note: This uses the `x/bank` keeper and modifies `x/bank` state.
func (k *Keeper) transferCollateralForIsolatedPerpetual(
ctx sdk.Context,
stateTransition *types.IsolatedPerpetualPositionStateTransition,
) error {
// No collateral to transfer if no state transition.
if stateTransition == nil {
return nil
}

// If there are zero quantums to transfer, don't transfer collateral.
if stateTransition.QuoteQuantums.Sign() == 0 {
return nil
}

isolatedCollateralPoolAddr, err := k.GetCollateralPoolFromPerpetualId(ctx, stateTransition.PerpetualId)
if err != nil {
return err
}
var toModuleAddr sdk.AccAddress
var fromModuleAddr sdk.AccAddress

// If an isolated perpetual position was opened in the subaccount, then move collateral from the
// cross-perpetual collateral pool to the isolated perpetual collateral pool.
if stateTransition.Transition == types.Opened {
toModuleAddr = isolatedCollateralPoolAddr
fromModuleAddr = types.ModuleAddress
// If the isolated perpetual position was closed, then move collateral from the isolated
// perpetual collateral pool to the cross-perpetual collateral pool.
} else if stateTransition.Transition == types.Closed {
toModuleAddr = types.ModuleAddress
fromModuleAddr = isolatedCollateralPoolAddr
} else {
// Should never hit this.
return errorsmod.Wrapf(
types.ErrFailedToUpdateSubaccounts,
"Invalid state transition %v for isolated perpetual with id %d in subaccount with id %v",
stateTransition,
stateTransition.PerpetualId,
stateTransition.SubaccountId,
)
}

// Invalid to transfer negative quantums. This should already be caught by collateralization
// checks as well.
if stateTransition.QuoteQuantums.Sign() == -1 {
return errorsmod.Wrapf(
types.ErrFailedToUpdateSubaccounts,
"Subaccount with id %v %s perpteual position with perpetual id %d with negative collateral %s to transfer",
stateTransition.SubaccountId,
stateTransition.Transition.String(),
stateTransition.PerpetualId,
stateTransition.QuoteQuantums.String(),
)
}

// Transfer collateral between collateral pools.
_, coinToTransfer, err := k.assetsKeeper.ConvertAssetToCoin(
ctx,
// TODO(DEC-715): Support non-USDC assets.
assettypes.AssetUsdc.Id,
stateTransition.QuoteQuantums,
)
if err != nil {
return err
}

if err = k.bankKeeper.SendCoins(
ctx,
fromModuleAddr,
toModuleAddr,
[]sdk.Coin{coinToTransfer},
); err != nil {
return err
}

return nil
}

// computeAndExecuteCollateralTransfer computes collateral transfers resulting from updates to
// a subaccount and executes the collateral transfer using `x/bank`.`
// The input `settledUpdate` must have an updated subaccount (`settledUpdate.SettledSubaccount`),
// so all the updates must have been applied already to the subaccount.
// Note: This uses the `x/bank` keeper and modifies `x/bank` state.
func (k *Keeper) computeAndExecuteCollateralTransfer(
ctx sdk.Context,
settledUpdateWithUpdatedSubaccount SettledUpdate,
perpetuals []perptypes.Perpetual,
) error {
// The subaccount in `settledUpdateWithUpdatedSubaccount` already has the perpetual updates
// and asset updates applied to it.
stateTransition, err := GetIsolatedPerpetualStateTransition(
settledUpdateWithUpdatedSubaccount,
perpetuals,
)
if err != nil {
return err
}
if err := k.transferCollateralForIsolatedPerpetual(
ctx,
stateTransition,
); err != nil {
return err
}

return nil
}

func getPerpIdToMarketTypeMap(
perpetuals []perptypes.Perpetual,
) map[uint32]perptypes.PerpetualMarketType {
var perpIdToMarketType = make(map[uint32]perptypes.PerpetualMarketType)

for _, perpetual := range perpetuals {
perpIdToMarketType[perpetual.GetId()] = perpetual.Params.MarketType
}

return perpIdToMarketType
}
Loading

0 comments on commit 52a5783

Please sign in to comment.