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

fix(x/accounts/lockup): prevent double withdraw (backport #21619) #21641

Merged
merged 2 commits into from
Sep 11, 2024
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
18 changes: 15 additions & 3 deletions x/accounts/defaults/lockup/lockup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"time"

"github.com/cosmos/gogoproto/proto"
Expand Down Expand Up @@ -254,6 +256,10 @@ func (bva *BaseLockup) SendCoins(

hs := bva.headerService.HeaderInfo(ctx)

if err := msg.Amount.Validate(); err != nil {
return nil, err
}

lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Amount.Denoms()...)
if err != nil {
return nil, err
Expand Down Expand Up @@ -294,15 +300,21 @@ func (bva *BaseLockup) WithdrawUnlockedCoins(
return nil, err
}

// deduplicate the denoms
denoms := make(map[string]struct{})
for _, denom := range msg.Denoms {
denoms[denom] = struct{}{}
}
uniqueDenoms := slices.Collect(maps.Keys(denoms))

hs := bva.headerService.HeaderInfo(ctx)
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Denoms...)
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, uniqueDenoms...)
if err != nil {
return nil, err
}

amount := sdk.Coins{}

for _, denom := range msg.Denoms {
for _, denom := range uniqueDenoms {
balance, err := bva.getBalance(ctx, fromAddress, denom)
if err != nil {
return nil, err
Expand Down
8 changes: 6 additions & 2 deletions x/accounts/defaults/lockup/periodic_locking_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,16 @@ func TestPeriodicAccountWithdrawUnlockedCoins(t *testing.T) {
Time: startTime.Add(time.Minute * 1),
})

_, err = acc.WithdrawUnlockedCoins(sdkCtx, &lockuptypes.MsgWithdraw{
// withdraw unlocked token
resp, err := acc.WithdrawUnlockedCoins(sdkCtx, &lockuptypes.MsgWithdraw{
Withdrawer: "owner",
ToAddress: "receiver",
Denoms: []string{"test"},
Denoms: []string{"test", "test"}, // duplicate tokens should be ignored
})
require.NoError(t, err)
require.Equal(t, resp.AmountReceived.Len(), 1)
require.Equal(t, resp.AmountReceived, sdk.NewCoins(sdk.NewCoin("test", math.NewInt(5))))
require.Equal(t, resp.Receiver, "receiver")
}

func TestPeriodicAccountGetLockCoinInfo(t *testing.T) {
Expand Down
105 changes: 59 additions & 46 deletions x/accounts/defaults/multisig/v1/multisig.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,44 @@ syntax = "proto3";
package cosmos.accounts.defaults.multisig.v1;

import "google/protobuf/any.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "cosmossdk.io/x/accounts/defaults/multisig/v1";

// MsgInit is used to initialize a multisig account.
message MsgInit {
repeated Member members = 1;
Config Config = 2;
Config config = 2;
}

// MsgInitResponse is the response returned after account initialization.
message MsgInitResponse {}

// MsgCreateProposal creates a new proposal.
message MsgCreateProposal {
Proposal proposal = 1;
}

// MsgCreateProposalResponse is the response returned after creating a proposal.
message MsgCreateProposalResponse {
uint64 proposal_id = 1;
}

// MsgVote is used to vote on a proposal.
message MsgVote {
uint64 proposal_id = 1;
VoteOption vote = 2;
}

// MsgVoteResponse is the response returned after voting on a proposal.
message MsgVoteResponse {}

// MsgExecuteProposal is used to execute a proposal.
message MsgExecuteProposal {
uint64 proposal_id = 1;
}

// MsgExecuteProposalResponse is the response returned after executing a proposal.
message MsgExecuteProposalResponse {
repeated google.protobuf.Any responses = 1;
}
Expand All @@ -46,11 +51,13 @@ message MsgUpdateConfig {
repeated Member update_members = 1;

// not all fields from Config can be changed
Config Config = 2;
Config config = 2;
}

// MsgUpdateConfigResponse is the response returned after updating the config.
message MsgUpdateConfigResponse {}

// Member defines the member of the multisig account.
message Member {
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
uint64 weight = 2;
Expand All @@ -59,6 +66,7 @@ message Member {
// when aggregating on-chain I can only use the address
// off-chain would send a tx directly, won't create a proposal.

// Config defines the configuration of the multisig account.
message Config {
int64 threshold = 1;

Expand All @@ -74,6 +82,7 @@ message Config {
bool early_execution = 5;
}

// Proposal defines the structure of a proposal.
message Proposal {
string title = 1;
string summary = 2;
Expand All @@ -94,19 +103,22 @@ message QuerySequenceResponse {
uint64 sequence = 1;
}

// QueryConfig is the request for the account config.
message QueryConfig {}

// QueryConfigResponse returns the config of the account.
message QueryConfigResponse {
repeated Member members = 1;

Config Config = 2;
Config config = 2;
}

// QueryProposal is the request for a proposal.
message QueryProposal {
uint64 proposal_id = 1;
}

// QueryProposalResponse returns the proposal.
message QueryProposalResponse {
Proposal proposal = 1;
}
Expand Down
Loading