Skip to content

Commit

Permalink
feat(iro): IRO simulation tests (#1592)
Browse files Browse the repository at this point in the history
Co-authored-by: keruch <[email protected]>
  • Loading branch information
mtsitrin and keruch authored Dec 8, 2024
1 parent de9f463 commit 30391f3
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 1 deletion.
2 changes: 1 addition & 1 deletion app/keepers/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func (a *AppKeepers) SetupModules(
packetforwardmiddleware.NewAppModule(a.PacketForwardMiddlewareKeeper, a.GetSubspace(packetforwardtypes.ModuleName)),
ibctransfer.NewAppModule(a.TransferKeeper),
rollappmodule.NewAppModule(appCodec, a.RollappKeeper),
iro.NewAppModule(appCodec, *a.IROKeeper),
iro.NewAppModule(appCodec, *a.IROKeeper, a.AccountKeeper, a.BankKeeper),

sequencermodule.NewAppModule(appCodec, a.SequencerKeeper),
sponsorship.NewAppModule(a.SponsorshipKeeper, a.AccountKeeper, a.BankKeeper, a.IncentivesKeeper, a.StakingKeeper),
Expand Down
9 changes: 9 additions & 0 deletions x/iro/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"

dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types"
"github.com/dymensionxyz/dymension/v3/x/iro/cli"
"github.com/dymensionxyz/dymension/v3/x/iro/keeper"
"github.com/dymensionxyz/dymension/v3/x/iro/types"
Expand Down Expand Up @@ -94,15 +95,23 @@ type AppModule struct {
AppModuleBasic

keeper keeper.Keeper

// simulation keepers
accountKeeper dymsimtypes.AccountKeeper
bankKeeper dymsimtypes.BankKeeper
}

func NewAppModule(
cdc codec.Codec,
keeper keeper.Keeper,
ak dymsimtypes.AccountKeeper,
bk dymsimtypes.BankKeeper,
) AppModule {
return AppModule{
AppModuleBasic: NewAppModuleBasic(cdc),
keeper: keeper,
accountKeeper: ak,
bankKeeper: bk,
}
}

Expand Down
26 changes: 26 additions & 0 deletions x/iro/module_simulation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package iro

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"

"github.com/dymensionxyz/dymension/v3/x/iro/simulation"
)

// ----------------------------------------------------------------------------
// AppModuleSimulation
// ----------------------------------------------------------------------------

// GenerateGenesisState creates a randomized GenState of x/iro.
func (AppModule) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}

// RegisterStoreDecoder registers a decoder for supply module's types.
func (AppModule) RegisterStoreDecoder(sdk.StoreDecoderRegistry) {}

// WeightedOperations returns the all the module's operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation {
return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper)
}
87 changes: 87 additions & 0 deletions x/iro/simulation/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package simulation

import (
"math/rand"
"time"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"

"github.com/dymensionxyz/dymension/v3/x/iro/types"
"github.com/dymensionxyz/sdk-utils/utils/urand"
)

// RandomizedGenState generates a random GenesisState for iro module
func RandomizedGenState(simState *module.SimulationState) {
// Generate number of plans. each operation will test one plan in random
numPlans := 30
plans := make([]types.Plan, numPlans)

for i := 0; i < numPlans; i++ {
plan := generateRandomPlan(simState.Rand, uint64(i+1))
plans[i] = plan
}

iroGenesis := types.GenesisState{
Params: types.DefaultParams(),
Plans: plans,
}

simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&iroGenesis)
}

func generateRandomPlan(r *rand.Rand, id uint64) types.Plan {
rollappId := urand.RollappID()

startTime := time.Now()
preLaunchTime := startTime.Add(24 * time.Hour)

// Generate random allocated amount (between 100_000 and 1_000_000_000 RA tokens)
baseDenom := types.IRODenom(rollappId)
allocatedAmount := simtypes.RandomAmount(r, math.NewInt(1e9).SubRaw(100_000)).AddRaw(100_000).MulRaw(1e18)
allocation := sdk.Coin{
Denom: baseDenom,
Amount: allocatedAmount,
}

// Generate random bonding curve
curve := generateRandomBondingCurve(r, allocatedAmount)
plan := types.NewPlan(id, rollappId, allocation, curve, startTime, preLaunchTime, types.DefaultIncentivePlanParams())

// randomize starting sold amount
// minSoldAmt < soldAmt < allocatedAmount - minUnsoldAmt
minSoldAmt := math.NewInt(1).MulRaw(1e18) // 1 token minimum
minUnsoldAmt := math.NewInt(100).MulRaw(1e18)
soldAmt := simtypes.RandomAmount(r, allocatedAmount.Sub(minUnsoldAmt).Sub(minSoldAmt)).Add(minSoldAmt)
plan.SoldAmt = soldAmt

return plan
}

func generateRandomBondingCurve(r *rand.Rand, allocatedAmount math.Int) types.BondingCurve {
// Generate 0.5 < N < 1.5 with maximum precision of 3
nInt := r.Int63n(1000) // Generate a random integer between 0 and 999
n := math.LegacyNewDecWithPrec(nInt, 3) // Convert to decimal with 3 decimal places
n = n.Add(math.LegacyNewDecWithPrec(5, 1)) // Add 0.5

// targetRaiseDYM between 10K and 100M DYM
targetRaiseDYM := simtypes.RandomAmount(r, math.NewInt(1e8)).AddRaw(10_000)

// Scale allocatedAmount from base denomination to decimal representation
allocatedTokens := types.ScaleFromBase(allocatedAmount, types.DYMDecimals)

m := types.CalculateM(
math.LegacyNewDecFromInt(targetRaiseDYM),
allocatedTokens,
n,
math.LegacyZeroDec(),
)

return types.BondingCurve{
M: m,
N: n,
C: math.LegacyZeroDec(),
}
}
153 changes: 153 additions & 0 deletions x/iro/simulation/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package simulation

import (
"fmt"
"math/rand"

"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"

dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types"
"github.com/dymensionxyz/dymension/v3/x/iro/keeper"
"github.com/dymensionxyz/dymension/v3/x/iro/types"
)

// Simulation operation weights constants
const (
DefaultWeightMsgTestBondingCurve int = 100
OpWeightMsgTestBondingCurve = "op_weight_msg_test_bonding_curve" //nolint:gosec
)

// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simtypes.AppParams,
cdc codec.JSONCodec,
ak dymsimtypes.AccountKeeper,
bk dymsimtypes.BankKeeper,
k keeper.Keeper,
) simulation.WeightedOperations {
protoCdc := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())

var weightMsgTestBondingCurve int
appParams.GetOrGenerate(cdc, OpWeightMsgTestBondingCurve, &weightMsgTestBondingCurve, nil,
func(_ *rand.Rand) {
weightMsgTestBondingCurve = DefaultWeightMsgTestBondingCurve
},
)

return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgTestBondingCurve,
SimulateTestBondingCurve(k, protoCdc),
),
}
}

// SimulateTestBondingCurve tests the bonding curve calculations without actual trading
func SimulateTestBondingCurve(k keeper.Keeper, cdc *codec.ProtoCodec) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
plans := k.GetAllPlans(ctx, true)
if len(plans) == 0 {
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", ""), nil, fmt.Errorf("no plans found")
}

// Randomly select a plan
plan := plans[r.Intn(len(plans))]
curve := plan.BondingCurve

// Test different token amounts
testAmounts := []math.Int{
math.NewInt(1).MulRaw(1e17), // 0.1 token
math.NewInt(1).MulRaw(1e18), // 1 token
math.NewInt(100).MulRaw(1e18), // 100 tokens
math.NewInt(1000).MulRaw(1e18), // 1000 tokens
math.NewInt(10000).MulRaw(1e18), // 10000 tokens
math.NewInt(100000).MulRaw(1e18), // 100000 tokens
}

