Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] MsgSwapSend #348

Merged
merged 4 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions x/auth/ante/tax.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/bank"

core "github.com/terra-project/core/types"
"github.com/terra-project/core/x/market"
wasmexported "github.com/terra-project/core/x/wasm/exported"
)

Expand Down Expand Up @@ -119,6 +120,9 @@ func FilterMsgAndComputeTax(ctx sdk.Context, tk TreasuryKeeper, msgs []sdk.Msg)
taxes = taxes.Add(computeTax(ctx, tk, input.Coins)...)
}

case market.MsgSwapSend:
taxes = taxes.Add(computeTax(ctx, tk, sdk.NewCoins(msg.OfferCoin))...)

case wasmexported.MsgInstantiateContract:
taxes = taxes.Add(computeTax(ctx, tk, msg.InitCoins)...)

Expand Down
2 changes: 2 additions & 0 deletions x/market/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewMsgSwap = types.NewMsgSwap
NewMsgSwapSend = types.NewMsgSwapSend
DefaultParams = types.DefaultParams
NewQuerySwapParams = types.NewQuerySwapParams
ParamKeyTable = types.ParamKeyTable
Expand All @@ -53,6 +54,7 @@ type (
OracleKeeper = types.OracleKeeper
GenesisState = types.GenesisState
MsgSwap = types.MsgSwap
MsgSwapSend = types.MsgSwapSend
Params = types.Params
QuerySwapParams = types.QuerySwapParams
Keeper = keeper.Keeper
Expand Down
22 changes: 18 additions & 4 deletions x/market/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"bufio"
"fmt"
"strings"

"github.com/terra-project/core/x/market/internal/types"
Expand Down Expand Up @@ -37,13 +38,17 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
// GetSwapCmd will create and send a MsgSwap
func GetSwapCmd(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "swap [offer-coin] [ask-denom]",
Args: cobra.ExactArgs(2),
Use: "swap [offer-coin] [ask-denom] [to-address]",
Args: cobra.RangeArgs(2, 3),
Short: "Atomically swap currencies at their target exchange rate",
Long: strings.TrimSpace(`
Swap the offer-coin to the ask-denom currency at the oracle's effective exchange rate.

$ terracli market swap "1000ukrw" "uusd"

The fund to-address can be specfied. A default to-address is trader.

$ terracli market swap "1000ukrw" "uusd" "terra1..."
`),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
Expand All @@ -57,16 +62,25 @@ $ terracli market swap "1000ukrw" "uusd"
}

askDenom := args[1]

fromAddress := cliCtx.GetFromAddress()

toAddress := fromAddress
if len(args) == 3 {
toAddress, err = sdk.AccAddressFromBech32(args[2])
if err != nil {
return err
}
}

// build and sign the transaction, then broadcast to Tendermint
msg := types.NewMsgSwap(fromAddress, offerCoin, askDenom)
msg := types.NewMsgSwapSend(fromAddress, toAddress, offerCoin, askDenom)
err = msg.ValidateBasic()
if err != nil {
return err
}

fmt.Println(msg)

return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
Expand Down
14 changes: 10 additions & 4 deletions x/market/client/rest/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {

// SwapReq defines request body for swap operation
type SwapReq struct {
BaseReq rest.BaseReq `json:"base_req"`
OfferCoin sdk.Coin `json:"offer_coin"`
AskDenom string `json:"ask_denom"`
BaseReq rest.BaseReq `json:"base_req"`
OfferCoin sdk.Coin `json:"offer_coin"`
AskDenom string `json:"ask_denom"`
Receiver sdk.AccAddress `json:"receiver,omitempty"`
}

// submitSwapHandlerFn handles a POST vote request
Expand All @@ -43,8 +44,13 @@ func submitSwapHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}

toAddress := req.Receiver
if toAddress.Empty() {
toAddress = fromAddress
}

// create the message
msg := types.NewMsgSwap(fromAddress, req.OfferCoin, req.AskDenom)
msg := types.NewMsgSwapSend(fromAddress, toAddress, req.OfferCoin, req.AskDenom)
err = msg.ValidateBasic()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
Expand Down
56 changes: 34 additions & 22 deletions x/market/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,49 @@ func NewHandler(k Keeper) sdk.Handler {
switch msg := msg.(type) {
case MsgSwap:
return handleMsgSwap(ctx, k, msg)
case MsgSwapSend:
return handleMsgSwapSend(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized distribution message type: %T", msg)
}
}
}

// handleMsgSwap handles the logic of a MsgSwap
func handleMsgSwapSend(ctx sdk.Context, k Keeper, mss MsgSwapSend) (*sdk.Result, error) {
return handleSwapRequest(ctx, k, mss.FromAddress, mss.ToAddress, mss.OfferCoin, mss.AskDenom)
}

func handleMsgSwap(ctx sdk.Context, k Keeper, ms MsgSwap) (*sdk.Result, error) {
return handleSwapRequest(ctx, k, ms.Trader, ms.Trader, ms.OfferCoin, ms.AskDenom)
}

// handleMsgSwap handles the logic of a MsgSwap
func handleSwapRequest(ctx sdk.Context, k Keeper,
trader sdk.AccAddress, receiver sdk.AccAddress,
offerCoin sdk.Coin, askDenom string) (*sdk.Result, error) {
// Can't swap to the same coin
if ms.OfferCoin.Denom == ms.AskDenom {
return nil, sdkerrors.Wrap(ErrRecursiveSwap, ms.AskDenom)
if offerCoin.Denom == askDenom {
return nil, ErrRecursiveSwap
}

// Compute exchange rates between the ask and offer
swapCoin, spread, swapErr := k.ComputeSwap(ctx, ms.OfferCoin, ms.AskDenom)
if swapErr != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "ComputeSwap failed: %s", swapErr.Error())
swapCoin, spread, err := k.ComputeSwap(ctx, offerCoin, askDenom)

if err != nil {
return nil, err
}

// Update pool delta
deltaUpdateErr := k.ApplySwapToPool(ctx, ms.OfferCoin, swapCoin)
deltaUpdateErr := k.ApplySwapToPool(ctx, offerCoin, swapCoin)
if deltaUpdateErr != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "ApplySwapToPool failed: %s", deltaUpdateErr.Error())
return nil, deltaUpdateErr
}

// Send offer coins to module account
offerCoins := sdk.NewCoins(ms.OfferCoin)
err := k.SupplyKeeper.SendCoinsFromAccountToModule(ctx, ms.Trader, ModuleName, offerCoins)
offerCoins := sdk.NewCoins(offerCoin)
err = k.SupplyKeeper.SendCoinsFromAccountToModule(ctx, trader, ModuleName, offerCoins)
if err != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "SendCoinsFromAccountToModule failed: %s", err.Error())
return nil, err
}

// Charge a spread if applicable; distributed to vote winners in the oracle module
Expand All @@ -61,30 +73,30 @@ func handleMsgSwap(ctx sdk.Context, k Keeper, ms MsgSwap) (*sdk.Result, error) {
}

// Burn offered coins and subtract from the trader's account
burnErr := k.SupplyKeeper.BurnCoins(ctx, ModuleName, offerCoins)
if burnErr != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "BurnCoins failed: %s", burnErr.Error())
err = k.SupplyKeeper.BurnCoins(ctx, ModuleName, offerCoins)
if err != nil {
return nil, err
}

// Mint asked coins and credit Trader's account
retCoin, decimalCoin := swapCoin.TruncateDecimal()
swapFee = swapFee.Add(decimalCoin) // add truncated decimalCoin to swapFee
swapCoins := sdk.NewCoins(retCoin)
mintErr := k.SupplyKeeper.MintCoins(ctx, ModuleName, swapCoins)
if mintErr != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "MintCoins failed: %s", mintErr.Error())
err = k.SupplyKeeper.MintCoins(ctx, ModuleName, swapCoins)
if err != nil {
return nil, err
}

sendErr := k.SupplyKeeper.SendCoinsFromModuleToAccount(ctx, ModuleName, ms.Trader, swapCoins)
if sendErr != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPanic, "SendCoinsFromModuleToAccount failed: %s", sendErr.Error())
err = k.SupplyKeeper.SendCoinsFromModuleToAccount(ctx, ModuleName, receiver, swapCoins)
if err != nil {
return nil, err
}

ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventSwap,
sdk.NewAttribute(types.AttributeKeyOffer, ms.OfferCoin.String()),
sdk.NewAttribute(types.AttributeKeyTrader, ms.Trader.String()),
sdk.NewAttribute(types.AttributeKeyOffer, offerCoin.String()),
sdk.NewAttribute(types.AttributeKeyTrader, trader.String()),
sdk.NewAttribute(types.AttributeKeySwapCoin, retCoin.String()),
sdk.NewAttribute(types.AttributeKeySwapFee, swapFee.String()),
),
Expand Down
18 changes: 18 additions & 0 deletions x/market/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,21 @@ func TestSwapMsg(t *testing.T) {
_, err = h(input.Ctx, swapMsg)
require.NoError(t, err)
}

func TestSwapSendMsg(t *testing.T) {
input, h := setup(t)

amt := sdk.NewInt(10)
offerCoin := sdk.NewCoin(core.MicroLunaDenom, amt)
retCoin, spread, err := input.MarketKeeper.ComputeSwap(input.Ctx, offerCoin, core.MicroSDRDenom)
require.NoError(t, err)

expectedAmt := retCoin.Amount.Mul(sdk.OneDec().Sub(spread)).TruncateInt()

swapSendMsg := NewMsgSwapSend(keeper.Addrs[0], keeper.Addrs[1], offerCoin, core.MicroSDRDenom)
_, err = h(input.Ctx, swapSendMsg)
require.NoError(t, err)

acc := input.Acckeeper.GetAccount(input.Ctx, keeper.Addrs[1])
require.Equal(t, expectedAmt, acc.GetCoins().AmountOf(core.MicroSDRDenom))
}
3 changes: 2 additions & 1 deletion x/market/internal/keeper/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var (
type TestInput struct {
Ctx sdk.Context
Cdc *codec.Codec
Acckeeper auth.AccountKeeper
OracleKeeper oracle.Keeper
SupplyKeeper supply.Keeper
MarketKeeper Keeper
Expand Down Expand Up @@ -174,5 +175,5 @@ func CreateTestInput(t *testing.T) TestInput {
supply = supply.SetTotal(sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, InitTokens.MulRaw(int64(len(Addrs))))))
supplyKeeper.SetSupply(ctx, supply)

return TestInput{ctx, cdc, oracleKeeper, supplyKeeper, keeper}
return TestInput{ctx, cdc, accountKeeper, oracleKeeper, supplyKeeper, keeper}
}
1 change: 1 addition & 0 deletions x/market/internal/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var ModuleCdc = codec.New()
// RegisterCodec concretes types on codec codec
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgSwap{}, "market/MsgSwap", nil)
cdc.RegisterConcrete(MsgSwapSend{}, "market/MsgSwapSend", nil)
}

