mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
Add E2E Swap Support (#959)
* add message types for swaps * add tx client commands * add test coverage for swap message deadlines * start handler swap tests, export handler result message event into private method, add stubbed keeper methods * add initial swap implementation to get handler tests passing; adds event specific for trades * add handler acceptance test for slippage in exact input and exact output swaps * implement slippage limit for swap keeper methods * add tests to ensure a user can only swap spendable coins * test pool not found, panic on invalid pool, and panic when module account does not have enough funds * validate that the exact output when using for exact swaps is less than the pool liquidity * nit: long line * add validation that swap output is greater than zero * add rest txs for swap messages * nit: lints * dry up swap keeper methods * from pr feedback - spelling and increase clairty around the output amount of a swap rounding to zero
This commit is contained in:
parent
880b9a2cc5
commit
20437a91fb
@ -8,20 +8,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttributeKeyDepositor = types.AttributeKeyDepositor
|
AttributeKeyDepositor = types.AttributeKeyDepositor
|
||||||
AttributeKeyOwner = types.AttributeKeyOwner
|
AttributeKeyExactDirection = types.AttributeKeyExactDirection
|
||||||
AttributeKeyPoolID = types.AttributeKeyPoolID
|
AttributeKeyFeePaid = types.AttributeKeyFeePaid
|
||||||
AttributeKeyShares = types.AttributeKeyShares
|
AttributeKeyOwner = types.AttributeKeyOwner
|
||||||
AttributeValueCategory = types.AttributeValueCategory
|
AttributeKeyPoolID = types.AttributeKeyPoolID
|
||||||
DefaultParamspace = types.DefaultParamspace
|
AttributeKeyRequester = types.AttributeKeyRequester
|
||||||
EventTypeSwapDeposit = types.EventTypeSwapDeposit
|
AttributeKeyShares = types.AttributeKeyShares
|
||||||
EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
|
AttributeKeySwapInput = types.AttributeKeySwapInput
|
||||||
ModuleAccountName = types.ModuleAccountName
|
AttributeKeySwapOutput = types.AttributeKeySwapOutput
|
||||||
ModuleName = types.ModuleName
|
AttributeValueCategory = types.AttributeValueCategory
|
||||||
QuerierRoute = types.QuerierRoute
|
DefaultParamspace = types.DefaultParamspace
|
||||||
QueryGetParams = types.QueryGetParams
|
EventTypeSwapDeposit = types.EventTypeSwapDeposit
|
||||||
RouterKey = types.RouterKey
|
EventTypeSwapTrade = types.EventTypeSwapTrade
|
||||||
StoreKey = types.StoreKey
|
EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
|
||||||
|
ModuleAccountName = types.ModuleAccountName
|
||||||
|
ModuleName = types.ModuleName
|
||||||
|
QuerierRoute = types.QuerierRoute
|
||||||
|
QueryGetParams = types.QueryGetParams
|
||||||
|
RouterKey = types.RouterKey
|
||||||
|
StoreKey = types.StoreKey
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -39,6 +45,8 @@ var (
|
|||||||
NewDenominatedPoolWithExistingShares = types.NewDenominatedPoolWithExistingShares
|
NewDenominatedPoolWithExistingShares = types.NewDenominatedPoolWithExistingShares
|
||||||
NewGenesisState = types.NewGenesisState
|
NewGenesisState = types.NewGenesisState
|
||||||
NewMsgDeposit = types.NewMsgDeposit
|
NewMsgDeposit = types.NewMsgDeposit
|
||||||
|
NewMsgSwapExactForTokens = types.NewMsgSwapExactForTokens
|
||||||
|
NewMsgSwapForExactTokens = types.NewMsgSwapForExactTokens
|
||||||
NewMsgWithdraw = types.NewMsgWithdraw
|
NewMsgWithdraw = types.NewMsgWithdraw
|
||||||
NewParams = types.NewParams
|
NewParams = types.NewParams
|
||||||
NewPoolRecord = types.NewPoolRecord
|
NewPoolRecord = types.NewPoolRecord
|
||||||
@ -72,18 +80,20 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Keeper = keeper.Keeper
|
Keeper = keeper.Keeper
|
||||||
AccountKeeper = types.AccountKeeper
|
AccountKeeper = types.AccountKeeper
|
||||||
AllowedPool = types.AllowedPool
|
AllowedPool = types.AllowedPool
|
||||||
AllowedPools = types.AllowedPools
|
AllowedPools = types.AllowedPools
|
||||||
BasePool = types.BasePool
|
BasePool = types.BasePool
|
||||||
DenominatedPool = types.DenominatedPool
|
DenominatedPool = types.DenominatedPool
|
||||||
GenesisState = types.GenesisState
|
GenesisState = types.GenesisState
|
||||||
MsgDeposit = types.MsgDeposit
|
MsgDeposit = types.MsgDeposit
|
||||||
MsgWithDeadline = types.MsgWithDeadline
|
MsgSwapExactForTokens = types.MsgSwapExactForTokens
|
||||||
MsgWithdraw = types.MsgWithdraw
|
MsgSwapForExactTokens = types.MsgSwapForExactTokens
|
||||||
Params = types.Params
|
MsgWithDeadline = types.MsgWithDeadline
|
||||||
PoolRecord = types.PoolRecord
|
MsgWithdraw = types.MsgWithdraw
|
||||||
ShareRecord = types.ShareRecord
|
Params = types.Params
|
||||||
SupplyKeeper = types.SupplyKeeper
|
PoolRecord = types.PoolRecord
|
||||||
|
ShareRecord = types.ShareRecord
|
||||||
|
SupplyKeeper = types.SupplyKeeper
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,8 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
|||||||
swapTxCmd.AddCommand(flags.PostCommands(
|
swapTxCmd.AddCommand(flags.PostCommands(
|
||||||
getCmdDeposit(cdc),
|
getCmdDeposit(cdc),
|
||||||
getCmdWithdraw(cdc),
|
getCmdWithdraw(cdc),
|
||||||
|
getCmdSwapExactForTokens(cdc),
|
||||||
|
getCmdSwapForExactTokens(cdc),
|
||||||
)...)
|
)...)
|
||||||
|
|
||||||
return swapTxCmd
|
return swapTxCmd
|
||||||
@ -123,3 +125,89 @@ func getCmdWithdraw(cdc *codec.Codec) *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCmdSwapExactForTokens(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "swap-exact-for-tokens [exactCoinA] [coinB] [slippage] [deadline]",
|
||||||
|
Short: "swap an exact amount of token a for token b",
|
||||||
|
Example: fmt.Sprintf(
|
||||||
|
`%s tx %s swap-exact-for-tokens 1000000ukava 5000000usdx 0.01 1624224736 --from <key>`, version.ClientName, types.ModuleName,
|
||||||
|
),
|
||||||
|
Args: cobra.ExactArgs(4),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
|
||||||
|
exactTokenA, err := sdk.ParseCoin(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenB, err := sdk.ParseCoin(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slippage, err := sdk.NewDecFromStr(args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, err := strconv.ParseInt(args[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapExactForTokens(cliCtx.GetFromAddress(), exactTokenA, tokenB, slippage, deadline)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdSwapForExactTokens(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "swap-for-exact-tokens [coinA] [exactCoinB] [slippage] [deadline]",
|
||||||
|
Short: "swap token a for exact amount of token b",
|
||||||
|
Example: fmt.Sprintf(
|
||||||
|
`%s tx %s swap-for-exact-tokens 1000000ukava 5000000usdx 0.01 1624224736 --from <key>`, version.ClientName, types.ModuleName,
|
||||||
|
),
|
||||||
|
Args: cobra.ExactArgs(4),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
|
||||||
|
tokenA, err := sdk.ParseCoin(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exactTokenB, err := sdk.ParseCoin(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slippage, err := sdk.NewDecFromStr(args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, err := strconv.ParseInt(args[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapForExactTokens(cliCtx.GetFromAddress(), tokenA, exactTokenB, slippage, deadline)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -104,7 +104,7 @@ func queryPoolHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
|||||||
poolName = strings.TrimSpace(x)
|
poolName = strings.TrimSpace(x)
|
||||||
}
|
}
|
||||||
if len(poolName) == 0 {
|
if len(poolName) == 0 {
|
||||||
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("must specify pool param"))
|
rest.WriteErrorResponse(w, http.StatusBadRequest, "must specify pool param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,3 +40,23 @@ type PostCreateWithdrawReq struct {
|
|||||||
MinTokenB sdk.Coin `json:"token_b" yaml:"token_b"`
|
MinTokenB sdk.Coin `json:"token_b" yaml:"token_b"`
|
||||||
Deadline int64 `json:"deadline" yaml:"deadline"`
|
Deadline int64 `json:"deadline" yaml:"deadline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostCreateSwapExactForTokensReq trades an exact coinA for coinB
|
||||||
|
type PostCreateSwapExactForTokensReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Requester sdk.AccAddress `json:"requester" yaml:"requester"`
|
||||||
|
ExactTokenA sdk.Coin `json:"exact_token_a" yaml:"exact_token_a"`
|
||||||
|
TokenB sdk.Coin `json:"token_b" yaml:"token_b"`
|
||||||
|
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
|
||||||
|
Deadline int64 `json:"deadline" yaml:"deadline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostCreateSwapForExactTokensReq trades an exact coinA for coinB
|
||||||
|
type PostCreateSwapForExactTokensReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Requester sdk.AccAddress `json:"requester" yaml:"requester"`
|
||||||
|
TokenA sdk.Coin `json:"exact_token_a" yaml:"exact_token_a"`
|
||||||
|
ExactTokenB sdk.Coin `json:"token_b" yaml:"token_b"`
|
||||||
|
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
|
||||||
|
Deadline int64 `json:"deadline" yaml:"deadline"`
|
||||||
|
}
|
||||||
|
@ -17,6 +17,8 @@ import (
|
|||||||
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
r.HandleFunc(fmt.Sprintf("/%s/deposit", types.ModuleName), postDepositHandlerFn(cliCtx)).Methods("POST")
|
r.HandleFunc(fmt.Sprintf("/%s/deposit", types.ModuleName), postDepositHandlerFn(cliCtx)).Methods("POST")
|
||||||
r.HandleFunc(fmt.Sprintf("/%s/withdraw", types.ModuleName), postWithdrawHandlerFn(cliCtx)).Methods("POST")
|
r.HandleFunc(fmt.Sprintf("/%s/withdraw", types.ModuleName), postWithdrawHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/swapExactForTokens", types.ModuleName), postSwapExactForTokensHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/swapForExactTokens", types.ModuleName), postSwapForExactTokensHandlerFn(cliCtx)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
@ -60,3 +62,45 @@ func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
|||||||
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postSwapExactForTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Decode POST request body
|
||||||
|
var req PostCreateSwapExactForTokensReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.BaseReq = req.BaseReq.Sanitize()
|
||||||
|
if !req.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapExactForTokens(req.Requester, req.ExactTokenA, req.TokenB, req.Slippage, req.Deadline)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postSwapForExactTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Decode POST request body
|
||||||
|
var req PostCreateSwapForExactTokensReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.BaseReq = req.BaseReq.Sanitize()
|
||||||
|
if !req.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapForExactTokens(req.Requester, req.TokenA, req.ExactTokenB, req.Slippage, req.Deadline)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,6 +24,10 @@ func NewHandler(k Keeper) sdk.Handler {
|
|||||||
return handleMsgDeposit(ctx, k, msg)
|
return handleMsgDeposit(ctx, k, msg)
|
||||||
case types.MsgWithdraw:
|
case types.MsgWithdraw:
|
||||||
return handleMsgWithdraw(ctx, k, msg)
|
return handleMsgWithdraw(ctx, k, msg)
|
||||||
|
case types.MsgSwapExactForTokens:
|
||||||
|
return handleMsgSwapExactForTokens(ctx, k, msg)
|
||||||
|
case types.MsgSwapForExactTokens:
|
||||||
|
return handleMsgSwapForExactTokens(ctx, k, msg)
|
||||||
default:
|
default:
|
||||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
||||||
}
|
}
|
||||||
@ -35,17 +39,7 @@ func handleMsgDeposit(ctx sdk.Context, k keeper.Keeper, msg types.MsgDeposit) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.EventManager().EmitEvent(
|
return resultWithMsgSender(ctx, msg.Depositor), nil
|
||||||
sdk.NewEvent(
|
|
||||||
sdk.EventTypeMessage,
|
|
||||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
|
||||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &sdk.Result{
|
|
||||||
Events: ctx.EventManager().Events(),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) (*sdk.Result, error) {
|
func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) (*sdk.Result, error) {
|
||||||
@ -53,15 +47,35 @@ func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resultWithMsgSender(ctx, msg.From), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgSwapExactForTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgSwapExactForTokens) (*sdk.Result, error) {
|
||||||
|
if err := k.SwapExactForTokens(ctx, msg.Requester, msg.ExactTokenA, msg.TokenB, msg.Slippage); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultWithMsgSender(ctx, msg.Requester), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgSwapForExactTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgSwapForExactTokens) (*sdk.Result, error) {
|
||||||
|
if err := k.SwapForExactTokens(ctx, msg.Requester, msg.TokenA, msg.ExactTokenB, msg.Slippage); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultWithMsgSender(ctx, msg.Requester), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultWithMsgSender(ctx sdk.Context, sender sdk.AccAddress) *sdk.Result {
|
||||||
ctx.EventManager().EmitEvent(
|
ctx.EventManager().EmitEvent(
|
||||||
sdk.NewEvent(
|
sdk.NewEvent(
|
||||||
sdk.EventTypeMessage,
|
sdk.EventTypeMessage,
|
||||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
|
sdk.NewAttribute(sdk.AttributeKeySender, sender.String()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &sdk.Result{
|
return &sdk.Result{
|
||||||
Events: ctx.EventManager().Events(),
|
Events: ctx.EventManager().Events(),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
@ -349,6 +349,226 @@ func (suite *handlerTestSuite) TestWithdraw_DeadlineExceeded() {
|
|||||||
suite.Nil(res)
|
suite.Nil(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapExactForTokens() {
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
err := suite.CreatePool(reserves)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
swapInput := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
swapMsg := swap.NewMsgSwapExactForTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
swapInput,
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
time.Now().Add(10*time.Minute).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
res, err := suite.handler(ctx, swapMsg)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
expectedSwapOutput := sdk.NewCoin("usdx", sdk.NewInt(4980034))
|
||||||
|
|
||||||
|
suite.AccountBalanceEqual(requester, balance.Sub(sdk.NewCoins(swapInput)).Add(expectedSwapOutput))
|
||||||
|
suite.ModuleAccountBalanceEqual(reserves.Add(swapInput).Sub(sdk.NewCoins(expectedSwapOutput)))
|
||||||
|
suite.PoolLiquidityEqual(reserves.Add(swapInput).Sub(sdk.NewCoins(expectedSwapOutput)))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, requester.GetAddress().String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
bank.EventTypeTransfer,
|
||||||
|
sdk.NewAttribute(bank.AttributeKeyRecipient, swapModuleAccountAddress.String()),
|
||||||
|
sdk.NewAttribute(bank.AttributeKeySender, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, swapInput.String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
bank.EventTypeTransfer,
|
||||||
|
sdk.NewAttribute(bank.AttributeKeyRecipient, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(bank.AttributeKeySender, swapModuleAccountAddress.String()),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedSwapOutput.String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
swap.EventTypeSwapTrade,
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID("ukava", "usdx")),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyRequester, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeySwapInput, swapInput.String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeySwapOutput, expectedSwapOutput.String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyFeePaid, "3000ukava"),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyExactDirection, "input"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapExactForTokens_SlippageFailure() {
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
err := suite.CreatePool(reserves)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
swapInput := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
swapMsg := swap.NewMsgSwapExactForTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
swapInput,
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5030338)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
time.Now().Add(10*time.Minute).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
res, err := suite.handler(ctx, swapMsg)
|
||||||
|
suite.EqualError(err, "slippage exceeded: slippage 0.010000123252155223 > limit 0.010000000000000000")
|
||||||
|
suite.Nil(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapExactForTokens_DeadlineExceeded() {
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.CreateAccount(balance)
|
||||||
|
|
||||||
|
swapMsg := swap.NewMsgSwapExactForTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(5e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(25e5)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
suite.Ctx.BlockTime().Add(-1*time.Second).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
res, err := suite.handler(suite.Ctx, swapMsg)
|
||||||
|
suite.EqualError(err, fmt.Sprintf("deadline exceeded: block time %d >= deadline %d", suite.Ctx.BlockTime().Unix(), swapMsg.GetDeadline().Unix()))
|
||||||
|
suite.Nil(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapForExactTokens() {
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
err := suite.CreatePool(reserves)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
swapOutput := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
swapMsg := swap.NewMsgSwapForExactTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
|
||||||
|
swapOutput,
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
time.Now().Add(10*time.Minute).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
res, err := suite.handler(ctx, swapMsg)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
expectedSwapInput := sdk.NewCoin("ukava", sdk.NewInt(1004015))
|
||||||
|
|
||||||
|
suite.AccountBalanceEqual(requester, balance.Sub(sdk.NewCoins(expectedSwapInput)).Add(swapOutput))
|
||||||
|
suite.ModuleAccountBalanceEqual(reserves.Add(expectedSwapInput).Sub(sdk.NewCoins(swapOutput)))
|
||||||
|
suite.PoolLiquidityEqual(reserves.Add(expectedSwapInput).Sub(sdk.NewCoins(swapOutput)))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, requester.GetAddress().String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
bank.EventTypeTransfer,
|
||||||
|
sdk.NewAttribute(bank.AttributeKeyRecipient, swapModuleAccountAddress.String()),
|
||||||
|
sdk.NewAttribute(bank.AttributeKeySender, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedSwapInput.String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
bank.EventTypeTransfer,
|
||||||
|
sdk.NewAttribute(bank.AttributeKeyRecipient, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(bank.AttributeKeySender, swapModuleAccountAddress.String()),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, swapOutput.String()),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.EventsContains(res.Events, sdk.NewEvent(
|
||||||
|
swap.EventTypeSwapTrade,
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID("ukava", "usdx")),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyRequester, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeySwapInput, expectedSwapInput.String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeySwapOutput, swapOutput.String()),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyFeePaid, "3013ukava"),
|
||||||
|
sdk.NewAttribute(swap.AttributeKeyExactDirection, "output"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapForExactTokens_SlippageFailure() {
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
err := suite.CreatePool(reserves)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
swapOutput := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
swapMsg := swap.NewMsgSwapForExactTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(990991)),
|
||||||
|
swapOutput,
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
time.Now().Add(10*time.Minute).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
res, err := suite.handler(ctx, swapMsg)
|
||||||
|
suite.EqualError(err, "slippage exceeded: slippage 0.010000979019022939 > limit 0.010000000000000000")
|
||||||
|
suite.Nil(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *handlerTestSuite) TestSwapForExactTokens_DeadlineExceeded() {
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.CreateAccount(balance)
|
||||||
|
|
||||||
|
swapMsg := swap.NewMsgSwapForExactTokens(
|
||||||
|
requester.GetAddress(),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(5e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(25e5)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
suite.Ctx.BlockTime().Add(-1*time.Second).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
res, err := suite.handler(suite.Ctx, swapMsg)
|
||||||
|
suite.EqualError(err, fmt.Sprintf("deadline exceeded: block time %d >= deadline %d", suite.Ctx.BlockTime().Unix(), swapMsg.GetDeadline().Unix()))
|
||||||
|
suite.Nil(res)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *handlerTestSuite) TestInvalidMsg() {
|
func (suite *handlerTestSuite) TestInvalidMsg() {
|
||||||
res, err := suite.handler(suite.Ctx, sdk.NewTestMsg())
|
res, err := suite.handler(suite.Ctx, sdk.NewTestMsg())
|
||||||
suite.Nil(res)
|
suite.Nil(res)
|
||||||
|
@ -61,6 +61,11 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
|
|||||||
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSwapFee returns the swap fee set in the module parameters
|
||||||
|
func (k Keeper) GetSwapFee(ctx sdk.Context) sdk.Dec {
|
||||||
|
return k.GetParams(ctx).SwapFee
|
||||||
|
}
|
||||||
|
|
||||||
// GetPool retrieves a pool record from the store
|
// GetPool retrieves a pool record from the store
|
||||||
func (k Keeper) GetPool(ctx sdk.Context, poolID string) (types.PoolRecord, bool) {
|
func (k Keeper) GetPool(ctx sdk.Context, poolID string) (types.PoolRecord, bool) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
|
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
|
||||||
@ -174,7 +179,7 @@ func (k Keeper) IterateDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllDepositorShares returns all depositor share records from the store for a specific address
|
// GetAllDepositorSharesByOwner returns all depositor share records from the store for a specific address
|
||||||
func (k Keeper) GetAllDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddress) (records types.ShareRecords) {
|
func (k Keeper) GetAllDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddress) (records types.ShareRecords) {
|
||||||
k.IterateDepositorSharesByOwner(ctx, owner, func(record types.ShareRecord) bool {
|
k.IterateDepositorSharesByOwner(ctx, owner, func(record types.ShareRecord) bool {
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
@ -69,6 +69,17 @@ func (suite keeperTestSuite) TestParams_Persistance() {
|
|||||||
suite.Equal(keeper.GetParams(suite.Ctx), params)
|
suite.Equal(keeper.GetParams(suite.Ctx), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite keeperTestSuite) TestParams_GetSwapFee() {
|
||||||
|
keeper := suite.Keeper
|
||||||
|
|
||||||
|
params := types.Params{
|
||||||
|
SwapFee: sdk.MustNewDecFromStr("0.00333"),
|
||||||
|
}
|
||||||
|
keeper.SetParams(suite.Ctx, params)
|
||||||
|
|
||||||
|
suite.Equal(keeper.GetSwapFee(suite.Ctx), params.SwapFee)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *keeperTestSuite) TestPool_Persistance() {
|
func (suite *keeperTestSuite) TestPool_Persistance() {
|
||||||
reserves := sdk.NewCoins(
|
reserves := sdk.NewCoins(
|
||||||
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
122
x/swap/keeper/swap.go
Normal file
122
x/swap/keeper/swap.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/swap/types"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SwapExactForTokens swaps an exact coin a input for a coin b output
|
||||||
|
func (k *Keeper) SwapExactForTokens(ctx sdk.Context, requester sdk.AccAddress, exactCoinA, coinB sdk.Coin, slippageLimit sdk.Dec) error {
|
||||||
|
poolID, pool, err := k.loadPool(ctx, exactCoinA.Denom, coinB.Denom)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
swapOutput, feePaid := pool.SwapWithExactInput(exactCoinA, k.GetSwapFee(ctx))
|
||||||
|
if swapOutput.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(types.ErrInsufficientLiquidity, "swap output rounds to zero, increase input amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
priceChange := swapOutput.Amount.ToDec().Quo(coinB.Amount.ToDec())
|
||||||
|
if err := k.assertSlippageWithinLimit(priceChange, slippageLimit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.commitSwap(ctx, poolID, pool, requester, exactCoinA, swapOutput, feePaid, "input"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwapForExactTokens swaps a coin a input for an exact coin b output
|
||||||
|
func (k *Keeper) SwapForExactTokens(ctx sdk.Context, requester sdk.AccAddress, coinA, exactCoinB sdk.Coin, slippageLimit sdk.Dec) error {
|
||||||
|
poolID, pool, err := k.loadPool(ctx, coinA.Denom, exactCoinB.Denom)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exactCoinB.Amount.GTE(pool.Reserves().AmountOf(exactCoinB.Denom)) {
|
||||||
|
return sdkerrors.Wrapf(
|
||||||
|
types.ErrInsufficientLiquidity,
|
||||||
|
"output %s >= pool reserves %s", exactCoinB.Amount.String(), pool.Reserves().AmountOf(exactCoinB.Denom).String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
swapInput, feePaid := pool.SwapWithExactOutput(exactCoinB, k.GetSwapFee(ctx))
|
||||||
|
|
||||||
|
priceChange := coinA.Amount.ToDec().Quo(swapInput.Sub(feePaid).Amount.ToDec())
|
||||||
|
if err := k.assertSlippageWithinLimit(priceChange, slippageLimit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.commitSwap(ctx, poolID, pool, requester, swapInput, exactCoinB, feePaid, "output"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) loadPool(ctx sdk.Context, denomA string, denomB string) (string, *types.DenominatedPool, error) {
|
||||||
|
poolID := types.PoolID(denomA, denomB)
|
||||||
|
|
||||||
|
poolRecord, found := k.GetPool(ctx, poolID)
|
||||||
|
if !found {
|
||||||
|
return poolID, nil, sdkerrors.Wrapf(types.ErrInvalidPool, "pool %s not found", poolID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := types.NewDenominatedPoolWithExistingShares(poolRecord.Reserves(), poolRecord.TotalShares)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("invalid pool %s: %s", poolID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return poolID, pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) assertSlippageWithinLimit(priceChange sdk.Dec, slippageLimit sdk.Dec) error {
|
||||||
|
slippage := sdk.OneDec().Sub(priceChange)
|
||||||
|
if slippage.GT(slippageLimit) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrSlippageExceeded, "slippage %s > limit %s", slippage, slippageLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) commitSwap(
|
||||||
|
ctx sdk.Context,
|
||||||
|
poolID string,
|
||||||
|
pool *types.DenominatedPool,
|
||||||
|
requester sdk.AccAddress,
|
||||||
|
swapInput sdk.Coin,
|
||||||
|
swapOutput sdk.Coin,
|
||||||
|
feePaid sdk.Coin,
|
||||||
|
exactDirection string,
|
||||||
|
) error {
|
||||||
|
k.SetPool(ctx, types.NewPoolRecord(pool))
|
||||||
|
|
||||||
|
if err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, requester, types.ModuleAccountName, sdk.NewCoins(swapInput)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, requester, sdk.NewCoins(swapOutput)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeSwapTrade,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRequester, requester.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapInput, swapInput.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapOutput, swapOutput.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyFeePaid, feePaid.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyExactDirection, exactDirection),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
630
x/swap/keeper/swap_test.go
Normal file
630
x/swap/keeper/swap_test.go
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
"github.com/kava-labs/kava/x/swap/types"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
tmtime "github.com/tendermint/tendermint/types/time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens() {
|
||||||
|
suite.Keeper.SetParams(suite.Ctx, types.Params{
|
||||||
|
SwapFee: sdk.MustNewDecFromStr("0.0025"),
|
||||||
|
})
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
err := suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
expectedOutput := sdk.NewCoin("usdx", sdk.NewInt(4982529))
|
||||||
|
|
||||||
|
suite.AccountBalanceEqual(requester, balance.Sub(sdk.NewCoins(coinA)).Add(expectedOutput))
|
||||||
|
suite.ModuleAccountBalanceEqual(reserves.Add(coinA).Sub(sdk.NewCoins(expectedOutput)))
|
||||||
|
suite.PoolLiquidityEqual(reserves.Add(coinA).Sub(sdk.NewCoins(expectedOutput)))
|
||||||
|
|
||||||
|
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
|
||||||
|
types.EventTypeSwapTrade,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRequester, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapInput, coinA.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapOutput, expectedOutput.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyFeePaid, "2500ukava"),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyExactDirection, "input"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_OutputGreaterThanZero() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("usdx", sdk.NewInt(5))
|
||||||
|
coinB := sdk.NewCoin("ukava", sdk.NewInt(1))
|
||||||
|
|
||||||
|
err := suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("1"))
|
||||||
|
suite.EqualError(err, "insufficient liquidity: swap output rounds to zero, increase input amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_Slippage() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
slippage sdk.Dec
|
||||||
|
fee sdk.Dec
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
// positive slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(2e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(50e6)), sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(50e6)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
// positive slippage with zero slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(2e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(50e6)), sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(50e6)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
// exact zero slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4950495)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4935790)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4705299)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(990099)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(987158)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(941059)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
// slippage failure, zero slippage tolerance
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4950496)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4935793)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4705300)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(990100)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(987159)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(941060)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
// slippage failure, 1 percent slippage
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5000501)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4985647)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4752828)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(1000101)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(997130)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(950565)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
// slippage OK, 1 percent slippage
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5000500)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4985646)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4752827)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(1000100)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(997129)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(950564)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(fmt.Sprintf("coinA=%s coinB=%s slippage=%s fee=%s", tc.coinA, tc.coinB, tc.slippage, tc.fee), func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
suite.Keeper.SetParams(suite.Ctx, types.Params{
|
||||||
|
SwapFee: tc.fee,
|
||||||
|
})
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(100e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapExactForTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, tc.slippage)
|
||||||
|
|
||||||
|
if tc.shouldFail {
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Contains(err.Error(), "slippage exceeded")
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_InsufficientFunds() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
balanceA sdk.Coin
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
}{
|
||||||
|
{"no ukava balance", sdk.NewCoin("ukava", sdk.ZeroInt()), sdk.NewCoin("ukava", sdk.NewInt(100)), sdk.NewCoin("usdx", sdk.NewInt(500))},
|
||||||
|
{"no usdx balance", sdk.NewCoin("usdx", sdk.ZeroInt()), sdk.NewCoin("usdx", sdk.NewInt(500)), sdk.NewCoin("ukava", sdk.NewInt(100))},
|
||||||
|
{"low ukava balance", sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("ukava", sdk.NewInt(1000001)), sdk.NewCoin("usdx", sdk.NewInt(5000000))},
|
||||||
|
{"low ukava balance", sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("usdx", sdk.NewInt(5000001)), sdk.NewCoin("ukava", sdk.NewInt(1000000))},
|
||||||
|
{"large ukava balance difference", sdk.NewCoin("ukava", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6))},
|
||||||
|
{"large usdx balance difference", sdk.NewCoin("usdx", sdk.NewInt(500e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(tc.balanceA)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapExactForTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, sdk.MustNewDecFromStr("0.1"))
|
||||||
|
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds), fmt.Sprintf("got err %s", err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_InsufficientFunds_Vesting() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
balanceA sdk.Coin
|
||||||
|
vestingA sdk.Coin
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
}{
|
||||||
|
{"no ukava balance, vesting only", sdk.NewCoin("ukava", sdk.ZeroInt()), sdk.NewCoin("ukava", sdk.NewInt(100)), sdk.NewCoin("ukava", sdk.NewInt(100)), sdk.NewCoin("usdx", sdk.NewInt(500))},
|
||||||
|
{"no usdx balance, vesting only", sdk.NewCoin("usdx", sdk.ZeroInt()), sdk.NewCoin("usdx", sdk.NewInt(500)), sdk.NewCoin("usdx", sdk.NewInt(500)), sdk.NewCoin("ukava", sdk.NewInt(100))},
|
||||||
|
{"low ukava balance, vesting matches exact", sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("ukava", sdk.NewInt(1)), sdk.NewCoin("ukava", sdk.NewInt(1000001)), sdk.NewCoin("usdx", sdk.NewInt(5000000))},
|
||||||
|
{"low ukava balance, vesting matches exact", sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("usdx", sdk.NewInt(1)), sdk.NewCoin("usdx", sdk.NewInt(5000001)), sdk.NewCoin("ukava", sdk.NewInt(1000000))},
|
||||||
|
{"large ukava balance difference, vesting covers difference", sdk.NewCoin("ukava", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6))},
|
||||||
|
{"large usdx balance difference, vesting covers difference", sdk.NewCoin("usdx", sdk.NewInt(500e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(tc.balanceA)
|
||||||
|
vesting := sdk.NewCoins(tc.vestingA)
|
||||||
|
requester := suite.CreateVestingAccount(balance, vesting)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapExactForTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, sdk.MustNewDecFromStr("0.1"))
|
||||||
|
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds), fmt.Sprintf("got err %s", err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_PoolNotFound() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
suite.Keeper.DeletePool(suite.Ctx, poolID)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
err := suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "invalid pool: pool ukava/usdx not found")
|
||||||
|
|
||||||
|
err = suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinB, coinA, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "invalid pool: pool ukava/usdx not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_PanicOnInvalidPool() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
poolRecord, found := suite.Keeper.GetPool(suite.Ctx, poolID)
|
||||||
|
suite.Require().True(found, "expected pool record to exist")
|
||||||
|
|
||||||
|
poolRecord.TotalShares = sdk.ZeroInt()
|
||||||
|
suite.Keeper.SetPool(suite.Ctx, poolRecord)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
suite.PanicsWithValue("invalid pool ukava/usdx: invalid pool: total shares must be greater than zero", func() {
|
||||||
|
_ = suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected invalid pool record to panic")
|
||||||
|
|
||||||
|
suite.PanicsWithValue("invalid pool ukava/usdx: invalid pool: total shares must be greater than zero", func() {
|
||||||
|
_ = suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinB, coinA, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected invalid pool record to panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapExactForTokens_PanicOnInsufficientModuleAccFunds() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
suite.RemoveCoinsFromModule(sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
))
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
suite.Panics(func() {
|
||||||
|
_ = suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected panic when module account does not have enough funds")
|
||||||
|
|
||||||
|
suite.Panics(func() {
|
||||||
|
_ = suite.Keeper.SwapExactForTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected panic when module account does not have enough funds")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens() {
|
||||||
|
suite.Keeper.SetParams(suite.Ctx, types.Params{
|
||||||
|
SwapFee: sdk.MustNewDecFromStr("0.0025"),
|
||||||
|
})
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
err := suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
expectedInput := sdk.NewCoin("ukava", sdk.NewInt(1003511))
|
||||||
|
|
||||||
|
suite.AccountBalanceEqual(requester, balance.Sub(sdk.NewCoins(expectedInput)).Add(coinB))
|
||||||
|
suite.ModuleAccountBalanceEqual(reserves.Add(expectedInput).Sub(sdk.NewCoins(coinB)))
|
||||||
|
suite.PoolLiquidityEqual(reserves.Add(expectedInput).Sub(sdk.NewCoins(coinB)))
|
||||||
|
|
||||||
|
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
|
||||||
|
types.EventTypeSwapTrade,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRequester, requester.GetAddress().String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapInput, expectedInput.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeySwapOutput, coinB.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyFeePaid, "2509ukava"),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyExactDirection, "output"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_OutputLessThanPoolReserves() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(300e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(500e6).Add(sdk.OneInt()))
|
||||||
|
err := suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "insufficient liquidity: output 500000001 >= pool reserves 500000000")
|
||||||
|
|
||||||
|
coinB = sdk.NewCoin("usdx", sdk.NewInt(500e6))
|
||||||
|
err = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "insufficient liquidity: output 500000000 >= pool reserves 500000000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_Slippage() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
slippage sdk.Dec
|
||||||
|
fee sdk.Dec
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
// positive slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
// positive slippage with zero slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.0025"), false},
|
||||||
|
// exact zero slippage OK
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010102)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010102)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010102)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050506)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050506)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050506)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
// slippage failure, zero slippage tolerance
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010101)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010101)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1010101)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050505)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050505)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5050505)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.ZeroDec(), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
// slippage failure, 1 percent slippage
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), true},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), true},
|
||||||
|
// slippage OK, 1 percent slippage
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000001)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000001)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("ukava", sdk.NewInt(1000001)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000001)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000001)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.003"), false},
|
||||||
|
{sdk.NewCoin("usdx", sdk.NewInt(5000001)), sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.MustNewDecFromStr("0.01"), sdk.MustNewDecFromStr("0.05"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(fmt.Sprintf("coinA=%s coinB=%s slippage=%s fee=%s", tc.coinA, tc.coinB, tc.slippage, tc.fee), func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
suite.Keeper.SetParams(suite.Ctx, types.Params{
|
||||||
|
SwapFee: tc.fee,
|
||||||
|
})
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(100e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapForExactTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, tc.slippage)
|
||||||
|
|
||||||
|
if tc.shouldFail {
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Contains(err.Error(), "slippage exceeded")
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_InsufficientFunds() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
balanceA sdk.Coin
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
}{
|
||||||
|
{"no ukava balance", sdk.NewCoin("ukava", sdk.ZeroInt()), sdk.NewCoin("ukava", sdk.NewInt(100)), sdk.NewCoin("usdx", sdk.NewInt(500))},
|
||||||
|
{"no usdx balance", sdk.NewCoin("usdx", sdk.ZeroInt()), sdk.NewCoin("usdx", sdk.NewInt(500)), sdk.NewCoin("ukava", sdk.NewInt(100))},
|
||||||
|
{"low ukava balance", sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("usdx", sdk.NewInt(5000000))},
|
||||||
|
{"low ukava balance", sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("ukava", sdk.NewInt(1000000))},
|
||||||
|
{"large ukava balance difference", sdk.NewCoin("ukava", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6))},
|
||||||
|
{"large usdx balance difference", sdk.NewCoin("usdx", sdk.NewInt(500e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(tc.balanceA)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapForExactTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, sdk.MustNewDecFromStr("0.1"))
|
||||||
|
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds), fmt.Sprintf("got err %s", err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_InsufficientFunds_Vesting() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
balanceA sdk.Coin
|
||||||
|
vestingA sdk.Coin
|
||||||
|
coinA sdk.Coin
|
||||||
|
coinB sdk.Coin
|
||||||
|
}{
|
||||||
|
{"no ukava balance, vesting only", sdk.NewCoin("ukava", sdk.ZeroInt()), sdk.NewCoin("ukava", sdk.NewInt(100)), sdk.NewCoin("ukava", sdk.NewInt(1000)), sdk.NewCoin("usdx", sdk.NewInt(500))},
|
||||||
|
{"no usdx balance, vesting only", sdk.NewCoin("usdx", sdk.ZeroInt()), sdk.NewCoin("usdx", sdk.NewInt(500)), sdk.NewCoin("usdx", sdk.NewInt(5000)), sdk.NewCoin("ukava", sdk.NewInt(100))},
|
||||||
|
{"low ukava balance, vesting matches exact", sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("ukava", sdk.NewInt(100000)), sdk.NewCoin("ukava", sdk.NewInt(1000000)), sdk.NewCoin("usdx", sdk.NewInt(5000000))},
|
||||||
|
{"low ukava balance, vesting matches exact", sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("usdx", sdk.NewInt(500000)), sdk.NewCoin("usdx", sdk.NewInt(5000000)), sdk.NewCoin("ukava", sdk.NewInt(1000000))},
|
||||||
|
{"large ukava balance difference, vesting covers difference", sdk.NewCoin("ukava", sdk.NewInt(100e6)), sdk.NewCoin("ukava", sdk.NewInt(10000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6))},
|
||||||
|
{"large usdx balance difference, vesting covers difference", sdk.NewCoin("usdx", sdk.NewInt(500e6)), sdk.NewCoin("usdx", sdk.NewInt(500000e6)), sdk.NewCoin("usdx", sdk.NewInt(5000e6)), sdk.NewCoin("ukava", sdk.NewInt(1000e6))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(100000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(500000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(30000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
balance := sdk.NewCoins(tc.balanceA)
|
||||||
|
vesting := sdk.NewCoins(tc.vestingA)
|
||||||
|
requester := suite.CreateVestingAccount(balance, vesting)
|
||||||
|
|
||||||
|
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||||
|
err := suite.Keeper.SwapForExactTokens(ctx, requester.GetAddress(), tc.coinA, tc.coinB, sdk.MustNewDecFromStr("0.1"))
|
||||||
|
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds), fmt.Sprintf("got err %s", err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_PoolNotFound() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
suite.Keeper.DeletePool(suite.Ctx, poolID)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
err := suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "invalid pool: pool ukava/usdx not found")
|
||||||
|
|
||||||
|
err = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinB, coinA, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
suite.EqualError(err, "invalid pool: pool ukava/usdx not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_PanicOnInvalidPool() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
poolRecord, found := suite.Keeper.GetPool(suite.Ctx, poolID)
|
||||||
|
suite.Require().True(found, "expected pool record to exist")
|
||||||
|
|
||||||
|
poolRecord.TotalShares = sdk.ZeroInt()
|
||||||
|
suite.Keeper.SetPool(suite.Ctx, poolRecord)
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
suite.PanicsWithValue("invalid pool ukava/usdx: invalid pool: total shares must be greater than zero", func() {
|
||||||
|
_ = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected invalid pool record to panic")
|
||||||
|
|
||||||
|
suite.PanicsWithValue("invalid pool ukava/usdx: invalid pool: total shares must be greater than zero", func() {
|
||||||
|
_ = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinB, coinA, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected invalid pool record to panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *keeperTestSuite) TestSwapForExactTokens_PanicOnInsufficientModuleAccFunds() {
|
||||||
|
owner := suite.CreateAccount(sdk.Coins{})
|
||||||
|
reserves := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
)
|
||||||
|
totalShares := sdk.NewInt(3000e6)
|
||||||
|
suite.setupPool(reserves, totalShares, owner.GetAddress())
|
||||||
|
|
||||||
|
suite.RemoveCoinsFromModule(sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
|
||||||
|
))
|
||||||
|
|
||||||
|
balance := sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(10e6)),
|
||||||
|
)
|
||||||
|
requester := suite.NewAccountFromAddr(sdk.AccAddress("requester"), balance)
|
||||||
|
coinA := sdk.NewCoin("ukava", sdk.NewInt(1e6))
|
||||||
|
coinB := sdk.NewCoin("usdx", sdk.NewInt(5e6))
|
||||||
|
|
||||||
|
suite.Panics(func() {
|
||||||
|
_ = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected panic when module account does not have enough funds")
|
||||||
|
|
||||||
|
suite.Panics(func() {
|
||||||
|
_ = suite.Keeper.SwapForExactTokens(suite.Ctx, requester.GetAddress(), coinA, coinB, sdk.MustNewDecFromStr("0.01"))
|
||||||
|
}, "expected panic when module account does not have enough funds")
|
||||||
|
}
|
@ -20,6 +20,8 @@ import (
|
|||||||
tmtime "github.com/tendermint/tendermint/types/time"
|
tmtime "github.com/tendermint/tendermint/types/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultSwapFee = sdk.MustNewDecFromStr("0.003")
|
||||||
|
|
||||||
// Suite implements a test suite for the swap module integration tests
|
// Suite implements a test suite for the swap module integration tests
|
||||||
type Suite struct {
|
type Suite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
@ -111,7 +113,7 @@ func (suite *Suite) CreatePool(reserves sdk.Coins) error {
|
|||||||
depositor := suite.CreateAccount(reserves)
|
depositor := suite.CreateAccount(reserves)
|
||||||
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
|
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
|
||||||
suite.Require().NoError(pool.Validate())
|
suite.Require().NoError(pool.Validate())
|
||||||
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
|
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), defaultSwapFee))
|
||||||
|
|
||||||
return suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
|
return suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,6 @@ func init() {
|
|||||||
func RegisterCodec(cdc *codec.Codec) {
|
func RegisterCodec(cdc *codec.Codec) {
|
||||||
cdc.RegisterConcrete(MsgDeposit{}, "swap/MsgDeposit", nil)
|
cdc.RegisterConcrete(MsgDeposit{}, "swap/MsgDeposit", nil)
|
||||||
cdc.RegisterConcrete(MsgWithdraw{}, "swap/MsgWithdraw", nil)
|
cdc.RegisterConcrete(MsgWithdraw{}, "swap/MsgWithdraw", nil)
|
||||||
|
cdc.RegisterConcrete(MsgSwapExactForTokens{}, "swap/MsgSwapExactForTokens", nil)
|
||||||
|
cdc.RegisterConcrete(MsgSwapForExactTokens{}, "swap/MsgSwapForExactTokens", nil)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,17 @@ package types
|
|||||||
|
|
||||||
// Event types for swap module
|
// Event types for swap module
|
||||||
const (
|
const (
|
||||||
AttributeValueCategory = ModuleName
|
AttributeValueCategory = ModuleName
|
||||||
EventTypeSwapDeposit = "swap_deposit"
|
EventTypeSwapDeposit = "swap_deposit"
|
||||||
EventTypeSwapWithdraw = "swap_withdraw"
|
EventTypeSwapWithdraw = "swap_withdraw"
|
||||||
AttributeKeyPoolID = "pool_id"
|
EventTypeSwapTrade = "swap_trade"
|
||||||
AttributeKeyDepositor = "depositor"
|
AttributeKeyPoolID = "pool_id"
|
||||||
AttributeKeyShares = "shares"
|
AttributeKeyDepositor = "depositor"
|
||||||
AttributeKeyOwner = "owner"
|
AttributeKeyShares = "shares"
|
||||||
|
AttributeKeyOwner = "owner"
|
||||||
|
AttributeKeyRequester = "requester"
|
||||||
|
AttributeKeySwapInput = "input"
|
||||||
|
AttributeKeySwapOutput = "output"
|
||||||
|
AttributeKeyFeePaid = "fee"
|
||||||
|
AttributeKeyExactDirection = "exact"
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,10 @@ var (
|
|||||||
_ MsgWithDeadline = &MsgDeposit{}
|
_ MsgWithDeadline = &MsgDeposit{}
|
||||||
_ sdk.Msg = &MsgWithdraw{}
|
_ sdk.Msg = &MsgWithdraw{}
|
||||||
_ MsgWithDeadline = &MsgWithdraw{}
|
_ MsgWithDeadline = &MsgWithdraw{}
|
||||||
|
_ sdk.Msg = &MsgSwapExactForTokens{}
|
||||||
|
_ MsgWithDeadline = &MsgSwapExactForTokens{}
|
||||||
|
_ sdk.Msg = &MsgSwapForExactTokens{}
|
||||||
|
_ MsgWithDeadline = &MsgSwapForExactTokens{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// MsgWithDeadline allows messages to define a deadline of when they are considered invalid
|
// MsgWithDeadline allows messages to define a deadline of when they are considered invalid
|
||||||
@ -179,3 +183,163 @@ func (msg MsgWithdraw) GetDeadline() time.Time {
|
|||||||
func (msg MsgWithdraw) DeadlineExceeded(blockTime time.Time) bool {
|
func (msg MsgWithdraw) DeadlineExceeded(blockTime time.Time) bool {
|
||||||
return blockTime.Unix() >= msg.Deadline
|
return blockTime.Unix() >= msg.Deadline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MsgSwapExactForTokens trades an exact coinA for coinB
|
||||||
|
type MsgSwapExactForTokens struct {
|
||||||
|
Requester sdk.AccAddress `json:"requester" yaml:"requester"`
|
||||||
|
ExactTokenA sdk.Coin `json:"exact_token_a" yaml:"exact_token_a"`
|
||||||
|
TokenB sdk.Coin `json:"token_b" yaml:"token_b"`
|
||||||
|
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
|
||||||
|
Deadline int64 `json:"deadline" yaml:"deadline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgSwapExactForTokens returns a new MsgSwapExactForTokens
|
||||||
|
func NewMsgSwapExactForTokens(requester sdk.AccAddress, exactTokenA sdk.Coin, tokenB sdk.Coin, slippage sdk.Dec, deadline int64) MsgSwapExactForTokens {
|
||||||
|
return MsgSwapExactForTokens{
|
||||||
|
Requester: requester,
|
||||||
|
ExactTokenA: exactTokenA,
|
||||||
|
TokenB: tokenB,
|
||||||
|
Slippage: slippage,
|
||||||
|
Deadline: deadline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgSwapExactForTokens) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgSwapExactForTokens) Type() string { return "swap_exact_for_tokens" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
|
func (msg MsgSwapExactForTokens) ValidateBasic() error {
|
||||||
|
if msg.Requester.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "requester address cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !msg.ExactTokenA.IsValid() || msg.ExactTokenA.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "exact token a deposit amount %s", msg.ExactTokenA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !msg.TokenB.IsValid() || msg.TokenB.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "token b deposit amount %s", msg.TokenB)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ExactTokenA.Denom == msg.TokenB.Denom {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "denominations can not be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Slippage.IsNil() {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Slippage.IsNegative() {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage can not be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Deadline <= 0 {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidDeadline, "deadline %d", msg.Deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||||
|
func (msg MsgSwapExactForTokens) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign.
|
||||||
|
func (msg MsgSwapExactForTokens) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Requester}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeadline returns the time at which the msg is considered invalid
|
||||||
|
func (msg MsgSwapExactForTokens) GetDeadline() time.Time {
|
||||||
|
return time.Unix(msg.Deadline, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeadlineExceeded returns if the msg has exceeded it's deadline
|
||||||
|
func (msg MsgSwapExactForTokens) DeadlineExceeded(blockTime time.Time) bool {
|
||||||
|
return blockTime.Unix() >= msg.Deadline
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgSwapForExactTokens trades coinA for an exact coinB
|
||||||
|
type MsgSwapForExactTokens struct {
|
||||||
|
Requester sdk.AccAddress `json:"requester" yaml:"requester"`
|
||||||
|
TokenA sdk.Coin `json:"token_a" yaml:"token_a"`
|
||||||
|
ExactTokenB sdk.Coin `json:"exact_token_b" yaml:"exact_token_b"`
|
||||||
|
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
|
||||||
|
Deadline int64 `json:"deadline" yaml:"deadline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgSwapForExactTokens returns a new MsgSwapForExactTokens
|
||||||
|
func NewMsgSwapForExactTokens(requester sdk.AccAddress, tokenA sdk.Coin, exactTokenB sdk.Coin, slippage sdk.Dec, deadline int64) MsgSwapForExactTokens {
|
||||||
|
return MsgSwapForExactTokens{
|
||||||
|
Requester: requester,
|
||||||
|
TokenA: tokenA,
|
||||||
|
ExactTokenB: exactTokenB,
|
||||||
|
Slippage: slippage,
|
||||||
|
Deadline: deadline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgSwapForExactTokens) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgSwapForExactTokens) Type() string { return "swap_for_exact_tokens" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
|
func (msg MsgSwapForExactTokens) ValidateBasic() error {
|
||||||
|
if msg.Requester.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "requester address cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !msg.TokenA.IsValid() || msg.TokenA.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "token a deposit amount %s", msg.TokenA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !msg.ExactTokenB.IsValid() || msg.ExactTokenB.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "exact token b deposit amount %s", msg.ExactTokenB)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.TokenA.Denom == msg.ExactTokenB.Denom {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "denominations can not be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Slippage.IsNil() {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Slippage.IsNegative() {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage can not be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Deadline <= 0 {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidDeadline, "deadline %d", msg.Deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||||
|
func (msg MsgSwapForExactTokens) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign.
|
||||||
|
func (msg MsgSwapForExactTokens) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Requester}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeadline returns the time at which the msg is considered invalid
|
||||||
|
func (msg MsgSwapForExactTokens) GetDeadline() time.Time {
|
||||||
|
return time.Unix(msg.Deadline, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeadlineExceeded returns if the msg has exceeded it's deadline
|
||||||
|
func (msg MsgSwapForExactTokens) DeadlineExceeded(blockTime time.Time) bool {
|
||||||
|
return blockTime.Unix() >= msg.Deadline
|
||||||
|
}
|
||||||
|
@ -406,3 +406,393 @@ func TestMsgWithdraw_Deadline(t *testing.T) {
|
|||||||
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
|
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapExactForTokens_Attributes(t *testing.T) {
|
||||||
|
msg := types.MsgSwapExactForTokens{}
|
||||||
|
assert.Equal(t, "swap", msg.Route())
|
||||||
|
assert.Equal(t, "swap_exact_for_tokens", msg.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapExactForTokens_Signing(t *testing.T) {
|
||||||
|
signData := `{"type":"swap/MsgSwapExactForTokens","value":{"deadline":"1623606299","exact_token_a":{"amount":"1000000","denom":"ukava"},"requester":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","slippage":"0.010000000000000000","token_b":{"amount":"5000000","denom":"usdx"}}}`
|
||||||
|
signBytes := []byte(signData)
|
||||||
|
|
||||||
|
addr, err := sdk.AccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapExactForTokens(addr, sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), 1623606299)
|
||||||
|
assert.Equal(t, []sdk.AccAddress{addr}, msg.GetSigners())
|
||||||
|
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapExactForTokens_Validation(t *testing.T) {
|
||||||
|
validMsg := types.NewMsgSwapExactForTokens(
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
1623606299,
|
||||||
|
)
|
||||||
|
require.NoError(t, validMsg.ValidateBasic())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
requester sdk.AccAddress
|
||||||
|
exactTokenA sdk.Coin
|
||||||
|
tokenB sdk.Coin
|
||||||
|
slippage sdk.Dec
|
||||||
|
deadline int64
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty address",
|
||||||
|
requester: sdk.AccAddress(""),
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid address: requester address cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token a deposit amount -1ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token a deposit amount 0ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid denom token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token a deposit amount 1000000UKAVA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token b deposit amount -1ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token b deposit amount 0ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid denom token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token b deposit amount 1000000UKAVA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denoms can not be the same",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
|
||||||
|
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: denominations can not be equal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero deadline",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: 0,
|
||||||
|
expectedErr: "invalid deadline: deadline 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative deadline",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: -1,
|
||||||
|
expectedErr: "invalid deadline: deadline -1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative slippage",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: sdk.MustNewDecFromStr("-0.01"),
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid slippage: slippage can not be negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil slippage",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
exactTokenA: validMsg.ExactTokenA,
|
||||||
|
tokenB: validMsg.TokenB,
|
||||||
|
slippage: sdk.Dec{},
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid slippage: slippage must be set",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
msg := types.NewMsgSwapExactForTokens(tc.requester, tc.exactTokenA, tc.tokenB, tc.slippage, tc.deadline)
|
||||||
|
err := msg.ValidateBasic()
|
||||||
|
assert.EqualError(t, err, tc.expectedErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapExactForTokens_Deadline(t *testing.T) {
|
||||||
|
blockTime := time.Now()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
deadline int64
|
||||||
|
isExceeded bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "deadline in future",
|
||||||
|
deadline: blockTime.Add(1 * time.Second).Unix(),
|
||||||
|
isExceeded: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline in past",
|
||||||
|
deadline: blockTime.Add(-1 * time.Second).Unix(),
|
||||||
|
isExceeded: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline is equal",
|
||||||
|
deadline: blockTime.Unix(),
|
||||||
|
isExceeded: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
msg := types.NewMsgSwapExactForTokens(
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000000)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(2000000)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
tc.deadline,
|
||||||
|
)
|
||||||
|
require.NoError(t, msg.ValidateBasic())
|
||||||
|
assert.Equal(t, tc.isExceeded, msg.DeadlineExceeded(blockTime))
|
||||||
|
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapForExactTokens_Attributes(t *testing.T) {
|
||||||
|
msg := types.MsgSwapForExactTokens{}
|
||||||
|
assert.Equal(t, "swap", msg.Route())
|
||||||
|
assert.Equal(t, "swap_for_exact_tokens", msg.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapForExactTokens_Signing(t *testing.T) {
|
||||||
|
signData := `{"type":"swap/MsgSwapForExactTokens","value":{"deadline":"1623606299","exact_token_b":{"amount":"5000000","denom":"usdx"},"requester":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","slippage":"0.010000000000000000","token_a":{"amount":"1000000","denom":"ukava"}}}`
|
||||||
|
signBytes := []byte(signData)
|
||||||
|
|
||||||
|
addr, err := sdk.AccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg := types.NewMsgSwapForExactTokens(addr, sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), 1623606299)
|
||||||
|
assert.Equal(t, []sdk.AccAddress{addr}, msg.GetSigners())
|
||||||
|
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapForExactTokens_Validation(t *testing.T) {
|
||||||
|
validMsg := types.NewMsgSwapForExactTokens(
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
1623606299,
|
||||||
|
)
|
||||||
|
require.NoError(t, validMsg.ValidateBasic())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
requester sdk.AccAddress
|
||||||
|
tokenA sdk.Coin
|
||||||
|
exactTokenB sdk.Coin
|
||||||
|
slippage sdk.Dec
|
||||||
|
deadline int64
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty address",
|
||||||
|
requester: sdk.AccAddress(""),
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid address: requester address cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token a deposit amount -1ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token a deposit amount 0ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid denom token a",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: token a deposit amount 1000000UKAVA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token b deposit amount -1ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token b deposit amount 0ukava",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid denom token b",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: exact token b deposit amount 1000000UKAVA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denoms can not be the same",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
|
||||||
|
exactTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid coins: denominations can not be equal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero deadline",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: 0,
|
||||||
|
expectedErr: "invalid deadline: deadline 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative deadline",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: validMsg.Slippage,
|
||||||
|
deadline: -1,
|
||||||
|
expectedErr: "invalid deadline: deadline -1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative slippage",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: sdk.MustNewDecFromStr("-0.01"),
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid slippage: slippage can not be negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil slippage",
|
||||||
|
requester: validMsg.Requester,
|
||||||
|
tokenA: validMsg.TokenA,
|
||||||
|
exactTokenB: validMsg.ExactTokenB,
|
||||||
|
slippage: sdk.Dec{},
|
||||||
|
deadline: validMsg.Deadline,
|
||||||
|
expectedErr: "invalid slippage: slippage must be set",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
msg := types.NewMsgSwapForExactTokens(tc.requester, tc.tokenA, tc.exactTokenB, tc.slippage, tc.deadline)
|
||||||
|
err := msg.ValidateBasic()
|
||||||
|
assert.EqualError(t, err, tc.expectedErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgSwapForExactTokens_Deadline(t *testing.T) {
|
||||||
|
blockTime := time.Now()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
deadline int64
|
||||||
|
isExceeded bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "deadline in future",
|
||||||
|
deadline: blockTime.Add(1 * time.Second).Unix(),
|
||||||
|
isExceeded: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline in past",
|
||||||
|
deadline: blockTime.Add(-1 * time.Second).Unix(),
|
||||||
|
isExceeded: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline is equal",
|
||||||
|
deadline: blockTime.Unix(),
|
||||||
|
isExceeded: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
msg := types.NewMsgSwapForExactTokens(
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
sdk.NewCoin("ukava", sdk.NewInt(1000000)),
|
||||||
|
sdk.NewCoin("usdx", sdk.NewInt(2000000)),
|
||||||
|
sdk.MustNewDecFromStr("0.01"),
|
||||||
|
tc.deadline,
|
||||||
|
)
|
||||||
|
require.NoError(t, msg.ValidateBasic())
|
||||||
|
assert.Equal(t, tc.isExceeded, msg.DeadlineExceeded(blockTime))
|
||||||
|
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user