Skip to content

Commit

Permalink
feat(IRO): IRO Rollapp token creation fee (#1333)
Browse files Browse the repository at this point in the history
Co-authored-by: zale144 <[email protected]>
Co-authored-by: ducnt131 <[email protected]>
  • Loading branch information
3 people authored Oct 20, 2024
1 parent 0a553f9 commit 198f418
Show file tree
Hide file tree
Showing 13 changed files with 891 additions and 299 deletions.
18 changes: 18 additions & 0 deletions proto/dymensionxyz/dymension/iro/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ service Query {
option (google.api.http).get = "/dymensionxyz/dymension/iro/cost/{plan_id}";
}

rpc QueryTokensForDYM(QueryTokensForDYMRequest) returns (QueryTokensForDYMResponse) {
option (google.api.http).get = "/dymensionxyz/dymension/iro/tokens_for_dym/{plan_id}";
}

// QueryClaimed retrieves the claimed amount thus far for the specified plan ID.
rpc QueryClaimed(QueryClaimedRequest) returns (QueryClaimedResponse) {
option (google.api.http).get =
Expand Down Expand Up @@ -108,6 +112,20 @@ message QueryCostRequest {
// QueryCostResponse is the response type for the Query/QueryCost RPC method.
message QueryCostResponse { cosmos.base.v1beta1.Coin cost = 1; }


// QueryTokensForDYMRequest is the request type for the Query/QueryTokensForDYM RPC method.
message QueryTokensForDYMRequest {
string plan_id = 1;
string amt = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
}

// QueryTokensForDYMResponse is the response type for the Query/QueryTokensForDYM RPC method.
message QueryTokensForDYMResponse { cosmos.base.v1beta1.Coin tokens = 1; }


// QueryClaimedRequest is the request type for the Query/QueryClaimed RPC
// method.
message QueryClaimedRequest { string plan_id = 1; }
Expand Down
5 changes: 5 additions & 0 deletions x/iro/keeper/create_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func (k Keeper) CreatePlan(ctx sdk.Context, allocatedAmount math.Int, start, pre
if err != nil {
return "", err
}

plan := types.NewPlan(k.GetNextPlanIdAndIncrement(ctx), rollapp.RollappId, allocation, curve, start, preLaunchTime, incentivesParams)
if err := plan.ValidateBasic(); err != nil {
return "", errors.Join(gerrc.ErrInvalidArgument, err)
Expand All @@ -129,6 +130,10 @@ func (k Keeper) CreatePlan(ctx sdk.Context, allocatedAmount math.Int, start, pre
return "", err
}

// charge rollapp token creation fee. Same as DYM creation fee, will be used to open the pool.
tokenFee := math.NewIntWithDecimal(types.TokenCreationFee, int(rollapp.GenesisInfo.NativeDenom.Exponent))
plan.SoldAmt = tokenFee

// Set the plan in the store
k.SetPlan(ctx, plan)

Expand Down
21 changes: 21 additions & 0 deletions x/iro/keeper/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ func (k Keeper) QueryCost(goCtx context.Context, req *types.QueryCostRequest) (*
return &types.QueryCostResponse{Cost: &cost}, nil
}

// QueryTokensForDYM implements types.QueryServer.
func (k Keeper) QueryTokensForDYM(goCtx context.Context, req *types.QueryTokensForDYMRequest) (*types.QueryTokensForDYMResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)

plan, found := k.GetPlan(ctx, req.PlanId)
if !found {
return nil, status.Error(codes.NotFound, "plan not found")
}

tokensAmt, err := plan.BondingCurve.TokensForExactDYM(plan.SoldAmt, req.Amt)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

tokens := sdk.NewCoin(plan.GetIRODenom(), tokensAmt)
return &types.QueryTokensForDYMResponse{Tokens: &tokens}, nil
}

// QueryPlan implements types.QueryServer.
func (k Keeper) QueryPlan(goCtx context.Context, req *types.QueryPlanRequest) (*types.QueryPlanResponse, error) {
if req == nil {
Expand Down
5 changes: 3 additions & 2 deletions x/iro/keeper/settle.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ func (k Keeper) Settle(ctx sdk.Context, rollappId, rollappIBCDenom string) error
// - Creates a balancer pool with the determined tokens and DYM.
// - Uses leftover tokens as incentives to the pool LP token holders.
func (k Keeper) bootstrapLiquidityPool(ctx sdk.Context, plan types.Plan) (poolID, gaugeID uint64, err error) {
unallocatedTokens := plan.TotalAllocation.Amount.Sub(plan.SoldAmt) // assumed > 0, as we enforce it in the Buy function
raisedDYM := k.BK.GetBalance(ctx, plan.GetAddress(), appparams.BaseDenom) // assumed > 0, as we enforce it by IRO creation fee
tokenFee := math.NewIntWithDecimal(types.TokenCreationFee, int(plan.BondingCurve.SupplyDecimals()))
unallocatedTokens := plan.TotalAllocation.Amount.Sub(plan.SoldAmt.Sub(tokenFee)) // at least "reserve" amount of tokens (>0)
raisedDYM := k.BK.GetBalance(ctx, plan.GetAddress(), appparams.BaseDenom) // at least IRO creation fee (>0)

// send the raised DYM to the iro module as it will be used as the pool creator
err = k.BK.SendCoinsFromAccountToModule(ctx, plan.GetAddress(), types.ModuleName, sdk.NewCoins(raisedDYM))
Expand Down
266 changes: 102 additions & 164 deletions x/iro/keeper/settle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
appparams "github.com/dymensionxyz/dymension/v3/app/params"
"github.com/dymensionxyz/dymension/v3/testutil/sample"
incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types"
keeper "github.com/dymensionxyz/dymension/v3/x/iro/keeper"
"github.com/dymensionxyz/dymension/v3/x/iro/types"
)

Expand Down Expand Up @@ -62,177 +61,116 @@ func (s *KeeperTestSuite) TestSettle() {
s.Require().Equal(soldAmt, balance.Amount)
}

// Test liquidity pool bootstrap
func (s *KeeperTestSuite) TestBootstrapLiquidityPool() {
rollappId := s.CreateDefaultRollapp()
k := s.App.IROKeeper
curve := types.DefaultBondingCurve()
incentives := types.DefaultIncentivePlanParams()

startTime := time.Now()
allocation := sdk.NewInt(1_000_000).MulRaw(1e18)
maxAmt := sdk.NewInt(1_000_000_000).MulRaw(1e18)
rollappDenom := "dasdasdasdasdsa"

rollapp := s.App.RollappKeeper.MustGetRollapp(s.Ctx, rollappId)

// create IRO plan
apptesting.FundAccount(s.App, s.Ctx, sdk.MustAccAddressFromBech32(rollapp.Owner), sdk.NewCoins(sdk.NewCoin(appparams.BaseDenom, k.GetParams(s.Ctx).CreationFee)))
planId, err := k.CreatePlan(s.Ctx, allocation, startTime, startTime.Add(time.Hour), rollapp, curve, incentives)
s.Require().NoError(err)

// buy some tokens
s.Ctx = s.Ctx.WithBlockTime(startTime.Add(time.Minute))
buyer := sample.Acc()
buyersFunds := sdk.NewCoins(sdk.NewCoin("adym", maxAmt))
s.FundAcc(buyer, buyersFunds)

err = k.Buy(s.Ctx, planId, buyer, sdk.NewInt(1_000).MulRaw(1e18), maxAmt)
s.Require().NoError(err)

plan := k.MustGetPlan(s.Ctx, planId)
raisedDYM := k.BK.GetBalance(s.Ctx, plan.GetAddress(), appparams.BaseDenom)
preSettleCoins := sdk.NewCoins(raisedDYM, sdk.NewCoin(rollappDenom, allocation.Sub(plan.SoldAmt)))

// settle should succeed after fund
s.FundModuleAcc(types.ModuleName, sdk.NewCoins(sdk.NewCoin(rollappDenom, allocation)))
err = k.Settle(s.Ctx, rollappId, rollappDenom)
s.Require().NoError(err)

/* -------------------------- assert liquidity pool ------------------------- */
// pool created
expectedPoolID := uint64(1)
pool, err := s.App.GAMMKeeper.GetPool(s.Ctx, expectedPoolID)
s.Require().NoError(err)

// pool price should be the same as the last price of the plan
price, err := pool.SpotPrice(s.Ctx, "adym", rollappDenom)
s.Require().NoError(err)

plan = k.MustGetPlan(s.Ctx, planId)
lastPrice := plan.SpotPrice()
s.Require().Equal(lastPrice, price)

// assert incentives
poolCoins := pool.GetTotalPoolLiquidity(s.Ctx)
gauges, err := s.App.IncentivesKeeper.GetGaugesForDenom(s.Ctx, gammtypes.GetPoolShareDenom(expectedPoolID))
s.Require().NoError(err)
found := false
gauge := incentivestypes.Gauge{}
for _, gauge = range gauges {
if !gauge.IsPerpetual {
found = true
break
}
}
s.Require().True(found)
s.Require().False(gauge.Coins.IsZero())

// expected tokens for incentives:
// raisedDYM - poolCoins
// totalAllocation - soldAmt - poolCoins
expectedIncentives := preSettleCoins.Sub(poolCoins...)
s.Assert().Equal(expectedIncentives, gauge.Coins)
}

func (s *KeeperTestSuite) TestSettleNothingSold() {
rollappId := s.CreateDefaultRollapp()
k := s.App.IROKeeper
curve := types.DefaultBondingCurve()
incentives := types.DefaultIncentivePlanParams()

startTime := time.Now()
endTime := startTime.Add(time.Hour)
amt := sdk.NewInt(1_000_000).MulRaw(1e18)
rollappDenom := "rollapp_denom"

rollapp := s.App.RollappKeeper.MustGetRollapp(s.Ctx, rollappId)
_, err := k.CreatePlan(s.Ctx, amt, startTime, endTime, rollapp, curve, incentives)
s.Require().NoError(err)
// planDenom := k.MustGetPlan(s.Ctx, planId).TotalAllocation.Denom

// Settle without any tokens sold
s.Ctx = s.Ctx.WithBlockTime(endTime.Add(time.Minute))
s.FundModuleAcc(types.ModuleName, sdk.NewCoins(sdk.NewCoin(rollappDenom, amt)))
err = k.Settle(s.Ctx, rollappId, rollappDenom)
s.Require().NoError(err)

/* -------------------------- assert liquidity pool ------------------------- */
// pool created
expectedPoolID := uint64(1)
pool, err := s.App.GAMMKeeper.GetPool(s.Ctx, expectedPoolID)
s.Require().NoError(err)
poolCoins := pool.GetTotalPoolLiquidity(s.Ctx)
poolCoins.AmountOf("adym").Equal(s.App.IROKeeper.GetParams(s.Ctx).CreationFee)

// incentives expected to have zero coins
gauges, err := s.App.IncentivesKeeper.GetGaugesForDenom(s.Ctx, gammtypes.GetPoolShareDenom(expectedPoolID))
s.Require().NoError(err)
found := false
gauge := incentivestypes.Gauge{}
for _, gauge = range gauges {
if !gauge.IsPerpetual {
found = true
break
}
}
s.Require().True(found)
s.Require().True(gauge.Coins.IsZero())
}

func (s *KeeperTestSuite) TestSettleAllSold() {
rollappId := s.CreateDefaultRollapp()
k := s.App.IROKeeper
// setting curve with fixed price
curve := types.BondingCurve{
M: math.LegacyMustNewDecFromStr("0"),
N: math.LegacyMustNewDecFromStr("1"),
C: math.LegacyMustNewDecFromStr("0.00001"),
C: math.LegacyMustNewDecFromStr("0.1"), // each token costs 0.1 DYM
}
incentives := types.DefaultIncentivePlanParams()

startTime := time.Now()
endTime := startTime.Add(time.Hour)
amt := sdk.NewInt(1_000_000).MulRaw(1e18)
rollappDenom := "rollapp_denom"

rollapp := s.App.RollappKeeper.MustGetRollapp(s.Ctx, rollappId)
planId, err := k.CreatePlan(s.Ctx, amt, startTime, endTime, rollapp, curve, incentives)
s.Require().NoError(err)

// Buy all possible tokens
s.Ctx = s.Ctx.WithBlockTime(startTime.Add(time.Minute))
buyer := sample.Acc()
buyAmt := amt.ToLegacyDec().Mul(keeper.AllocationSellLimit).TruncateInt()
s.BuySomeTokens(planId, buyer, buyAmt)

// Settle
s.Ctx = s.Ctx.WithBlockTime(endTime.Add(time.Minute))
s.FundModuleAcc(types.ModuleName, sdk.NewCoins(sdk.NewCoin(rollappDenom, amt)))
err = k.Settle(s.Ctx, rollappId, rollappDenom)
s.Require().NoError(err)

plan := k.MustGetPlan(s.Ctx, planId)
allocation := sdk.NewInt(1_000_000).MulRaw(1e18)
rollappDenom := "dasdasdasdasdsa"

pool, err := s.App.GAMMKeeper.GetPool(s.Ctx, 1)
s.Require().NoError(err)
testCases := []struct {
name string
buyAmt math.Int
expectedDYM math.Int
expectedTokens math.Int
}{
// for small purchases, the raised dym is the limiting factor:
// - the expected DYM in the pool is the buy amount * 0.1 (fixed price) + 10 DYM creation fee
// for large purchases, the left tokens are the limiting factor:
// - the expected DYM in the pool is the left tokens / 0.1 (fixed price)
{
name: "Small purchase",
buyAmt: math.NewInt(1_000).MulRaw(1e18),
expectedDYM: math.NewInt(110).MulRaw(1e18),
expectedTokens: math.NewInt(1_100).MulRaw(1e18),
},
{
name: "Large purchase - left tokens are limiting factor",
buyAmt: math.NewInt(800_000).MulRaw(1e18),
expectedDYM: math.NewInt(20_000).MulRaw(1e18),
expectedTokens: math.NewInt(200_000).MulRaw(1e18),
},
{
name: "Nothing sold - pool contains only creation fee",
buyAmt: math.NewInt(0),
expectedDYM: math.NewInt(10).MulRaw(1e18), // creation fee
expectedTokens: math.NewInt(100).MulRaw(1e18),
},
{
name: "All sold - pool contains only reserved tokens",
buyAmt: math.NewInt(999_999).MulRaw(1e18),
expectedDYM: math.NewInt(1).MulRaw(1e17), // 0.1 DYM
expectedTokens: math.NewInt(1).MulRaw(1e18), // reserved tokens
},
}

gauges, err := s.App.IncentivesKeeper.GetGaugesForDenom(s.Ctx, gammtypes.GetPoolShareDenom(1))
s.Require().NoError(err)
found := false
gauge := incentivestypes.Gauge{}
for _, gauge = range gauges {
if !gauge.IsPerpetual {
found = true
break
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTest() // Reset the test state for each test case
rollappId := s.CreateDefaultRollapp()
rollapp := s.App.RollappKeeper.MustGetRollapp(s.Ctx, rollappId)
k := s.App.IROKeeper

// Create IRO plan
apptesting.FundAccount(s.App, s.Ctx, sdk.MustAccAddressFromBech32(rollapp.Owner), sdk.NewCoins(sdk.NewCoin(appparams.BaseDenom, k.GetParams(s.Ctx).CreationFee)))
planId, err := k.CreatePlan(s.Ctx, allocation, startTime, startTime.Add(time.Hour), rollapp, curve, types.DefaultIncentivePlanParams())
s.Require().NoError(err)
reservedTokens := k.MustGetPlan(s.Ctx, planId).SoldAmt

// Buy tokens
if tc.buyAmt.GT(math.ZeroInt()) {
s.Ctx = s.Ctx.WithBlockTime(startTime.Add(time.Minute))
buyer := sample.Acc()
s.BuySomeTokens(planId, buyer, tc.buyAmt)
}

plan := k.MustGetPlan(s.Ctx, planId)
raisedDYM := k.BK.GetBalance(s.Ctx, plan.GetAddress(), appparams.BaseDenom)
unallocatedTokensAmt := allocation.Sub(plan.SoldAmt).Add(reservedTokens)

// Settle
s.FundModuleAcc(types.ModuleName, sdk.NewCoins(sdk.NewCoin(rollappDenom, allocation)))
err = k.Settle(s.Ctx, rollappId, rollappDenom)
s.Require().NoError(err)

// Assert liquidity pool
poolId := uint64(1)
pool, err := s.App.GAMMKeeper.GetPool(s.Ctx, poolId)
s.Require().NoError(err)

poolCoins := pool.GetTotalPoolLiquidity(s.Ctx)
s.Require().Equal(tc.expectedDYM, poolCoins.AmountOf("adym"))
s.Require().Equal(tc.expectedTokens, poolCoins.AmountOf(rollappDenom))

// Assert pool price
lastIROPrice := plan.SpotPrice()
price, err := pool.SpotPrice(s.Ctx, "adym", rollappDenom)
s.Require().NoError(err)
s.Require().Equal(lastIROPrice, price)

// Assert incentives
gauges, err := s.App.IncentivesKeeper.GetGaugesForDenom(s.Ctx, gammtypes.GetPoolShareDenom(poolId))
s.Require().NoError(err)
found := false
var gauge incentivestypes.Gauge
for _, g := range gauges {
if !g.IsPerpetual {
found = true
gauge = g
break
}
}
s.Require().True(found)

// expected tokens for incentives:
// raisedDYM - poolCoins
// unallocatedTokens - poolCoins
expectedIncentives := sdk.NewCoins(raisedDYM, sdk.NewCoin(rollappDenom, unallocatedTokensAmt)).Sub(poolCoins...)
s.Assert().Equal(expectedIncentives, gauge.Coins)
})
}
s.Require().True(found)

// only few RA tokens left, so the pool should be quite small
// most of the dym should be as incentive
s.T().Log("Pool coins", pool.GetTotalPoolLiquidity(s.Ctx))
s.T().Log("Gauge coins", gauge.Coins)
s.Require().True(pool.GetTotalPoolLiquidity(s.Ctx).AmountOf("adym").LT(gauge.Coins.AmountOf("adym")))
s.Require().Equal(pool.GetTotalPoolLiquidity(s.Ctx).AmountOf(plan.SettledDenom), amt.Sub(buyAmt))
}
Loading

0 comments on commit 198f418

Please sign in to comment.