func init() {
Expand Down
66 changes: 65 additions & 1 deletion x/market/internal/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,72 @@ func (msg MsgSwap) ValidateBasic() error {
// String implements fmt.Stringer interface
func (msg MsgSwap) String() string {
return fmt.Sprintf(`MsgSwap
trader: %s,
trader: %s,
offer: %s,
ask: %s`,
msg.Trader, msg.OfferCoin, msg.AskDenom)
}

// MsgSwapSend contains a swap request
type MsgSwapSend struct {
FromAddress sdk.AccAddress `json:"from_address" yaml:"from_address"` // Address of the offer coin payer
ToAddress sdk.AccAddress `json:"to_address" yaml:"to_address"` // Address of the recipient
OfferCoin sdk.Coin `json:"offer_coin" yaml:"offer_coin"` // Coin being offered
AskDenom string `json:"ask_denom" yaml:"ask_denom"` // Denom of the coin to swap to
}

func NewMsgSwapSend(fromAddress sdk.AccAddress, toAddress sdk.AccAddress, offerCoin sdk.Coin, askCoin string) MsgSwapSend {
return MsgSwapSend{
FromAddress: fromAddress,
ToAddress: toAddress,
OfferCoin: offerCoin,
AskDenom: askCoin,
}
}

// Route Implements Msg
func (msg MsgSwapSend) Route() string { return RouterKey }

// Type implements sdk.Msg
func (msg MsgSwapSend) Type() string { return "swap" }

// GetSignBytes Implements Msg
func (msg MsgSwapSend) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}

// GetSigners Implements Msg
func (msg MsgSwapSend) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.FromAddress}
}

// ValidateBasic Implements Msg
func (msg MsgSwapSend) ValidateBasic() error {
if len(msg.FromAddress) == 0 {
return sdkerrors.ErrInvalidAddress
}

if len(msg.ToAddress) == 0 {
return sdkerrors.ErrInvalidAddress
}

if msg.OfferCoin.Amount.LTE(sdk.ZeroInt()) || msg.OfferCoin.Amount.BigInt().BitLen() > 100 {
return sdkerrors.Wrap(ErrInvalidOfferCoin, msg.OfferCoin.Amount.String())
}

if msg.OfferCoin.Denom == msg.AskDenom {
return sdkerrors.Wrap(ErrRecursiveSwap, msg.AskDenom)
}

return nil
}

// String implements fmt.Stringer interface
func (msg MsgSwapSend) String() string {
return fmt.Sprintf(`MsgSwapSend
fromAddress: %s,
toAddress: %s,
offer: %s,
ask: %s`,
msg.FromAddress, msg.ToAddress, msg.OfferCoin, msg.AskDenom)
}
30 changes: 30 additions & 0 deletions x/market/internal/types/msgs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,33 @@ func TestMsgSwap(t *testing.T) {
}
}
}

func TestMsgSwapSend(t *testing.T) {
_, addrs, _, _ := mock.CreateGenAccounts(1, sdk.Coins{})

overflowOfferAmt, _ := sdk.NewIntFromString("100000000000000000000000000000000000000000000000000000000")

tests := []struct {
fromAddress sdk.AccAddress
toAddress sdk.AccAddress
offerCoin sdk.Coin
askDenom string
expectPass bool
}{
{addrs[0], addrs[0], sdk.NewCoin(core.MicroLunaDenom, sdk.OneInt()), core.MicroSDRDenom, true},
{addrs[0], sdk.AccAddress{}, sdk.NewCoin(core.MicroLunaDenom, sdk.OneInt()), core.MicroSDRDenom, false},
{sdk.AccAddress{}, addrs[0], sdk.NewCoin(core.MicroLunaDenom, sdk.OneInt()), core.MicroSDRDenom, false},
{addrs[0], addrs[0], sdk.NewCoin(core.MicroLunaDenom, sdk.ZeroInt()), core.MicroSDRDenom, false},
{addrs[0], addrs[0], sdk.NewCoin(core.MicroLunaDenom, overflowOfferAmt), core.MicroSDRDenom, false},
{addrs[0], addrs[0], sdk.NewCoin(core.MicroLunaDenom, sdk.OneInt()), core.MicroLunaDenom, false},
}

for i, tc := range tests {
msg := NewMsgSwapSend(tc.fromAddress, tc.toAddress, tc.offerCoin, tc.askDenom)
if tc.expectPass {
require.Nil(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.NotNil(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
Loading