diff --git a/x/smartaccount/ante/pretransaction.go b/x/smartaccount/ante/pretransaction.go index 4fbb9c5a..7947a635 100644 --- a/x/smartaccount/ante/pretransaction.go +++ b/x/smartaccount/ante/pretransaction.go @@ -3,14 +3,13 @@ package ante import ( "encoding/json" - sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrortypes "github.com/cosmos/cosmos-sdk/types/errors" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" tx2 "github.com/cosmos/cosmos-sdk/types/tx" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" wasmvmtypes "github.com/CosmWasm/wasmvm/types" - smartaccounttypes "github.com/terra-money/core/v2/x/smartaccount/types" + "github.com/terra-money/core/v2/x/smartaccount/types" ) // SmartAccountCheckDecorator does authentication for smart accounts @@ -28,7 +27,7 @@ func NewPreTransactionHookDecorator(sak SmartAccountKeeper, wk WasmKeeper) PreTr // AnteHandle checks if the tx provides sufficient fee to cover the required fee from the fee market. func (pth PreTransactionHookDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - setting, ok := ctx.Value(smartaccounttypes.ModuleName).(smartaccounttypes.Setting) + setting, ok := ctx.Value(types.ModuleName).(types.Setting) if !ok { return next(ctx, tx, simulate) } @@ -54,10 +53,10 @@ func (pth PreTransactionHookDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, si } // TODO: to refactor -func BuildPreTransactionHookMsg(tx sdk.Tx) ([]byte, error) { +func BuildPrePostTransactionHookMsg(tx sdk.Tx, isPreTx bool) ([]byte, error) { sigTx, ok := tx.(authsigning.SigVerifiableTx) if !ok { - return nil, sdkerrors.Wrap(sdkerrortypes.ErrInvalidType, "expected SigVerifiableTx") + return nil, sdkerrors.ErrInvalidType.Wrap("expected SigVerifiableTx") } // Signer here is the account that the state transition is affecting @@ -65,14 +64,14 @@ func BuildPreTransactionHookMsg(tx sdk.Tx) ([]byte, error) { signers := sigTx.GetSigners() // Current only supports one signer (TODO review in the future) if len(signers) != 1 { - return nil, sdkerrors.Wrap(sdkerrortypes.ErrorInvalidSigner, "only one signer is supported") + return nil, sdkerrors.ErrorInvalidSigner.Wrap("only one signer is supported") } // Sender here is the account that signed the transaction // Could be different from the account above (confusingly named signer) signatures, _ := sigTx.GetSignaturesV2() if len(signatures) == 0 { - return nil, sdkerrors.Wrap(sdkerrortypes.ErrNoSignatures, "no signatures found") + return nil, sdkerrors.ErrNoSignatures.Wrap("no signatures found") } senderAddr, err := sdk.AccAddressFromHexUnsafe(signatures[0].PubKey.Address().String()) if err != nil { @@ -94,11 +93,26 @@ func BuildPreTransactionHookMsg(tx sdk.Tx) ([]byte, error) { Stargate: &stargateMsg, }) } - preTx := smartaccounttypes.PreTransaction{ - Sender: senderAddr.String(), - Account: signers[0].String(), - Messages: stargateMsgs, + var msg types.SudoMsg + if isPreTx { + preTx := types.PreTransaction{ + Sender: senderAddr.String(), + Account: signers[0].String(), + Messages: stargateMsgs, + } + msg = types.SudoMsg{PreTransaction: &preTx} + } else { + postTx := types.PostTransaction{ + Sender: senderAddr.String(), + Account: signers[0].String(), + Messages: stargateMsgs, + } + msg = types.SudoMsg{PostTransaction: &postTx} } - msg := smartaccounttypes.SudoMsg{PreTransaction: &preTx} + return json.Marshal(msg) } + +func BuildPreTransactionHookMsg(tx sdk.Tx) ([]byte, error) { + return BuildPrePostTransactionHookMsg(tx, true) +} diff --git a/x/smartaccount/ante/tests/ante_test.go b/x/smartaccount/ante/tests/auth_test.go similarity index 100% rename from x/smartaccount/ante/tests/ante_test.go rename to x/smartaccount/ante/tests/auth_test.go diff --git a/x/smartaccount/post/expected_keepers.go b/x/smartaccount/post/expected_keepers.go index 47d55fd5..3a346f91 100644 --- a/x/smartaccount/post/expected_keepers.go +++ b/x/smartaccount/post/expected_keepers.go @@ -9,3 +9,7 @@ import ( type SmartAccountKeeper interface { GetSetting(ctx sdk.Context, ownerAddr string) (*smartaccounttypes.Setting, error) } + +type WasmKeeper interface { + Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) +} diff --git a/x/smartaccount/post/posttransaction.go b/x/smartaccount/post/posttransaction.go new file mode 100644 index 00000000..2e601724 --- /dev/null +++ b/x/smartaccount/post/posttransaction.go @@ -0,0 +1,60 @@ +package post + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/terra-money/core/v2/x/smartaccount/ante" + "github.com/terra-money/core/v2/x/smartaccount/types" +) + +// PostTransactionHookDecorator does authentication for smart accounts +type PostTransactionHookDecorator struct { + smartaccountKeeper SmartAccountKeeper + wasmKeeper WasmKeeper +} + +func NewPostTransactionHookDecorator(sak SmartAccountKeeper, wk WasmKeeper) PostTransactionHookDecorator { + return PostTransactionHookDecorator{ + smartaccountKeeper: sak, + wasmKeeper: wk, + } +} + +// FeeSharePostHandler if the smartaccount module is enabled +// takes the total fees paid for each transaction and +// split these fees equally between all the contacts +// involved in the transaction based on the module params. +func (pth PostTransactionHookDecorator) PostHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + success bool, + next sdk.PostHandler, +) (newCtx sdk.Context, err error) { + setting, ok := ctx.Value(types.ModuleName).(types.Setting) + if !ok { + return next(ctx, tx, simulate, success) + } + + if setting.PostTransaction != nil && len(setting.PostTransaction) > 0 { + for _, postTx := range setting.PostTransaction { + contractAddr, err := sdk.AccAddressFromBech32(postTx) + if err != nil { + return ctx, err + } + data, err := BuildPostTransactionHookMsg(tx) + if err != nil { + return ctx, err + } + _, err = pth.wasmKeeper.Sudo(ctx, contractAddr, data) + if err != nil { + return ctx, err + } + } + } + return next(ctx, tx, simulate, success) +} + +func BuildPostTransactionHookMsg(tx sdk.Tx) ([]byte, error) { + return ante.BuildPrePostTransactionHookMsg(tx, false) +} diff --git a/x/smartaccount/post/posttransaction_test.go b/x/smartaccount/post/posttransaction_test.go new file mode 100644 index 00000000..6e4899e1 --- /dev/null +++ b/x/smartaccount/post/posttransaction_test.go @@ -0,0 +1,162 @@ +package post_test + +import ( + "testing" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/terra-money/core/v2/x/smartaccount/post" + "github.com/terra-money/core/v2/x/smartaccount/test_helpers" + smartaccounttypes "github.com/terra-money/core/v2/x/smartaccount/types" +) + +type PostTxTestSuite struct { + test_helpers.SmartAccountTestSuite + + PostTxDecorator post.PostTransactionHookDecorator + WasmKeeper *wasmkeeper.PermissionedKeeper +} + +func TestAnteSuite(t *testing.T) { + suite.Run(t, new(PostTxTestSuite)) +} + +func (s *PostTxTestSuite) Setup() { + s.SmartAccountTestSuite.SetupTests() + s.WasmKeeper = wasmkeeper.NewDefaultPermissionKeeper(s.App.Keepers.WasmKeeper) + s.PostTxDecorator = post.NewPostTransactionHookDecorator(s.SmartAccountKeeper, s.WasmKeeper) + s.Ctx = s.Ctx.WithChainID("test") +} + +func (s *PostTxTestSuite) TestPostTransactionHookWithoutSmartAccount() { + s.Setup() + txBuilder := s.BuildDefaultMsgTx(0, &types.MsgSend{ + FromAddress: s.TestAccs[0].String(), + ToAddress: s.TestAccs[1].String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("uluna", 100000000)), + }) + _, err := s.PostTxDecorator.PostHandle(s.Ctx, txBuilder.GetTx(), false, true, sdk.ChainPostDecorators(sdk.Terminator{})) + require.NoError(s.T(), err) +} + +func (s *PostTxTestSuite) TestPostTransactionHookWithEmptySmartAccount() { + s.Setup() + s.Ctx = s.Ctx.WithValue(smartaccounttypes.ModuleName, smartaccounttypes.Setting{}) + txBuilder := s.BuildDefaultMsgTx(0, &types.MsgSend{ + FromAddress: s.TestAccs[0].String(), + ToAddress: s.TestAccs[1].String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("uluna", 100000000)), + }) + _, err := s.PostTxDecorator.PostHandle(s.Ctx, txBuilder.GetTx(), false, true, sdk.ChainPostDecorators(sdk.Terminator{})) + require.NoError(s.T(), err) +} + +func (s *PostTxTestSuite) TestInvalidContractAddress() { + s.Setup() + s.Ctx = s.Ctx.WithValue(smartaccounttypes.ModuleName, smartaccounttypes.Setting{ + PostTransaction: []string{s.TestAccs[0].String()}, + }) + txBuilder := s.BuildDefaultMsgTx(0, &types.MsgSend{ + FromAddress: s.TestAccs[0].String(), + ToAddress: s.TestAccs[1].String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("uluna", 100000000)), + }) + _, err := s.PostTxDecorator.PostHandle(s.Ctx, txBuilder.GetTx(), false, true, sdk.ChainPostDecorators(sdk.Terminator{})) + require.ErrorContainsf(s.T(), err, "no such contract", "error message: %s", err) +} + +func (s *PostTxTestSuite) TestSendCoinsWithLimitSendHook() { + s.Setup() + + acc := s.TestAccs[0] + codeId, _, err := s.WasmKeeper.Create(s.Ctx, acc, test_helpers.LimitSendOnlyHookWasm, nil) + require.NoError(s.T(), err) + contractAddr, _, err := s.WasmKeeper.Instantiate(s.Ctx, codeId, acc, acc, []byte("{}"), "limit send", sdk.NewCoins()) + require.NoError(s.T(), err) + + s.Ctx = s.Ctx.WithValue(smartaccounttypes.ModuleName, smartaccounttypes.Setting{ + PostTransaction: []string{contractAddr.String()}, + }) + txBuilder := s.BuildDefaultMsgTx(0, &types.MsgSend{ + FromAddress: acc.String(), + ToAddress: acc.String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("uluna", 100000000)), + }) + _, err = s.PostTxDecorator.PostHandle(s.Ctx, txBuilder.GetTx(), false, true, sdk.ChainPostDecorators(sdk.Terminator{})) + require.NoError(s.T(), err) +} + +func (s *PostTxTestSuite) TestStakingWithLimitSendHook() { + s.Setup() + + acc := s.TestAccs[0] + codeId, _, err := s.WasmKeeper.Create(s.Ctx, acc, test_helpers.LimitSendOnlyHookWasm, nil) + require.NoError(s.T(), err) + contractAddr, _, err := s.WasmKeeper.Instantiate(s.Ctx, codeId, acc, acc, []byte("{}"), "limit send", sdk.NewCoins()) + require.NoError(s.T(), err) + + s.Ctx = s.Ctx.WithValue(smartaccounttypes.ModuleName, smartaccounttypes.Setting{ + PostTransaction: []string{contractAddr.String()}, + }) + txBuilder := s.BuildDefaultMsgTx(0, &stakingtypes.MsgDelegate{ + DelegatorAddress: acc.String(), + ValidatorAddress: acc.String(), + Amount: sdk.NewInt64Coin("uluna", 100000000), + }) + _, err = s.PostTxDecorator.PostHandle(s.Ctx, txBuilder.GetTx(), false, true, sdk.ChainPostDecorators(sdk.Terminator{})) + require.ErrorContainsf(s.T(), err, "Unauthorized message type", "error message: %s", err) +} + +func (s *PostTxTestSuite) BuildDefaultMsgTx(accountIndex int, msgs ...sdk.Msg) client.TxBuilder { + pk := s.TestAccPrivs[accountIndex] + sender := s.TestAccs[accountIndex] + acc := s.App.Keepers.AccountKeeper.GetAccount(s.Ctx, msgs[0].GetSigners()[0]) + txBuilder := s.EncodingConfig.TxConfig.NewTxBuilder() + err := txBuilder.SetMsgs( + msgs..., + ) + require.NoError(s.T(), err) + + signer := authsigning.SignerData{ + Address: sender.String(), + ChainID: "test", + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + PubKey: pk.PubKey(), + } + + emptySig := signing.SignatureV2{ + PubKey: signer.PubKey, + Data: &signing.SingleSignatureData{ + SignMode: s.EncodingConfig.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: signer.Sequence, + } + + err = txBuilder.SetSignatures(emptySig) + require.NoError(s.T(), err) + + sigV2, err := tx.SignWithPrivKey( + s.EncodingConfig.TxConfig.SignModeHandler().DefaultMode(), + signer, + txBuilder, + pk, + s.EncodingConfig.TxConfig, + acc.GetSequence(), + ) + require.NoError(s.T(), err) + + err = txBuilder.SetSignatures(sigV2) + require.NoError(s.T(), err) + + return txBuilder +} diff --git a/x/smartaccount/post/smartaccount_posttx.go b/x/smartaccount/post/smartaccount_posttx.go deleted file mode 100644 index 5f75034d..00000000 --- a/x/smartaccount/post/smartaccount_posttx.go +++ /dev/null @@ -1,45 +0,0 @@ -package post - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" -) - -// SmartAccountCheckDecorator does authentication for smart accounts -type SmartAccountPostTxDecorator struct { - smartaccountKeeper SmartAccountKeeper -} - -func NewSmartAccountPostTxDecorator(sak SmartAccountKeeper) SmartAccountPostTxDecorator { - return SmartAccountPostTxDecorator{ - smartaccountKeeper: sak, - } -} - -// FeeSharePostHandler if the smartaccount module is enabled -// takes the total fees paid for each transaction and -// split these fees equally between all the contacts -// involved in the transaction based on the module params. -func (sad SmartAccountPostTxDecorator) PostHandle( - ctx sdk.Context, - tx sdk.Tx, - simulate bool, - success bool, - next sdk.PostHandler, -) (newCtx sdk.Context, err error) { - - setting, err := sad.smartaccountKeeper.GetSetting(ctx, tx.GetMsgs()[0].GetSigners()[0].String()) - if sdkerrors.ErrKeyNotFound.Is(err) { - return next(ctx, tx, simulate, success) - } else if err != nil { - return ctx, err - } - - if setting.PostTransaction != nil && len(setting.PostTransaction) > 0 { - for _, postTx := range setting.PostTransaction { - _ = postTx - // TODO: add code that calls post-transaction on contracts - } - } - return next(ctx, tx, simulate, success) -} diff --git a/x/smartaccount/test_helpers/test_data/limit_send_only_hooks.wasm b/x/smartaccount/test_helpers/test_data/limit_send_only_hooks.wasm index 08d10780..b58d3962 100644 Binary files a/x/smartaccount/test_helpers/test_data/limit_send_only_hooks.wasm and b/x/smartaccount/test_helpers/test_data/limit_send_only_hooks.wasm differ diff --git a/x/smartaccount/types/wasm.go b/x/smartaccount/types/wasm.go index df7713cd..804078b5 100644 --- a/x/smartaccount/types/wasm.go +++ b/x/smartaccount/types/wasm.go @@ -30,7 +30,7 @@ type PreTransaction struct { } type PostTransaction struct { - Sender string `json:"sender"` - Account string `json:"account"` - Msgs []types.CosmosMsg `json:"msgs"` + Sender string `json:"sender"` + Account string `json:"account"` + Messages []types.CosmosMsg `json:"msgs"` }