Skip to content

Commit

Permalink
feat(iro): scale the bonding curve (#1247)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsitrin authored Sep 27, 2024
1 parent 1959cdd commit f88af02
Show file tree
Hide file tree
Showing 25 changed files with 896 additions and 560 deletions.
1 change: 0 additions & 1 deletion app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ type AppKeepers struct {
ParamsKeeper paramskeeper.Keeper
IBCKeeper *ibckeeper.Keeper // IBC Keeper must be a pointer in the app, so we can SetRouter on it correctly
TransferStack ibcporttypes.IBCModule
ICS4Wrapper ibcporttypes.ICS4Wrapper
delayedAckMiddleware *delayedackmodule.IBCMiddleware
EvidenceKeeper evidencekeeper.Keeper
TransferKeeper ibctransferkeeper.Keeper
Expand Down
2 changes: 1 addition & 1 deletion ibctesting/genesis_transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (s *transferGenesisSuite) TestNoIRO() {
// In this case, the genesis transfer is required
// regular transfers should fail until the genesis transfer is done
func (s *transferGenesisSuite) TestIRO() {
amt := math.NewIntFromUint64(10000000000000000000)
amt := math.NewIntFromUint64(1_000_000).MulRaw(1e18)
rollapp := s.hubApp().RollappKeeper.MustGetRollapp(s.hubCtx(), rollappChainID())

denom := rollapp.GenesisInfo.NativeDenom.Base
Expand Down
33 changes: 19 additions & 14 deletions proto/dymensionxyz/dymension/iro/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,44 @@ import "cosmos/base/v1beta1/coin.proto";

option go_package = "github.com/dymensionxyz/dymension/v3/x/iro/types";

// Query defines the gRPC querier service.
// Query defines the gRPC querier service for the IRO module.
service Query {
// Param queries the parameters of the module.
// Params queries the parameters of the IRO module.
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/dymensionxyz/dymension/iro/params";
}

// Plans
// QueryPlans retrieves all available plans.
rpc QueryPlans(QueryPlansRequest) returns (QueryPlansResponse) {
option (google.api.http).get = "/dymensionxyz/dymension/iro/plans";
}

// Plan returns the plan for the specified plan ID.
// QueryPlan retrieves the plan for the specified plan ID.
rpc QueryPlan(QueryPlanRequest) returns (QueryPlanResponse) {
option (google.api.http).get =
"/dymensionxyz/dymension/iro/plans/{plan_id}";
}

// PlanByRollapp returns the plans for the specified rollapp ID.
// QueryPlanByRollapp retrieves the plans for the specified rollapp ID.
rpc QueryPlanByRollapp(QueryPlanByRollappRequest)
returns (QueryPlanByRollappResponse) {
option (google.api.http).get =
"/dymensionxyz/dymension/iro/plans_by_rollapp/{rollapp_id}";
}

// Price returns the current price for 1 IRO token for the specified plan ID.
rpc QueryPrice(QueryPriceRequest) returns (QueryPriceResponse) {
// QuerySpotPrice retrieves the current spot price for the specified plan ID.
// The result is the price of 1 IRO token (not iro's base denom)
rpc QuerySpotPrice(QuerySpotPriceRequest) returns (QuerySpotPriceResponse) {
option (google.api.http).get =
"/dymensionxyz/dymension/iro/price/{plan_id}";
}

// Cost returns the expected cost for buying or selling the specified amount
// of shares.
// QueryCost retrieves the expected cost for buying or selling the specified amount of shares.
rpc QueryCost(QueryCostRequest) returns (QueryCostResponse) {
option (google.api.http).get = "/dymensionxyz/dymension/iro/cost/{plan_id}";
}

// Claimed returns the claimed amount thus far for the specified plan ID.
// QueryClaimed retrieves the claimed amount thus far for the specified plan ID.
rpc QueryClaimed(QueryClaimedRequest) returns (QueryClaimedResponse) {
option (google.api.http).get =
"/dymensionxyz/dymension/iro/claimed/{plan_id}";
Expand Down Expand Up @@ -84,11 +84,16 @@ message QueryPlanByRollappRequest { string rollapp_id = 1; }
// Query/QueryPlanByRollapp RPC method.
message QueryPlanByRollappResponse { Plan plan = 1; }

// QueryPriceRequest is the request type for the Query/QueryPrice RPC method.
message QueryPriceRequest { string plan_id = 1; }
// QuerySpotPriceRequest is the request type for the Query/QuerySpotPrice RPC method.
message QuerySpotPriceRequest { string plan_id = 1; }

// QueryPriceResponse is the response type for the Query/QueryPrice RPC method.
message QueryPriceResponse { cosmos.base.v1beta1.Coin price = 1; }
// QuerySpotPriceResponse is the response type for the Query/QuerySpotPrice RPC method.
message QuerySpotPriceResponse {
string price = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

// QueryCostRequest is the request type for the Query/QueryCost RPC method.
message QueryCostRequest {
Expand Down
8 changes: 4 additions & 4 deletions proto/dymensionxyz/dymension/iro/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ message MsgBuy {
(gogoproto.nullable) = false
];

// The expected output amount.
string expected_out_amount = 4 [
// The maximum cost this buy action can incur.
string max_cost_amount = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
Expand All @@ -117,8 +117,8 @@ message MsgSell {
(gogoproto.nullable) = false
];

// The expected output amount.
string expected_out_amount = 4 [
// The minimum income this sell action can incur.
string min_income_amount = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
Expand Down
6 changes: 3 additions & 3 deletions x/iro/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func GetQueryCmd() *cobra.Command {
CmdQueryPlans(),
CmdQueryPlan(),
CmdQueryPlanByRollapp(),
CmdQueryPrice(),
CmdQuerySpotPrice(),
CmdQueryCost(),
CmdQueryClaimed(),
)
Expand Down Expand Up @@ -109,7 +109,7 @@ func CmdQueryPlanByRollapp() *cobra.Command {
return cmd
}

func CmdQueryPrice() *cobra.Command {
func CmdQuerySpotPrice() *cobra.Command {
cmd := &cobra.Command{
Use: "price [plan-id]",
Short: "Query the current price for 1 IRO token for a specific IRO plan",
Expand All @@ -121,7 +121,7 @@ func CmdQueryPrice() *cobra.Command {
}
queryClient := types.NewQueryClient(clientCtx)

res, err := queryClient.QueryPrice(cmd.Context(), &types.QueryPriceRequest{PlanId: args[0]})
res, err := queryClient.QuerySpotPrice(cmd.Context(), &types.QuerySpotPriceRequest{PlanId: args[0]})
if err != nil {
return err
}
Expand Down
22 changes: 11 additions & 11 deletions x/iro/cli/tx_trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,32 @@ func createBuySellCmd(use string, short string, isBuy bool) *cobra.Command {

planID := args[0]
argAmount := args[1]
argExpectedOutAmount := args[2]
argExpectedAmount := args[2]

amount, ok := math.NewIntFromString(argAmount)
if !ok {
return fmt.Errorf("invalid amount: %s", argAmount)
}

expectedOutAmount, ok := math.NewIntFromString(argExpectedOutAmount)
expectedAmount, ok := math.NewIntFromString(argExpectedAmount)
if !ok {
return fmt.Errorf("invalid expected out amount: %s", argExpectedOutAmount)
return fmt.Errorf("invalid expected out amount: %s", argExpectedAmount)
}

var msg sdk.Msg
if isBuy {
msg = &types.MsgBuy{
Buyer: clientCtx.GetFromAddress().String(),
PlanId: planID,
Amount: amount,
ExpectedOutAmount: expectedOutAmount,
Buyer: clientCtx.GetFromAddress().String(),
PlanId: planID,
Amount: amount,
MaxCostAmount: expectedAmount,
}
} else {
msg = &types.MsgSell{
Seller: clientCtx.GetFromAddress().String(),
PlanId: planID,
Amount: amount,
ExpectedOutAmount: expectedOutAmount,
Seller: clientCtx.GetFromAddress().String(),
PlanId: planID,
Amount: amount,
MinIncomeAmount: expectedAmount,
}
}

Expand Down
3 changes: 3 additions & 0 deletions x/iro/keeper/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func (m msgServer) Claim(ctx context.Context, req *types.MsgClaim) (*types.MsgCl
}

// Claim claims the FUT token for the real RA token
//
// This function allows a user to claim their RA tokens by burning their FUT tokens.
// It burns *all* the FUT tokens the claimer has, and sends the equivalent amount of RA tokens to the claimer.
func (k Keeper) Claim(ctx sdk.Context, planId string, claimer sdk.AccAddress) error {
plan, found := k.GetPlan(ctx, planId)
if !found {
Expand Down
56 changes: 56 additions & 0 deletions x/iro/keeper/claim_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package keeper_test

import (
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/dymension/v3/testutil/sample"
"github.com/dymensionxyz/dymension/v3/x/iro/types"
)

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

startTime := time.Now()
amt := sdk.NewInt(1_000_000).MulRaw(1e18)

rollapp, _ := s.App.RollappKeeper.GetRollapp(s.Ctx, rollappId)
planId, err := k.CreatePlan(s.Ctx, amt, startTime, startTime.Add(time.Hour), rollapp, curve, incentives)
s.Require().NoError(err)
planDenom := k.MustGetPlan(s.Ctx, planId).TotalAllocation.Denom
balance := s.App.BankKeeper.GetBalance(s.Ctx, k.AK.GetModuleAddress(types.ModuleName), planDenom)
s.Require().Equal(amt, balance.Amount)

claimer := sample.Acc()
// buy some tokens
s.Ctx = s.Ctx.WithBlockTime(startTime.Add(time.Minute))
soldAmt := sdk.NewInt(1_000).MulRaw(1e18)
s.BuySomeTokens(planId, claimer, soldAmt)

// claim should fail as not settled
err = k.Claim(s.Ctx, planId, claimer)
s.Require().Error(err)

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

// claim should fail as no balance available (random address)
err = k.Claim(s.Ctx, planId, sample.Acc())
s.Require().Error(err)

// fund. claim should succeed
err = k.Claim(s.Ctx, planId, claimer)
s.Require().NoError(err)

// assert claimed amt
balance = s.App.BankKeeper.GetBalance(s.Ctx, k.AK.GetModuleAddress(types.ModuleName), planDenom)
s.Require().True(balance.IsZero())
balance = s.App.BankKeeper.GetBalance(s.Ctx, claimer, rollappDenom)
s.Require().Equal(soldAmt, balance.Amount)
}
14 changes: 13 additions & 1 deletion x/iro/keeper/create_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import (
)

// This function is used to create a new plan for a rollapp.
// Validations on the request:
// Non stateful validation happens on the req.ValidateBasic() method
// Stateful validations on the request:
// - The rollapp must exist, with no IRO plan
// - The rollapp must be owned by the creator of the plan
// - The rollapp PreLaunchTime must be in the future
Expand Down Expand Up @@ -74,6 +75,13 @@ func (m msgServer) CreatePlan(goCtx context.Context, req *types.MsgCreatePlan) (
}

// CreatePlan creates a new IRO plan for a rollapp
// This function performs the following steps:
// 1. Sets the IRO plan to the rollapp with the specified pre-launch time.
// 2. Mints the allocated amount of tokens for the rollapp.
// 3. Creates a new plan with the provided parameters and validates it.
// 4. Creates a new module account for the IRO plan.
// 5. Charges the creation fee from the rollapp owner to the plan's module account.
// 6. Stores the plan in the keeper.
func (k Keeper) CreatePlan(ctx sdk.Context, allocatedAmount math.Int, start, preLaunchTime time.Time, rollapp rollapptypes.Rollapp, curve types.BondingCurve, incentivesParams types.IncentivePlanParams) (string, error) {
err := k.rk.SetIROPlanToRollapp(ctx, &rollapp, preLaunchTime)
if err != nil {
Expand All @@ -86,6 +94,10 @@ func (k Keeper) CreatePlan(ctx sdk.Context, allocatedAmount math.Int, start, pre
}

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)
}

// Create a new module account for the IRO plan
_, err = k.CreateModuleAccountForPlan(ctx, plan)
if err != nil {
Expand Down
15 changes: 9 additions & 6 deletions x/iro/keeper/create_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,27 @@ func (s *KeeperTestSuite) TestValidateRollappPreconditions_MissingGenesisInfo()
curve := types.DefaultBondingCurve()
incentives := types.DefaultIncentivePlanParams()

allocation := sdk.NewInt(100).MulRaw(1e18)

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

// test missing genesis checksum
rollapp.GenesisInfo.GenesisChecksum = ""
s.App.RollappKeeper.SetRollapp(s.Ctx, rollapp)
_, err := k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
_, err := k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
s.Require().Error(err)

// test already launched
rollapp.GenesisInfo.GenesisChecksum = "aaaaaa"
rollapp.Launched = true
s.App.RollappKeeper.SetRollapp(s.Ctx, rollapp)
_, err = k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
_, err = k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
s.Require().Error(err)
rollapp.Launched = false

// add check for happy path
s.App.RollappKeeper.SetRollapp(s.Ctx, rollapp)
_, err = k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
_, err = k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
s.Require().NoError(err)
}

Expand All @@ -44,18 +46,19 @@ func (s *KeeperTestSuite) TestCreatePlan() {
k := s.App.IROKeeper
curve := types.DefaultBondingCurve()
incentives := types.DefaultIncentivePlanParams()
allocation := sdk.NewInt(100).MulRaw(1e18)

rollapp, _ := s.App.RollappKeeper.GetRollapp(s.Ctx, rollappId)
planId, err := k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
planId, err := k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
s.Require().NoError(err)

// creating a a plan for same rollapp should fail
_, err = k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
_, err = k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp, curve, incentives)
s.Require().Error(err)

// create plan for different rollappID. test last planId increases
rollapp2, _ := s.App.RollappKeeper.GetRollapp(s.Ctx, rollappId2)
planId2, err := k.CreatePlan(s.Ctx, sdk.NewInt(100), time.Now(), time.Now().Add(time.Hour), rollapp2, curve, incentives)
planId2, err := k.CreatePlan(s.Ctx, allocation, time.Now(), time.Now().Add(time.Hour), rollapp2, curve, incentives)
s.Require().NoError(err)
s.Require().Greater(planId2, planId)

Expand Down
2 changes: 1 addition & 1 deletion x/iro/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (suite *KeeperTestSuite) SetupTest() {

// BuySomeTokens buys some tokens from the plan
func (suite *KeeperTestSuite) BuySomeTokens(planId string, buyer sdk.AccAddress, amt math.Int) {
maxAmt := sdk.NewInt(1_000_000_000)
maxAmt := sdk.NewInt(1_000_000_000).MulRaw(1e18)
suite.FundAcc(buyer, sdk.NewCoins(sdk.NewCoin("adym", amt.MulRaw(10)))) // 10 times the amount to buy, for buffer and fees
err := suite.App.IROKeeper.Buy(suite.Ctx, planId, buyer, amt, maxAmt)
suite.Require().NoError(err)
Expand Down
11 changes: 4 additions & 7 deletions x/iro/keeper/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ func (k Keeper) QueryPlans(goCtx context.Context, req *types.QueryPlansRequest)
return &types.QueryPlansResponse{Plans: k.GetAllPlans(ctx)}, nil
}

// QueryPrice implements types.QueryServer.
func (k Keeper) QueryPrice(goCtx context.Context, req *types.QueryPriceRequest) (*types.QueryPriceResponse, error) {
// QuerySpotPrice implements types.QueryServer.
func (k Keeper) QuerySpotPrice(goCtx context.Context, req *types.QuerySpotPriceRequest) (*types.QuerySpotPriceResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
Expand All @@ -116,10 +116,7 @@ func (k Keeper) QueryPrice(goCtx context.Context, req *types.QueryPriceRequest)
return nil, status.Error(codes.NotFound, "plan not found")
}

price := plan.BondingCurve.SpotPrice(plan.SoldAmt).TruncateInt()
coin := sdk.NewCoin(appparams.BaseDenom, price)

return &types.QueryPriceResponse{
Price: &coin,
return &types.QuerySpotPriceResponse{
Price: plan.SpotPrice(),
}, nil
}
Loading

0 comments on commit f88af02

Please sign in to comment.