Skip to content

Commit 89a0948

Browse files
committed
fix: types: correctly coalesce coins even with repeated denominations & simplify logic
This code correctly coalesces coins with repeated denominations which can come from mistakenly duplicated or split up coins being passed in. However, the code MUST always function correctly regardless of assumptions that no one should ever pass in duplicated denominations. While here simplified the prior clever but hard to understand code, by making the code directly implementing deduplicating the coalescing. Credit to the Ingenuity/Quicksilver codebase which exposed this problem from an observation we made. Fixes #13234
1 parent 641ab20 commit 89a0948

File tree

2 files changed

+54
-51
lines changed

2 files changed

+54
-51
lines changed

types/coin.go

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ func (coins Coins) Add(coinsB ...Coin) Coins {
319319
// denomination and addition only occurs when the denominations match, otherwise
320320
// the coin is simply added to the sum assuming it's not zero.
321321
// The function panics if `coins` or `coinsB` are not sorted (ascending).
322-
func (coins Coins) safeAdd(coinsB Coins) Coins {
322+
func (coins Coins) safeAdd(coinsB Coins) (coalesced Coins) {
323323
// probably the best way will be to make Coins and interface and hide the structure
324324
// definition (type alias)
325325
if !coins.isSorted() {
@@ -329,51 +329,24 @@ func (coins Coins) safeAdd(coinsB Coins) Coins {
329329
panic("Wrong argument: coins must be sorted")
330330
}
331331

332-
sum := ([]Coin)(nil)
333-
indexA, indexB := 0, 0
334-
lenA, lenB := len(coins), len(coinsB)
335-
336-
for {
337-
if indexA == lenA {
338-
if indexB == lenB {
339-
// return nil coins if both sets are empty
340-
return sum
341-
}
342-
343-
// return set B (excluding zero coins) if set A is empty
344-
return append(sum, removeZeroCoins(coinsB[indexB:])...)
345-
} else if indexB == lenB {
346-
// return set A (excluding zero coins) if set B is empty
347-
return append(sum, removeZeroCoins(coins[indexA:])...)
332+
uniqCoins := make(map[string]Coins, len(coins)+len(coinsB))
333+
// Traverse all the coins for each of the coins and coinsB.
334+
for _, cL := range []Coins{coins, coinsB} {
335+
for _, c := range cL {
336+
uniqCoins[c.Denom] = append(uniqCoins[c.Denom], c)
348337
}
338+
}
349339

350-
coinA, coinB := coins[indexA], coinsB[indexB]
351-
352-
switch strings.Compare(coinA.Denom, coinB.Denom) {
353-
case -1: // coin A denom < coin B denom
354-
if !coinA.IsZero() {
355-
sum = append(sum, coinA)
356-
}
357-
358-
indexA++
359-
360-
case 0: // coin A denom == coin B denom
361-
res := coinA.Add(coinB)
362-
if !res.IsZero() {
363-
sum = append(sum, res)
364-
}
365-
366-
indexA++
367-
indexB++
368-
369-
case 1: // coin A denom > coin B denom
370-
if !coinB.IsZero() {
371-
sum = append(sum, coinB)
372-
}
373-
374-
indexB++
340+
for denom, cL := range uniqCoins {
341+
comboCoin := Coin{Denom: denom, Amount: NewInt(0)}
342+
for _, c := range cL {
343+
comboCoin = comboCoin.Add(c)
344+
}
345+
if !comboCoin.IsZero() {
346+
coalesced = append(coalesced, comboCoin)
375347
}
376348
}
349+
return coalesced.Sort()
377350
}
378351

379352
// DenomsSubsetOf returns true if receiver's denom set

types/coin_test.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"cosmossdk.io/math"
9+
"github.com/stretchr/testify/require"
910
"github.com/stretchr/testify/suite"
1011

1112
"github.com/cosmos/cosmos-sdk/codec"
@@ -536,21 +537,50 @@ func (s *coinTestSuite) TestEqualCoins() {
536537

537538
func (s *coinTestSuite) TestAddCoins() {
538539
cases := []struct {
540+
name string
539541
inputOne sdk.Coins
540542
inputTwo sdk.Coins
541543
expected sdk.Coins
542544
}{
543-
{sdk.Coins{s.ca1, s.cm1}, sdk.Coins{s.ca1, s.cm1}, sdk.Coins{s.ca2, s.cm2}},
544-
{sdk.Coins{s.ca0, s.cm1}, sdk.Coins{s.ca0, s.cm0}, sdk.Coins{s.cm1}},
545-
{sdk.Coins{s.ca2}, sdk.Coins{s.cm0}, sdk.Coins{s.ca2}},
546-
{sdk.Coins{s.ca1}, sdk.Coins{s.ca1, s.cm2}, sdk.Coins{s.ca2, s.cm2}},
547-
{sdk.Coins{s.ca0, s.cm0}, sdk.Coins{s.ca0, s.cm0}, sdk.Coins(nil)},
545+
{"{1atom,1muon}+{1atom,1muon}", sdk.Coins{s.ca1, s.cm1}, sdk.Coins{s.ca1, s.cm1}, sdk.Coins{s.ca2, s.cm2}},
546+
{"{0atom,1muon}+{0atom,0muon}", sdk.Coins{s.ca0, s.cm1}, sdk.Coins{s.ca0, s.cm0}, sdk.Coins{s.cm1}},
547+
{"{2atom}+{0muon}", sdk.Coins{s.ca2}, sdk.Coins{s.cm0}, sdk.Coins{s.ca2}},
548+
{"{1atom}+{1atom,2muon}", sdk.Coins{s.ca1}, sdk.Coins{s.ca1, s.cm2}, sdk.Coins{s.ca2, s.cm2}},
549+
{"{0atom,0muon}+{0atom,0muon}", sdk.Coins{s.ca0, s.cm0}, sdk.Coins{s.ca0, s.cm0}, sdk.Coins(nil)},
548550
}
549551

550-
for tcIndex, tc := range cases {
551-
res := tc.inputOne.Add(tc.inputTwo...)
552-
s.Require().True(res.IsValid())
553-
s.Require().Equal(tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex)
552+
for _, tc := range cases {
553+
s.T().Run(tc.name, func(t *testing.T) {
554+
res := tc.inputOne.Add(tc.inputTwo...)
555+
require.True(t, res.IsValid(), fmt.Sprintf("%s + %s = %s", tc.inputOne, tc.inputTwo, res))
556+
require.Equal(t, tc.expected, res, "sum of coins is incorrect")
557+
})
558+
}
559+
}
560+
561+
// Tests that even if coins with repeated denominations are passed into .Add that they
562+
// are correctly coalesced. Please see issue https://github.com/cosmos/cosmos-sdk/issues/13234
563+
func TestCoinsAddCoalescesDuplicateDenominations(t *testing.T) {
564+
A := sdk.Coins{
565+
{"den", sdk.NewInt(2)},
566+
{"den", sdk.NewInt(3)},
567+
}
568+
B := sdk.Coins{
569+
{"den", sdk.NewInt(3)},
570+
{"den", sdk.NewInt(2)},
571+
{"den", sdk.NewInt(1)},
572+
}
573+
574+
A = A.Sort()
575+
B = B.Sort()
576+
got := A.Add(B...)
577+
578+
want := sdk.Coins{
579+
{"den", sdk.NewInt(11)},
580+
}
581+
582+
if !got.IsEqual(want) {
583+
t.Fatalf("Wrong result\n\tGot: %s\n\tWant: %s", got, want)
554584
}
555585
}
556586

0 commit comments

Comments
 (0)