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

feat(perp): Add DNR Epoch Wiring #1683

Merged
merged 11 commits into from
Dec 1, 2023
2 changes: 2 additions & 0 deletions proto/nibiru/perp/v2/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ message GenesisState {

repeated DNRAllocation rebates_allocations = 12 [ (gogoproto.nullable) = false ];

string dnr_epoch_name = 14;

message GlobalVolume {
uint64 epoch = 1;
string volume = 2 [
Expand Down
20 changes: 20 additions & 0 deletions x/perp/v2/keeper/dnr.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@

func (intKeyEncoder) Stringify(key math.Int) string { return key.String() }

// maybeUpdateDnREpoch checks if the current epoch hook call matches the
// epoch name that targets discounts and rebates, if it does then we simply
// invoke the StartNewEpoch function to kickstart a new epoch.
// This method is invoked by the AfterEpochEnd hook.
func (k Keeper) maybeUpdateDnREpoch(ctx sdk.Context, epochIdentifier string, number uint64) {
// if epoch name is empty, we just assume DnR is not enabled.
referenceEpochName := k.DnREpochName.GetOr(ctx, "")
if referenceEpochName != epochIdentifier {
return
}
// kickstart new epoch
k.Logger(ctx).Info("updating dnr epoch", "epochIdentifier", epochIdentifier, "number", number)
err := k.StartNewEpoch(ctx, number)
if err != nil {
// in case of error we panic in this case, because state may have been updated
// in a corrupted way.
panic(err)

Check warning on line 87 in x/perp/v2/keeper/dnr.go

View check run for this annotation

Codecov / codecov/patch

x/perp/v2/keeper/dnr.go#L85-L87

Added lines #L85 - L87 were not covered by tests
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
}
}

// StartNewEpoch is called by the epochs hooks when a new 30day epoch starts.
func (k Keeper) StartNewEpoch(ctx sdk.Context, identifier uint64) error {
// set the current epoch
Expand Down
49 changes: 49 additions & 0 deletions x/perp/v2/keeper/dnr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"time"

"cosmossdk.io/math"
"github.com/NibiruChain/nibiru/app"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/x/common/asset"
"github.com/NibiruChain/nibiru/x/common/denoms"
Expand Down Expand Up @@ -251,3 +253,50 @@ func TestRebates(t *testing.T) {
}
NewTestSuite(t).WithTestCases(tests...).Run()
}

type actionFn func(app *app.NibiruApp, ctx sdk.Context) (outCtx sdk.Context, err error, isMandatory bool)

func (f actionFn) Do(app *app.NibiruApp, ctx sdk.Context) (outCtx sdk.Context, err error, isMandatory bool) {
return f(app, ctx)
}

func TestDnREpoch(t *testing.T) {
dnrEpochIdentifierIs := func(identifier string) actionFn {
return func(app *app.NibiruApp, ctx sdk.Context) (outCtx sdk.Context, err error, isMandatory bool) {
app.PerpKeeperV2.DnREpochName.Set(ctx, identifier)
return ctx, nil, false
}
}

triggerEpoch := func(identifier string, epoch uint64) actionFn {
return func(app *app.NibiruApp, ctx sdk.Context) (outCtx sdk.Context, err error, isMandatory bool) {
app.PerpKeeperV2.AfterEpochEnd(ctx, identifier, epoch)
return ctx, nil, false
}
}

expectDnREpoch := func(epoch uint64) actionFn {
return func(app *app.NibiruApp, ctx sdk.Context) (outCtx sdk.Context, err error, isMandatory bool) {
require.Equal(t, epoch, app.PerpKeeperV2.DnREpoch.GetOr(ctx, 0))
return ctx, nil, false
}
}

tests := TestCases{
TC("DnR epoch with valid identifier").
When(
dnrEpochIdentifierIs("monthly"),
triggerEpoch("monthly", 1)).
Then(
expectDnREpoch(1),
),
TC("DnR epoch with invalid identifier").
When(
dnrEpochIdentifierIs("monthly"),
triggerEpoch("weekly", 1)).
Then(
expectDnREpoch(0),
),
}
NewTestSuite(t).WithTestCases(tests...).Run()
}
3 changes: 2 additions & 1 deletion x/perp/v2/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import (
func (k Keeper) BeforeEpochStart(_ sdk.Context, _ string, _ uint64) {
}

func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, _ uint64) {
func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, number uint64) {
k.maybeUpdateDnREpoch(ctx, epochIdentifier, number)
for _, market := range k.Markets.Iterate(ctx, collections.Range[collections.Pair[asset.Pair, uint64]]{}).Values() {
if !market.Enabled || epochIdentifier != market.FundingRateEpochId {
return
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
8 changes: 7 additions & 1 deletion x/perp/v2/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ type Keeper struct {

Positions collections.Map[collections.Pair[collections.Pair[asset.Pair, uint64], sdk.AccAddress], types.Position]
ReserveSnapshots collections.Map[collections.Pair[asset.Pair, time.Time], types.ReserveSnapshot]
DnREpoch collections.Item[uint64]
DnREpoch collections.Item[uint64] // Keeps track of the current DnR epoch.
DnREpochName collections.Item[string] // Keeps track of the current DnR epoch identifier, provided by x/epoch.
GlobalVolumes collections.Map[uint64, math.Int] // Keeps track of global volumes for each epoch.
TraderVolumes collections.Map[collections.Pair[sdk.AccAddress, uint64], math.Int] // Keeps track of user volumes for each epoch.
GlobalDiscounts collections.Map[math.Int, math.LegacyDec] // maps a volume level to a discount
Expand Down Expand Up @@ -128,6 +129,10 @@ func NewKeeper(
storeKey, NamespaceCollateral,
common.StringValueEncoder,
),
DnREpochName: collections.NewItem(
storeKey, NamespaceDnrEpochName,
common.StringValueEncoder,
),
}
k.Admin = admin{&k}
return k
Expand All @@ -146,6 +151,7 @@ const (
NamespaceRebatesAllocations
NamespaceMarketLastVersion
NamespaceCollateral
NamespaceDnrEpochName
)

func (k Keeper) Logger(ctx sdk.Context) log.Logger {
Expand Down
3 changes: 3 additions & 0 deletions x/perp/v2/module/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState)

k.DnREpoch.Set(ctx, genState.DnrEpoch)

k.DnREpochName.Set(ctx, genState.DnrEpochName)

for _, m := range genState.Markets {
k.SaveMarket(ctx, m)
}
Expand Down Expand Up @@ -123,6 +125,7 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState {
}
genesis.ReserveSnapshots = k.ReserveSnapshots.Iterate(ctx, collections.PairRange[asset.Pair, time.Time]{}).Values()
genesis.DnrEpoch = k.DnREpoch.GetOr(ctx, 0)
genesis.DnrEpochName = k.DnREpochName.GetOr(ctx, "")

// export volumes
volumes := k.TraderVolumes.Iterate(ctx, collections.PairRange[sdk.AccAddress, uint64]{})
Expand Down
4 changes: 4 additions & 0 deletions x/perp/v2/module/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func RunTestGenesis(t *testing.T, tc TestCase) {
Epoch: 0,
Amount: sdk.NewCoins(sdk.NewCoin(denoms.NUSD, sdk.NewInt(1_000_000))),
})
app.PerpKeeperV2.DnREpochName.Set(ctx, "weekly")
app.PerpKeeperV2.DnREpoch.Set(ctx, 1)

// create some positions
for _, position := range tc.positions {
Expand Down Expand Up @@ -151,6 +153,8 @@ func RunTestGenesis(t *testing.T, tc TestCase) {
require.Equal(t, genState.CollateralDenom, genStateAfterInit.CollateralDenom)
require.Equal(t, genState.GlobalVolumes, genStateAfterInit.GlobalVolumes)
require.Equal(t, genState.RebatesAllocations, genStateAfterInit.RebatesAllocations)
require.Equal(t, genState.DnrEpochName, genStateAfterInit.DnrEpochName)
require.Equal(t, genState.DnrEpoch, genStateAfterInit.DnrEpoch)
}

func TestNewAppModuleBasic(t *testing.T) {
Expand Down
152 changes: 102 additions & 50 deletions x/perp/v2/types/genesis.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading