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:
Nick DeLuca 2021-07-13 17:44:05 -05:00 committed by GitHub
parent 880b9a2cc5
commit 20437a91fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1779 additions and 51 deletions

View File

@ -8,20 +8,26 @@ import (
)
const (
AttributeKeyDepositor = types.AttributeKeyDepositor
AttributeKeyOwner = types.AttributeKeyOwner
AttributeKeyPoolID = types.AttributeKeyPoolID
AttributeKeyShares = types.AttributeKeyShares
AttributeValueCategory = types.AttributeValueCategory
DefaultParamspace = types.DefaultParamspace
EventTypeSwapDeposit = types.EventTypeSwapDeposit
EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
ModuleAccountName = types.ModuleAccountName
ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
StoreKey = types.StoreKey
AttributeKeyDepositor = types.AttributeKeyDepositor
AttributeKeyExactDirection = types.AttributeKeyExactDirection
AttributeKeyFeePaid = types.AttributeKeyFeePaid
AttributeKeyOwner = types.AttributeKeyOwner
AttributeKeyPoolID = types.AttributeKeyPoolID
AttributeKeyRequester = types.AttributeKeyRequester
AttributeKeyShares = types.AttributeKeyShares
AttributeKeySwapInput = types.AttributeKeySwapInput
AttributeKeySwapOutput = types.AttributeKeySwapOutput
AttributeValueCategory = types.AttributeValueCategory
DefaultParamspace = types.DefaultParamspace
EventTypeSwapDeposit = types.EventTypeSwapDeposit
EventTypeSwapTrade = types.EventTypeSwapTrade
EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
ModuleAccountName = types.ModuleAccountName
ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
StoreKey = types.StoreKey
)
var (
@ -39,6 +45,8 @@ var (
NewDenominatedPoolWithExistingShares = types.NewDenominatedPoolWithExistingShares
NewGenesisState = types.NewGenesisState
NewMsgDeposit = types.NewMsgDeposit
NewMsgSwapExactForTokens = types.NewMsgSwapExactForTokens
NewMsgSwapForExactTokens = types.NewMsgSwapForExactTokens
NewMsgWithdraw = types.NewMsgWithdraw
NewParams = types.NewParams
NewPoolRecord = types.NewPoolRecord
@ -72,18 +80,20 @@ var (
)
type (
Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper
AllowedPool = types.AllowedPool
AllowedPools = types.AllowedPools
BasePool = types.BasePool
DenominatedPool = types.DenominatedPool
GenesisState = types.GenesisState
MsgDeposit = types.MsgDeposit
MsgWithDeadline = types.MsgWithDeadline
MsgWithdraw = types.MsgWithdraw
Params = types.Params
PoolRecord = types.PoolRecord
ShareRecord = types.ShareRecord
SupplyKeeper = types.SupplyKeeper
Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper
AllowedPool = types.AllowedPool
AllowedPools = types.AllowedPools
BasePool = types.BasePool
DenominatedPool = types.DenominatedPool
GenesisState = types.GenesisState
MsgDeposit = types.MsgDeposit
MsgSwapExactForTokens = types.MsgSwapExactForTokens
MsgSwapForExactTokens = types.MsgSwapForExactTokens
MsgWithDeadline = types.MsgWithDeadline
MsgWithdraw = types.MsgWithdraw
Params = types.Params
PoolRecord = types.PoolRecord
ShareRecord = types.ShareRecord
SupplyKeeper = types.SupplyKeeper
)

View File

@ -32,6 +32,8 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
swapTxCmd.AddCommand(flags.PostCommands(
getCmdDeposit(cdc),
getCmdWithdraw(cdc),
getCmdSwapExactForTokens(cdc),
getCmdSwapForExactTokens(cdc),
)...)
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})
},
}
}

View File

@ -104,7 +104,7 @@ func queryPoolHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
poolName = strings.TrimSpace(x)
}
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
}

View File

@ -40,3 +40,23 @@ type PostCreateWithdrawReq struct {
MinTokenB sdk.Coin `json:"token_b" yaml:"token_b"`
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"`
}

View File

@ -17,6 +17,8 @@ import (
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/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 {
@ -60,3 +62,45 @@ func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
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})
}
}

View File

@ -24,6 +24,10 @@ func NewHandler(k Keeper) sdk.Handler {
return handleMsgDeposit(ctx, k, msg)
case types.MsgWithdraw:
return handleMsgWithdraw(ctx, k, msg)
case types.MsgSwapExactForTokens:
return handleMsgSwapExactForTokens(ctx, k, msg)
case types.MsgSwapForExactTokens:
return handleMsgSwapForExactTokens(ctx, k, msg)
default:
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
}
ctx.EventManager().EmitEvent(
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
return resultWithMsgSender(ctx, msg.Depositor), nil
}
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 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(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
sdk.NewAttribute(sdk.AttributeKeySender, sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
}

View File

@ -349,6 +349,226 @@ func (suite *handlerTestSuite) TestWithdraw_DeadlineExceeded() {
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() {
res, err := suite.handler(suite.Ctx, sdk.NewTestMsg())
suite.Nil(res)

View File

@ -61,6 +61,11 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// 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
func (k Keeper) GetPool(ctx sdk.Context, poolID string) (types.PoolRecord, bool) {
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) {
k.IterateDepositorSharesByOwner(ctx, owner, func(record types.ShareRecord) bool {
records = append(records, record)

View File

@ -69,6 +69,17 @@ func (suite keeperTestSuite) TestParams_Persistance() {
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() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),

122
x/swap/keeper/swap.go Normal file
View 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
View 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")
}

View File

@ -20,6 +20,8 @@ import (
tmtime "github.com/tendermint/tendermint/types/time"
)
var defaultSwapFee = sdk.MustNewDecFromStr("0.003")
// Suite implements a test suite for the swap module integration tests
type Suite struct {
suite.Suite
@ -111,7 +113,7 @@ func (suite *Suite) CreatePool(reserves sdk.Coins) error {
depositor := suite.CreateAccount(reserves)
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
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"))
}

View File

@ -16,4 +16,6 @@ func init() {
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgDeposit{}, "swap/MsgDeposit", nil)
cdc.RegisterConcrete(MsgWithdraw{}, "swap/MsgWithdraw", nil)
cdc.RegisterConcrete(MsgSwapExactForTokens{}, "swap/MsgSwapExactForTokens", nil)
cdc.RegisterConcrete(MsgSwapForExactTokens{}, "swap/MsgSwapForExactTokens", nil)
}

View File

@ -2,11 +2,17 @@ package types
// Event types for swap module
const (
AttributeValueCategory = ModuleName
EventTypeSwapDeposit = "swap_deposit"
EventTypeSwapWithdraw = "swap_withdraw"
AttributeKeyPoolID = "pool_id"
AttributeKeyDepositor = "depositor"
AttributeKeyShares = "shares"
AttributeKeyOwner = "owner"
AttributeValueCategory = ModuleName
EventTypeSwapDeposit = "swap_deposit"
EventTypeSwapWithdraw = "swap_withdraw"
EventTypeSwapTrade = "swap_trade"
AttributeKeyPoolID = "pool_id"
AttributeKeyDepositor = "depositor"
AttributeKeyShares = "shares"
AttributeKeyOwner = "owner"
AttributeKeyRequester = "requester"
AttributeKeySwapInput = "input"
AttributeKeySwapOutput = "output"
AttributeKeyFeePaid = "fee"
AttributeKeyExactDirection = "exact"
)

View File

@ -12,6 +12,10 @@ var (
_ MsgWithDeadline = &MsgDeposit{}
_ sdk.Msg = &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
@ -179,3 +183,163 @@ func (msg MsgWithdraw) GetDeadline() time.Time {
func (msg MsgWithdraw) DeadlineExceeded(blockTime time.Time) bool {
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
}

View File

@ -406,3 +406,393 @@ func TestMsgWithdraw_Deadline(t *testing.T) {
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())
}
}