Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TRA-139] Add logic to transfer collateral when open/close isolated perpetual position. #1200

Merged
merged 6 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
202 changes: 195 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(
teddyding marked this conversation as resolved.
Show resolved Hide resolved
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,193 @@ 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
}

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: fix indent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's from go fmt, this indent is expected. I tried to fix it too and go fmt kept on changing it back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird

// 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,
)
}

// If there are zero quantums to transfer, don't transfer collateral.
if stateTransition.QuoteQuantums.Sign() == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe this should be at the top of the func?

return nil
}

// 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

informational q: what exactly are we converting for the collateral here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts between quantums (units that x/subaccounts use to track balances) and coins (units that x/bank use to track balances).

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,
teddyding marked this conversation as resolved.
Show resolved Hide resolved
[]sdk.Coin{coinToTransfer},
); 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
Loading