Skip to content

Commit

Permalink
feat(perp): Track prepaid bad debt (#451)
Browse files Browse the repository at this point in the history
* Refactor imports

* Add prepaid bad debt state

* Add increment function and rename prepaidbaddebtstate

* Default return zero when not found

* Remove errors from PBD state

* Add withdraw method

* Fix lint errors
  • Loading branch information
NibiruHeisenberg authored May 22, 2022
1 parent 0d45ffb commit bcfb80a
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 10 deletions.
20 changes: 10 additions & 10 deletions x/perp/keeper/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/events"
"github.com/NibiruChain/nibiru/x/perp/types"
pooltypes "github.com/NibiruChain/nibiru/x/vpool/types"
vpooltypes "github.com/NibiruChain/nibiru/x/vpool/types"
)

// TODO test: OpenPosition | https://github.com/NibiruChain/nibiru/issues/299
Expand Down Expand Up @@ -279,13 +279,13 @@ func (k Keeper) getPositionNotionalAndUnrealizedPnL(
return sdk.ZeroDec(), sdk.ZeroDec(), nil
}

var baseAssetDirection pooltypes.Direction
var baseAssetDirection vpooltypes.Direction
if position.Size_.IsPositive() {
// LONG
baseAssetDirection = pooltypes.Direction_ADD_TO_POOL
baseAssetDirection = vpooltypes.Direction_ADD_TO_POOL
} else {
// SHORT
baseAssetDirection = pooltypes.Direction_REMOVE_FROM_POOL
baseAssetDirection = vpooltypes.Direction_REMOVE_FROM_POOL
}

