-
Notifications
You must be signed in to change notification settings - Fork 353
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(iro): IRO simulation tests (#1592)
Co-authored-by: keruch <[email protected]>
- Loading branch information
Showing
6 changed files
with
285 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters