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 2 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
193 changes: 176 additions & 17 deletions protocol/x/subaccounts/keeper/isolated_subaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,35 @@ 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"
)

type positionStateTransition uint

const (
opened positionStateTransition = iota
closed
)

// Represents a state transition for an isolated perpetual.
teddyding marked this conversation as resolved.
Show resolved Hide resolved
type isolatedPerpetualStateTransition struct {
perpetualId uint32
// TODO(DEC-715): Support non-USDC assets.
// USDC position size of the subaccount that has a state change for an isolated perpetual.
usdcQuantumsBeforeUpdate *big.Int
teddyding marked this conversation as resolved.
Show resolved Hide resolved
transition positionStateTransition
teddyding marked this conversation as resolved.
Show resolved Hide resolved
}
vincentwschau marked this conversation as resolved.
Show resolved Hide resolved
vincentwschau marked this conversation as resolved.
Show resolved Hide resolved

// checkIsolatedSubaccountConstaints will validate all `updates` to the relevant subaccounts against
// isolated subaccount constraints.
// isolated subaccount constraints and computes whether each update leads to a state change
// (open / close) to an isolated perpetual occurred due to the updates if they are valid.
// This function checks each update in isolation, so if multiple updates for the same subaccount id
// are passed in, they are not evaluated separately.
// The input subaccounts must be settled.
Expand All @@ -20,72 +39,88 @@ import (
// Returns a `successPerUpdates` value, which is a slice of `UpdateResult`.
// These map to the updates and are used to indicate which of the updates
// caused a failure, if any.
// Returns a `isolatedPerpetualStateTransitions` value, which is a slice of
// `isolatedPerpetualStateTransition`.
// These map to the updates and are used to indicate which of the updates opened or closed an
// isolated perpetual position.
func (k Keeper) checkIsolatedSubaccountConstraints(
teddyding marked this conversation as resolved.
Show resolved Hide resolved
ctx sdk.Context,
settledUpdates []settledUpdate,
perpetuals []perptypes.Perpetual,
) (
success bool,
successPerUpdate []types.UpdateResult,
isolatedPerpetualStateTransitions []*isolatedPerpetualStateTransition,
err error,
) {
success = true
successPerUpdate = make([]types.UpdateResult, len(settledUpdates))
isolatedPerpetualStateTransitions = make([]*isolatedPerpetualStateTransition, len(settledUpdates))
var perpIdToMarketType = make(map[uint32]perptypes.PerpetualMarketType)

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

for i, u := range settledUpdates {
result, err := isValidIsolatedPerpetualUpdates(u, perpIdToMarketType)
result, stateTransition, err := processIsolatedPerpetualUpdates(u, perpIdToMarketType)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
if result != types.Success {
success = false
}

successPerUpdate[i] = result
isolatedPerpetualStateTransitions[i] = stateTransition
}

return success, successPerUpdate, nil
return success, successPerUpdate, isolatedPerpetualStateTransitions, nil
}

// Checks whether the perpetual updates to a settled subaccount violates constraints for isolated
// perpetuals. This function assumes the settled subaccount is valid and does not violate the
// the constraints.
// processIsolatedPerpetualUpdates checks whether the perpetual updates to a settled subaccount
// violates constraints for isolated perpetuals and computes whether the perpetual updates result in
// a state change (open / close) for an isolated perpetual position if the updates are valid.
// This function assumes the settled subaccount is valid and does not violate the constraints.
// The constraint being checked is:
// - a subaccount with a position in an isolated perpetual cannot have updates for other
// perpetuals
// - a subaccount with a position in a non-isolated perpetual cannot have updates for isolated
// perpetuals
// - 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(
//
// If there is a state change (open / close) from the perpetual updates, it is returned along with
// the perpetual id of the isolated perpetual and the size of the USDC position in the subaccount.
func processIsolatedPerpetualUpdates(
settledUpdate settledUpdate,
perpIdToMarketType map[uint32]perptypes.PerpetualMarketType,
) (types.UpdateResult, error) {
) (types.UpdateResult, *isolatedPerpetualStateTransition, error) {
// If there are no perpetual updates, then this update does not violate constraints for isolated
// markets.
if len(settledUpdate.PerpetualUpdates) == 0 {
return types.Success, nil
return types.Success, nil, nil
}

// Check if the updates contain an update to an isolated perpetual.
hasIsolatedUpdate := false
isolatedUpdatePerpetualId := uint32(math.MaxUint32)
isolatedUpdate := (*types.PerpetualUpdate)(nil)
for _, perpetualUpdate := range settledUpdate.PerpetualUpdates {
marketType, exists := perpIdToMarketType[perpetualUpdate.PerpetualId]
if !exists {
return types.UpdateCausedError, errorsmod.Wrap(
return types.UpdateCausedError, nil, errorsmod.Wrap(
perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualUpdate.PerpetualId),
)
}

if marketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED {
hasIsolatedUpdate = true
isolatedUpdatePerpetualId = perpetualUpdate.PerpetualId
isolatedUpdate = &types.PerpetualUpdate{
PerpetualId: perpetualUpdate.PerpetualId,
BigQuantumsDelta: perpetualUpdate.GetBigQuantums(),
}
break
}
}
Expand All @@ -94,38 +129,40 @@ func isValidIsolatedPerpetualUpdates(
// Assumes the subaccount itself does not violate the isolated perpetual constraints.
isIsolatedSubaccount := false
isolatedPositionPerpetualId := uint32(math.MaxUint32)
isolatedPerpetualPosition := (*types.PerpetualPosition)(nil)
hasPerpetualPositions := len(settledUpdate.SettledSubaccount.PerpetualPositions) > 0
for _, perpetualPosition := range settledUpdate.SettledSubaccount.PerpetualPositions {
marketType, exists := perpIdToMarketType[perpetualPosition.PerpetualId]
if !exists {
return types.UpdateCausedError, errorsmod.Wrap(
return types.UpdateCausedError, nil, errorsmod.Wrap(
perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualPosition.PerpetualId),
)
}

if marketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED {
isIsolatedSubaccount = true
isolatedPositionPerpetualId = perpetualPosition.PerpetualId
isolatedPerpetualPosition = perpetualPosition
break
}
}

// A subaccount with a perpetual position in an isolated perpetual cannot have updates to other
// non-isolated perpetuals.
if isIsolatedSubaccount && !hasIsolatedUpdate {
return types.ViolatesIsolatedSubaccountConstraints, nil
return types.ViolatesIsolatedSubaccountConstraints, nil, nil
}

// A subaccount with perpetual positions in non-isolated perpetuals cannot have an update
// to an isolated perpetual.
if !isIsolatedSubaccount && hasPerpetualPositions && hasIsolatedUpdate {
return types.ViolatesIsolatedSubaccountConstraints, nil
return types.ViolatesIsolatedSubaccountConstraints, nil, nil
}

// There cannot be more than a single perpetual update if an update to an isolated perpetual
// exists in the slice of perpetual updates.
if hasIsolatedUpdate && len(settledUpdate.PerpetualUpdates) > 1 {
return types.ViolatesIsolatedSubaccountConstraints, nil
return types.ViolatesIsolatedSubaccountConstraints, nil, nil
}

// Note we can assume that if `hasIsolatedUpdate` is true, there is only a single perpetual
Expand All @@ -135,8 +172,130 @@ func isValidIsolatedPerpetualUpdates(
if isIsolatedSubaccount &&
hasIsolatedUpdate &&
isolatedPositionPerpetualId != isolatedUpdatePerpetualId {
return types.ViolatesIsolatedSubaccountConstraints, nil
return types.ViolatesIsolatedSubaccountConstraints, nil, nil
}

return types.Success,
getIsolatedPerpetualStateTransition(
settledUpdate.SettledSubaccount,
isolatedPerpetualPosition,
isolatedUpdate,
),
nil
}

// getIsolatedPerpetualStateTransition computes whether an isolated perpetual position will be
// opened or closed for a subaccount given an isolated perpetual update for the subaccount.
// Input subaccount account must be settled.
func getIsolatedPerpetualStateTransition(
teddyding marked this conversation as resolved.
Show resolved Hide resolved
settledSubaccount types.Subaccount,
isolatedPerpetualPosition *types.PerpetualPosition,
isolatedPerpetualUpdate *types.PerpetualUpdate,
) *isolatedPerpetualStateTransition {
// If there is no update to an isolated perpetual position, then no state transitions have
// happened for the isolated perpetual.
if isolatedPerpetualUpdate == nil {
return nil
}

perpetualId := isolatedPerpetualUpdate.PerpetualId
// TODO(DEC-715): Support non-USDC assets.
usdcQuantumsBeforeUpdate := new(big.Int).Set(settledSubaccount.GetUsdcPosition())

// If the subaccount has no isolated perpetual position, then this update is opening an isolated
// perpetual position.
if isolatedPerpetualPosition == nil {
return &isolatedPerpetualStateTransition{
perpetualId: perpetualId,
usdcQuantumsBeforeUpdate: usdcQuantumsBeforeUpdate,
transition: opened,
}
}

// If the position size after the update is zero, then this update is closing an isolated
// perpetual position.
if finalPositionSize := new(big.Int).Add(
isolatedPerpetualPosition.GetBigQuantums(),
isolatedPerpetualUpdate.GetBigQuantums(),
); finalPositionSize.Cmp(lib.BigInt0()) == 0 {
return &isolatedPerpetualStateTransition{
perpetualId: perpetualId,
usdcQuantumsBeforeUpdate: usdcQuantumsBeforeUpdate,
transition: closed,
}
}

// If a position was not opened or closed, no state transition happened from the perpetual
// update.
return nil
}
vincentwschau marked this conversation as resolved.
Show resolved Hide resolved

// 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,
updatedSubaccount types.Subaccount,
stateTransition *isolatedPerpetualStateTransition,
) 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
}

// If an isolated perpetual position was opened in the subaccount, then move collateral equivalent
// to the USDC asset position size of the subaccount before the update from the
// cross-perpetual collateral pool to the isolated perpetual collateral pool.
if stateTransition.transition == opened {
teddyding marked this conversation as resolved.
Show resolved Hide resolved
_, coinToTransfer, err := k.assetsKeeper.ConvertAssetToCoin(
ctx,
// TODO(DEC-715): Support non-USDC assets.
assettypes.AssetUsdc.Id,
stateTransition.usdcQuantumsBeforeUpdate,
)
if err != nil {
return err
}

if err := k.bankKeeper.SendCoins(
ctx,
types.ModuleAddress,
isolatedCollateralPoolAddr,
[]sdk.Coin{coinToTransfer},
); err != nil {
return err
}
return nil
// If the isolated perpetual position was closed, then move collateral equivalent to the USDC
// asset position size of the subaccount after the update from the isolated perpetual collateral
// pool to the cross-perpetual collateral pool.
} else if stateTransition.transition == closed {
_, coinToTransfer, err := k.assetsKeeper.ConvertAssetToCoin(
ctx,
// TODO(DEC-715): Support non-USDC assets.
assettypes.AssetUsdc.Id,
updatedSubaccount.GetUsdcPosition(),
)
if err != nil {
return err
}

if err := k.bankKeeper.SendCoins(
ctx,
isolatedCollateralPoolAddr,
types.ModuleAddress,
[]sdk.Coin{coinToTransfer},
); err != nil {
return err
}
return nil
}

return types.Success, nil
return nil
}
Loading
Loading