switch pnlCalcOption {
Expand Down Expand Up @@ -638,11 +638,11 @@ func (k Keeper) closePositionEntirely(
positionResp.FundingPayment = remaining.FundingPayment
positionResp.MarginToVault = remaining.Margin.Neg()

var baseAssetDirection pooltypes.Direction
var baseAssetDirection vpooltypes.Direction
if currentPosition.Size_.IsPositive() {
baseAssetDirection = pooltypes.Direction_ADD_TO_POOL
baseAssetDirection = vpooltypes.Direction_ADD_TO_POOL
} else {
baseAssetDirection = pooltypes.Direction_REMOVE_FROM_POOL
baseAssetDirection = vpooltypes.Direction_REMOVE_FROM_POOL
}

exchangedQuoteAssetAmount, err := k.VpoolKeeper.SwapBaseForQuote(
Expand Down Expand Up @@ -795,12 +795,12 @@ func (k Keeper) swapQuoteForBase(
baseAssetLimit sdk.Dec,
canOverFluctuationLimit bool,
) (baseAmount sdk.Dec, err error) {
var quoteAssetDirection pooltypes.Direction
var quoteAssetDirection vpooltypes.Direction
if side == types.Side_BUY {
quoteAssetDirection = pooltypes.Direction_ADD_TO_POOL
quoteAssetDirection = vpooltypes.Direction_ADD_TO_POOL
} else {
// side == types.Side_SELL
quoteAssetDirection = pooltypes.Direction_REMOVE_FROM_POOL
quoteAssetDirection = vpooltypes.Direction_REMOVE_FROM_POOL
}

baseAmount, err = k.VpoolKeeper.SwapQuoteForBase(
Expand Down
49 changes: 49 additions & 0 deletions x/perp/keeper/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func (k Keeper) Whitelist() Whitelist {
return (Whitelist)(k)
}

func (k Keeper) PrepaidBadDebtState() PrepaidBadDebtState {
return (PrepaidBadDebtState)(k)
}

var paramsNamespace = []byte{0x0}
var paramsKey = []byte{0x0}

Expand Down Expand Up @@ -169,3 +173,48 @@ func (w Whitelist) IsWhitelisted(ctx sdk.Context, address string) bool {

return kv.Has([]byte(address))
}

var prepaidBadDebtNamespace = []byte{0x4}

type PrepaidBadDebtState Keeper

func (pbd PrepaidBadDebtState) getKVStore(ctx sdk.Context) sdk.KVStore {
return prefix.NewStore(ctx.KVStore(pbd.storeKey), prepaidBadDebtNamespace)
}

/*
Fetches the amount of bad debt prepaid by denom. Returns zero if the denom is not found.
*/
func (pbd PrepaidBadDebtState) Get(ctx sdk.Context, denom string) (amount sdk.Int) {
kv := pbd.getKVStore(ctx)

v := kv.Get([]byte(denom))
if v == nil {
return sdk.ZeroInt()
}

return sdk.NewIntFromUint64(sdk.BigEndianToUint64(v))
}

/*
Sets the amount of bad debt prepaid by denom.
*/
func (pbd PrepaidBadDebtState) Set(ctx sdk.Context, denom string, amount sdk.Int) {
kv := pbd.getKVStore(ctx)
kv.Set([]byte(denom), sdk.Uint64ToBigEndian(amount.Uint64()))
}

/*
Increments the amount of bad debt prepaid by denom.
Calling this method on a denom that doesn't exist is effectively the same as setting the amount (0 + increment).
*/
func (pbd PrepaidBadDebtState) Increment(ctx sdk.Context, denom string, increment sdk.Int) (
amount sdk.Int,
) {
kv := pbd.getKVStore(ctx)
amount = pbd.Get(ctx, denom).Add(increment)

kv.Set([]byte(denom), sdk.Uint64ToBigEndian(amount.Uint64()))

return amount
}
29 changes: 29 additions & 0 deletions x/perp/keeper/state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package keeper

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
)

func TestPrepaidBadDebtState(t *testing.T) {
perpKeeper, _, ctx := getKeeper(t)

t.Log("not found results in zero")
amount := perpKeeper.PrepaidBadDebtState().Get(ctx, "foo")
assert.EqualValues(t, sdk.ZeroInt(), amount)

t.Log("set and get")
perpKeeper.PrepaidBadDebtState().Set(ctx, "NUSD", sdk.NewInt(100))

amount = perpKeeper.PrepaidBadDebtState().Get(ctx, "NUSD")
assert.EqualValues(t, sdk.NewInt(100), amount)

t.Log("increment and check")
amount = perpKeeper.PrepaidBadDebtState().Increment(ctx, "NUSD", sdk.NewInt(50))
assert.EqualValues(t, sdk.NewInt(150), amount)

amount = perpKeeper.PrepaidBadDebtState().Get(ctx, "NUSD")
assert.EqualValues(t, sdk.NewInt(150), amount)
}
68 changes: 68 additions & 0 deletions x/perp/keeper/withdraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/NibiruChain/nibiru/x/perp/types"
)

/*
Withdraws coins from the vault to the receiver.
If the total amount of coins to withdraw is greater than the vault's amount, then
withdraw the shortage from the PerpEF and mark it as prepaid bad debt.
Prepaid bad debt will count towards realized bad debt from negative PnL positions
when those are closed/liquidated.
An example of this happening is when a long position has really high PnL and
closes their position, realizing their profits.
There is a counter party short position with really negative PnL, but
their position hasn't been closed/liquidated yet.
We must pay the long trader first, which results in funds being taken from the EF.
When the short position is closed, it also realizes some bad debt but because
we have already withdrawn from the EF, we don't need to withdraw more from the EF.
*/
func (k Keeper) Withdraw(
ctx sdk.Context,
denom string,
receiver sdk.AccAddress,
amountToWithdraw sdk.Int,
) (err error) {
if !amountToWithdraw.IsPositive() {
return nil
}

vaultQuoteBalance := k.BankKeeper.GetBalance(
ctx,
k.AccountKeeper.GetModuleAddress(types.VaultModuleAccount),
denom,
)
if vaultQuoteBalance.Amount.LT(amountToWithdraw) {
// if withdraw amount is larger than entire balance of vault
// means this trader's profit comes from other under collateral position's future loss
// and the balance of entire vault is not enough
// need money from PerpEF to pay first, and record this prepaidBadDebt
shortage := amountToWithdraw.Sub(vaultQuoteBalance.Amount)
k.PrepaidBadDebtState().Increment(ctx, denom, shortage)
if err := k.BankKeeper.SendCoinsFromModuleToModule(
ctx,
types.PerpEFModuleAccount,
types.VaultModuleAccount,
sdk.NewCoins(
sdk.NewCoin(denom, shortage),
),
); err != nil {
return err
}
}

// Transfer from Vault to receiver
return k.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
/* from */ types.VaultModuleAccount,
/* to */ receiver,
sdk.NewCoins(
sdk.NewCoin(denom, amountToWithdraw),
),
)
}
97 changes: 97 additions & 0 deletions x/perp/keeper/withdraw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package keeper

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/x/perp/types"
"github.com/NibiruChain/nibiru/x/testutil/sample"
)

func TestWithdraw(t *testing.T) {
tests := []struct {
name string
initialVaultBalance int64
initialPrepaidBadDebt int64
amountToWithdraw int64

expectedPerpEFWithdrawal int64
expectedFinalPrepaidBadDebt int64
}{
{
name: "no bad debt",
initialVaultBalance: 100,
initialPrepaidBadDebt: 0,

amountToWithdraw: 10,

expectedPerpEFWithdrawal: 0,
expectedFinalPrepaidBadDebt: 0,
},
{
name: "creates prepaid bad debt",
initialVaultBalance: 9,
initialPrepaidBadDebt: 0,

amountToWithdraw: 10,

expectedPerpEFWithdrawal: 1,
expectedFinalPrepaidBadDebt: 1,
},
{
name: "increases existing prepaid bad debt",
initialVaultBalance: 9,
initialPrepaidBadDebt: 1,

amountToWithdraw: 10,

expectedPerpEFWithdrawal: 1,
expectedFinalPrepaidBadDebt: 2,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Log("initialize variables")
perpKeeper, mocks, ctx := getKeeper(t)
receiver := sample.AccAddress()
denom := "NUSD"

t.Log("mock account keeper")
vaultAddr := authtypes.NewModuleAddress(types.VaultModuleAccount)
mocks.mockAccountKeeper.EXPECT().GetModuleAddress(
types.VaultModuleAccount).
Return(vaultAddr)

t.Log("mock bank keeper")
mocks.mockBankKeeper.EXPECT().GetBalance(ctx, vaultAddr, denom).
Return(sdk.NewInt64Coin(denom, tc.initialVaultBalance))
mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(
ctx, types.VaultModuleAccount, receiver,
sdk.NewCoins(sdk.NewInt64Coin(denom, tc.amountToWithdraw)),
).Return(nil)
if tc.expectedPerpEFWithdrawal > 0 {
mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule(
ctx, types.PerpEFModuleAccount, types.VaultModuleAccount,
sdk.NewCoins(sdk.NewInt64Coin(denom, tc.expectedPerpEFWithdrawal)),
).Return(nil)
}

t.Log("initial prepaid bad debt")
perpKeeper.PrepaidBadDebtState().Set(ctx, denom, sdk.NewInt(tc.initialPrepaidBadDebt))

t.Log("execute withdrawal")
err := perpKeeper.Withdraw(ctx, denom, receiver, sdk.NewInt(tc.amountToWithdraw))
require.NoError(t, err)

t.Log("assert new prepaid bad debt")
prepaidBadDebt := perpKeeper.PrepaidBadDebtState().Get(ctx, denom)
assert.EqualValues(t, tc.expectedFinalPrepaidBadDebt, prepaidBadDebt.Int64())
})
}
}

0 comments on commit bcfb80a

Please sign in to comment.