// prepare base error with curve context
totalAll := types.ScaleFromBase(plan.TotalAllocation.Amount, types.DYMDecimals)
soldAmt := types.ScaleFromBase(plan.SoldAmt, types.DYMDecimals)
targetRaise := types.ScaleFromBase(curve.Cost(math.ZeroInt(), plan.TotalAllocation.Amount), types.DYMDecimals)
curveDesc := fmt.Sprintf("total supply: %s, sold amount: %s, target raise: %s, bonding curve: %s", totalAll.String(), soldAmt.String(), targetRaise.String(), curve.Stringify())

if curve.M.IsZero() {
err := fmt.Errorf("%s: M is zero", curveDesc)
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", err.Error()), nil, err
}

var results []string
managed := false
lastCost := math.ZeroInt()
for _, amount := range testAmounts {
scaledAmount := types.ScaleFromBase(amount, types.DYMDecimals)
// Calculate cost for buying tokens
cost := curve.Cost(plan.SoldAmt, amount.Add(plan.SoldAmt))
if !cost.IsPositive() {
continue
}

// Calculate tokens for exact DYM spend
tokens, err := curve.TokensForExactDYM(plan.SoldAmt, cost)
if err != nil {
err = fmt.Errorf("%s: buy amount: %s,tokens for exact DYM spend: %w", curveDesc, scaledAmount.String(), err)
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", err.Error()), nil, err
}

// validate tokens are approximately the same as the amount
if err := approxEqualInt(amount, tokens); err != nil {
err = fmt.Errorf("%s: buy amount: %s, tokens not equal to amount: %w", curveDesc, scaledAmount.String(), err)
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", err.Error()), nil, err
}

if cost.LTE(lastCost) {
err = fmt.Errorf("%s: cost not increasing: %s <= %s", curveDesc, cost.String(), lastCost.String())
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", err.Error()), nil, err
}

lastCost = cost
managed = true

results = append(results, fmt.Sprintf(
"Amount: %s tokens, Cost: %s DYM, TokensForExactDYM: %s tokens",
amount.String(), cost.String(), tokens.String(),
))
}

if !managed {
err := fmt.Errorf("%s: no valid cost found for any of the test amounts", curveDesc)
return simtypes.NoOpMsg(types.ModuleName, "TestBondingCurve", err.Error()), nil, err
}

return simtypes.NewOperationMsg(&types.MsgBuy{}, true, fmt.Sprintf("%s Results:\n%s", curveDesc, results), cdc), nil, nil
}
}

// approxEqualInt checks if two math.Ints are approximately equal
// returns an error if they are not
// tolerance is 6% of the expected result
func approxEqualInt(expected, actual math.Int) error {
/*
6% is a magic number that works well for most cases
below it, we will get errors in the simulation tests like
total supply: 1000000000.000000000000000000, sold amount: 1.000000000000000000, target raise: 100009999.643826642884775609,
bonding curve: M=0.000000000150696405 N=1.014000000000000000 C=0.000000000000000000:
buy amount: 0.100000000000000000,
tokens not equal to amount: expected 100000000000000000, got 105072307480633859, diff 5072307480633859 (tolerance: 5000000000000000)
*/
tolerance := expected.QuoRaw(100).MulRaw(6) // 6%

diff := expected.Sub(actual).Abs()
if tolerance.LT(diff) {
return fmt.Errorf("expected %s, got %s, diff %s (tolerance: %s)",
expected, actual, diff, tolerance)
}
return nil
}
9 changes: 9 additions & 0 deletions x/iro/types/bonding_curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,12 @@ func checkPrecision(d math.LegacyDec) bool {
multiplied := d.Mul(math.LegacyNewDec(10).Power(uint64(MaxNPrecision)))
return multiplied.IsInteger()
}

// String returns a human readable string representation of the bonding curve
func (lbc BondingCurve) Stringify() string {
return fmt.Sprintf("M=%s N=%s C=%s",
lbc.M.String(),
lbc.N.String(),
lbc.C.String(),
)
}

0 comments on commit 30391f3

Please sign in to comment.