Skip to content

Commit

Permalink
feat: support multisend (#286)
Browse files Browse the repository at this point in the history
* support multisend

* bump movevm to v0.5.1

* add missing stargate query support (#285)

* fix nil memory access on authz (#281)

* fix: allow to be failed with invalid message without error (#283)

* allow to failed with invalid message

* set reason

* fix to consider movevm gas scale when we use infinity gas meter (#287)

* fix to use cache context at ibc hook (#288)

* feat: enable whitelist stableswap (#289)

* enable whitelist stableswap

* check division by zero and handle default values for balancer

* remove unnecessary slices.Copy

* ignore error

* fix test

* apply coderabbit comment

* emit same events with cosmos-sdk interface

* create account if not exists
  • Loading branch information
beer-1 authored Oct 24, 2024
1 parent aeacb63 commit 342a327
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ARG TARGETARCH
ARG GOARCH

# See https://github.com/initia-labs/movevm/releases
ENV LIBMOVEVM_VERSION=v0.5.0
ENV LIBMOVEVM_VERSION=v0.5.1

# Install necessary packages
RUN set -eux; apk add --no-cache ca-certificates build-base git cmake
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ require (
github.com/hashicorp/go-metrics v0.5.3
github.com/initia-labs/OPinit v0.5.5
// we also need to update `LIBMOVEVM_VERSION` of Dockerfile#9
github.com/initia-labs/movevm v0.5.0
github.com/initia-labs/movevm v0.5.1
github.com/noble-assets/forwarding/v2 v2.0.0-20240521090705-86712c4c9e43
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,8 @@ github.com/initia-labs/OPinit/api v0.5.1 h1:zwyJf7HtKJCKvLJ1R9PjVfJO1L+d/jKoeFyT
github.com/initia-labs/OPinit/api v0.5.1/go.mod h1:gHK6DEWb3/DqQD5LjKirUx9jilAh2UioXanoQdgqVfU=
github.com/initia-labs/cometbft v0.0.0-20240923045653-ba99eb347236 h1:+HmPQ1uptOe4r5oQHuHMG5zF1F3maNoEba5uiTUMnlk=
github.com/initia-labs/cometbft v0.0.0-20240923045653-ba99eb347236/go.mod h1:GPHp3/pehPqgX1930HmK1BpBLZPxB75v/dZg8Viwy+o=
github.com/initia-labs/movevm v0.5.0 h1:dBSxoVyUumSE4x6/ZSOWtvbtZpw+V4W25/NH6qLU0uQ=
github.com/initia-labs/movevm v0.5.0/go.mod h1:aUWdvFZPdULjJ2McQTE+mLnfnG3CLAz0TWJRFzFFUwg=
github.com/initia-labs/movevm v0.5.1 h1:Nl5SizJIfRLM6clz/zV8dOFUXcnlMP2wOQsZB/mmU2w=
github.com/initia-labs/movevm v0.5.1/go.mod h1:aUWdvFZPdULjJ2McQTE+mLnfnG3CLAz0TWJRFzFFUwg=
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
Expand Down
49 changes: 46 additions & 3 deletions x/bank/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"

"github.com/hashicorp/go-metrics"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/telemetry"
Expand Down Expand Up @@ -85,7 +83,52 @@ func (k msgServer) Send(goCtx context.Context, msg *types.MsgSend) (*types.MsgSe
}

func (k msgServer) MultiSend(goCtx context.Context, msg *types.MsgMultiSend) (*types.MsgMultiSendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "not supported")
if len(msg.Inputs) == 0 {
return nil, types.ErrNoInputs
}

if len(msg.Inputs) != 1 {
return nil, types.ErrMultipleSenders
}

if len(msg.Outputs) == 0 {
return nil, types.ErrNoOutputs
}

in := msg.Inputs[0]
if err := types.ValidateInputOutputs(in, msg.Outputs); err != nil {
return nil, err
}

ctx := sdk.UnwrapSDKContext(goCtx)

// NOTE: totalIn == totalOut should already have been checked
if err := k.IsSendEnabledCoins(ctx, in.Coins...); err != nil {
return nil, err
}

if base, ok := k.Keeper.(BaseKeeper); ok {
for _, out := range msg.Outputs {
accAddr, err := base.ak.AddressCodec().StringToBytes(out.Address)
if err != nil {
return nil, err
}

if k.BlockedAddr(accAddr) {
return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", out.Address)
}

}
} else {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}

err := k.InputOutputCoins(ctx, msg.Inputs[0], msg.Outputs)
if err != nil {
return nil, err
}

return &types.MsgMultiSendResponse{}, nil
}

func (k msgServer) UpdateParams(goCtx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
Expand Down
114 changes: 114 additions & 0 deletions x/bank/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,120 @@ func TestMsgSend(t *testing.T) {
}
}

func TestMsgMultiSend(t *testing.T) {
ctx, input := createDefaultTestInput(t)

origDenom := "sendableCoin"
origCoins := sdk.NewCoins(sdk.NewInt64Coin(origDenom, 100))
sendCoins := sdk.NewCoins(sdk.NewInt64Coin(origDenom, 50))
input.BankKeeper.SetSendEnabled(ctx, origDenom, true)

testCases := []struct {
name string
input *banktypes.MsgMultiSend
expErr bool
expErrMsg string
}{
{
name: "no inputs to send transaction",
input: &banktypes.MsgMultiSend{},
expErr: true,
expErrMsg: "no inputs to send transaction",
},
{
name: "no inputs to send transaction",
input: &banktypes.MsgMultiSend{
Outputs: []banktypes.Output{
{Address: addrs[4].String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "no inputs to send transaction",
},
{
name: "more than one inputs to send transaction",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
{Address: addrs[0].String(), Coins: origCoins},
},
},
expErr: true,
expErrMsg: "multiple senders not allowed",
},
{
name: "no outputs to send transaction",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
},
expErr: true,
expErrMsg: "no outputs to send transaction",
},
{
name: "invalid send to blocked address",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: sendCoins},
{Address: authtypes.NewModuleAddress(govtypes.ModuleName).String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "is not allowed to receive funds",
},
{
name: "input/output amount mismatch",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: origCoins},
{Address: addrs[2].String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "sum inputs != sum outputs",
},
{
name: "valid send",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: sendCoins},
{Address: addrs[2].String(), Coins: sendCoins},
},
},
expErr: false,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if len(tc.input.Inputs) > 0 && !tc.input.Inputs[0].Coins.IsZero() && tc.input.Inputs[0].Address != "" {
fromAddr, err := input.AccountKeeper.AddressCodec().StringToBytes(tc.input.Inputs[0].Address)
require.NoError(t, err)
input.Faucet.Fund(ctx, fromAddr, tc.input.Inputs[0].Coins...)
}

_, err := bankkeeper.NewMsgServerImpl(input.BankKeeper).MultiSend(ctx, tc.input)
if tc.expErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expErrMsg)
} else {
require.NoError(t, err)
}
})
}
}

func TestMsgSetSendEnabled(t *testing.T) {
ctx, input := createDefaultTestInput(t)

Expand Down
92 changes: 86 additions & 6 deletions x/bank/keeper/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"fmt"

"cosmossdk.io/core/store"
"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
cosmosbank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/cosmos/cosmos-sdk/x/bank/types"

Expand Down Expand Up @@ -104,11 +104,91 @@ func (k MoveSendKeeper) SetParams(ctx context.Context, params types.Params) erro
return k.Params.Set(ctx, params)
}

// InputOutputCoins performs multi-send functionality. It accepts a series of
// inputs that correspond to a series of outputs. It returns an error if the
// inputs and outputs don't lineup or if any single transfer of tokens fails.
func (k MoveSendKeeper) InputOutputCoins(ctx context.Context, inputs types.Input, outputs []types.Output) error {
return sdkerrors.ErrNotSupported
// InputOutputCoins performs multi-send functionality. It transfers coins from a single sender
// to multiple recipients. An error is returned upon failure.
func (k MoveSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, outputs []types.Output) error {
// Safety check ensuring that when sending coins the keeper must maintain the
// Check supply invariant and validity of Coins.
if err := types.ValidateInputOutputs(input, outputs); err != nil {
return err
}

fromAddr, err := k.ak.AddressCodec().StringToBytes(input.Address)
if err != nil {
return err
}

// event emission
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(types.AttributeKeySender, input.Address),
),
)

// emit coin spent event
sdkCtx.EventManager().EmitEvent(
types.NewCoinSpentEvent(fromAddr, input.Coins),
)

// emit coin received events and do address caching
addrMap := make(map[string][]byte)
for _, output := range outputs {
addr, err := k.ak.AddressCodec().StringToBytes(output.Address)
if err != nil {
return err
}

// cache bytes address
addrMap[output.Address] = addr

// emit coin received event
sdkCtx.EventManager().EmitEvent(
types.NewCoinReceivedEvent(addr, output.Coins),
)

// emit transfer event (for compatibility with cosmos bank)
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeTransfer,
sdk.NewAttribute(types.AttributeKeyRecipient, output.Address),
sdk.NewAttribute(sdk.AttributeKeyAmount, output.Coins.String()),
),
)
}

for _, coin := range input.Coins {
if !coin.Amount.IsPositive() {
continue
}

recipients := make([]sdk.AccAddress, 0, len(outputs))
amounts := make([]math.Int, 0, len(outputs))
for _, output := range outputs {
// Create account if recipient does not exist.
outAddress := addrMap[output.Address]
accExists := k.ak.HasAccount(ctx, outAddress)
if !accExists {
defer telemetry.IncrCounter(1, "new", "account")
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, outAddress))
}

amount := output.Coins.AmountOf(coin.Denom)
if !amount.IsPositive() {
continue
}

recipients = append(recipients, outAddress)
amounts = append(amounts, output.Coins.AmountOf(coin.Denom))
}

if err = k.mk.MultiSend(ctx, fromAddr, coin.Denom, recipients, amounts); err != nil {
return err
}
}

return nil
}

// SendCoins transfers amt coins from a sending account to a receiving account.
Expand Down
1 change: 1 addition & 0 deletions x/bank/types/expected_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MoveBankKeeper interface {
SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
MintCoins(ctx context.Context, addr sdk.AccAddress, amount sdk.Coins) error
BurnCoins(ctx context.Context, addr sdk.AccAddress, amount sdk.Coins) error
MultiSend(ctx context.Context, fromAddr sdk.AccAddress, denom string, toAddrs []sdk.AccAddress, amounts []math.Int) error

// supply
GetSupply(ctx context.Context, denom string) (math.Int, error)
Expand Down
Loading

0 comments on commit 342a327

Please sign in to comment.