mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-10-31 23:17:27 +00:00 
			
		
		
		
	Minor audit fixes (#540)
* use mod and quo to calculate how many auctions to start * check both return values in getDenomPrefix * split auctions using weighted buckets
This commit is contained in:
		
							parent
							
								
									aebb3093ff
								
							
						
					
					
						commit
						c049aad495
					
				| @ -32,32 +32,33 @@ func (k Keeper) CreateAuctionsFromDeposit( | |||||||
| 	ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int, | 	ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int, | ||||||
| 	principalDenom string) (err error) { | 	principalDenom string) (err error) { | ||||||
| 
 | 
 | ||||||
| 	amountToAuction := collateral.Amount | 	// the number of auctions to start with lot = auctionSize
 | ||||||
| 	totalCollateralAmount := collateral.Amount | 	wholeAuctions := collateral.Amount.Quo(auctionSize) | ||||||
| 	remainingDebt := debt | 	// remaining collateral (< lot) to auction
 | ||||||
| 	if !amountToAuction.IsPositive() { | 	partialAuctionAmount := collateral.Amount.Mod(auctionSize) | ||||||
| 		return nil | 	auctionLots := []sdk.Int{} | ||||||
| 	} |  | ||||||
| 	for amountToAuction.GT(auctionSize) { |  | ||||||
| 		debtCoveredByAuction := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateralAmount))).Mul(sdk.NewDecFromInt(debt)).RoundInt() |  | ||||||
| 		penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtCoveredByAuction) |  | ||||||
| 		_, err := k.auctionKeeper.StartCollateralAuction( |  | ||||||
| 			ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, auctionSize), sdk.NewCoin(principalDenom, debtCoveredByAuction.Add(penalty)), []sdk.AccAddress{returnAddr}, |  | ||||||
| 			[]sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), debtCoveredByAuction)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		amountToAuction = amountToAuction.Sub(auctionSize) |  | ||||||
| 		remainingDebt = remainingDebt.Sub(debtCoveredByAuction) |  | ||||||
| 	} |  | ||||||
| 	penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, remainingDebt) |  | ||||||
| 	_, err = k.auctionKeeper.StartCollateralAuction( |  | ||||||
| 		ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, amountToAuction), sdk.NewCoin(principalDenom, remainingDebt.Add(penalty)), []sdk.AccAddress{returnAddr}, |  | ||||||
| 		[]sdk.Int{amountToAuction}, sdk.NewCoin(k.GetDebtDenom(ctx), remainingDebt)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
|  | 	for i := int64(0); i < wholeAuctions.Int64(); i++ { | ||||||
|  | 		auctionLots = append(auctionLots, auctionSize) | ||||||
|  | 	} | ||||||
|  | 	if partialAuctionAmount.IsPositive() { | ||||||
|  | 		auctionLots = append(auctionLots, partialAuctionAmount) | ||||||
|  | 	} | ||||||
|  | 	// use the auction lots as weights to split the debt into buckets,
 | ||||||
|  | 	// where each bucket represents how much debt that auction will attempt to cover
 | ||||||
|  | 	debtAmounts := splitIntIntoWeightedBuckets(debt, auctionLots) | ||||||
|  | 	debtDenom := k.GetDebtDenom(ctx) | ||||||
|  | 	for i, debtAmount := range debtAmounts { | ||||||
|  | 		penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtAmount) | ||||||
|  | 		_, err := k.auctionKeeper.StartCollateralAuction( | ||||||
|  | 			ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, auctionLots[i]), | ||||||
|  | 			sdk.NewCoin(principalDenom, debtAmount.Add(penalty)), []sdk.AccAddress{returnAddr}, | ||||||
|  | 			[]sdk.Int{auctionLots[i]}, sdk.NewCoin(debtDenom, debtAmount), | ||||||
|  | 		) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import ( | |||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 
 | 
 | ||||||
| 	abci "github.com/tendermint/tendermint/abci/types" | 	abci "github.com/tendermint/tendermint/abci/types" | ||||||
|  | 	"github.com/tendermint/tendermint/crypto" | ||||||
| 	tmtime "github.com/tendermint/tendermint/types/time" | 	tmtime "github.com/tendermint/tendermint/types/time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -22,14 +23,18 @@ type AuctionTestSuite struct { | |||||||
| 	keeper keeper.Keeper | 	keeper keeper.Keeper | ||||||
| 	app    app.TestApp | 	app    app.TestApp | ||||||
| 	ctx    sdk.Context | 	ctx    sdk.Context | ||||||
|  | 	addrs  []sdk.AccAddress | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *AuctionTestSuite) SetupTest() { | func (suite *AuctionTestSuite) SetupTest() { | ||||||
| 	config := sdk.GetConfig() | 	config := sdk.GetConfig() | ||||||
| 	app.SetBech32AddressPrefixes(config) | 	app.SetBech32AddressPrefixes(config) | ||||||
| 	tApp := app.NewTestApp() | 	tApp := app.NewTestApp() | ||||||
|  | 	taddr := sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))) | ||||||
|  | 	authGS := app.NewAuthGenState([]sdk.AccAddress{taddr}, []sdk.Coins{cs(c("usdx", 21000000000))}) | ||||||
| 	ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) | 	ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) | ||||||
| 	tApp.InitializeFromGenesisStates( | 	tApp.InitializeFromGenesisStates( | ||||||
|  | 		authGS, | ||||||
| 		NewPricefeedGenStateMulti(), | 		NewPricefeedGenStateMulti(), | ||||||
| 		NewCDPGenStateMulti(), | 		NewCDPGenStateMulti(), | ||||||
| 	) | 	) | ||||||
| @ -37,6 +42,7 @@ func (suite *AuctionTestSuite) SetupTest() { | |||||||
| 	suite.app = tApp | 	suite.app = tApp | ||||||
| 	suite.ctx = ctx | 	suite.ctx = ctx | ||||||
| 	suite.keeper = keeper | 	suite.keeper = keeper | ||||||
|  | 	suite.addrs = []sdk.AccAddress{taddr} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -51,6 +57,15 @@ func (suite *AuctionTestSuite) TestNetDebtSurplus() { | |||||||
| 	suite.Equal(cs(c("debt", 90)), acc.GetCoins()) | 	suite.Equal(cs(c("debt", 90)), acc.GetCoins()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *AuctionTestSuite) TestCollateralAuction() { | ||||||
|  | 	sk := suite.app.GetSupplyKeeper() | ||||||
|  | 	err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 21000000000), c("bnb", 190000000000))) | ||||||
|  | 	suite.Require().NoError(err) | ||||||
|  | 	testDeposit := types.NewDeposit(1, suite.addrs[0], c("bnb", 190000000000)) | ||||||
|  | 	err = suite.keeper.AuctionCollateral(suite.ctx, types.Deposits{testDeposit}, i(21000000000), "usdx") | ||||||
|  | 	suite.Require().NoError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (suite *AuctionTestSuite) TestSurplusAuction() { | func (suite *AuctionTestSuite) TestSurplusAuction() { | ||||||
| 	sk := suite.app.GetSupplyKeeper() | 	sk := suite.app.GetSupplyKeeper() | ||||||
| 	err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 600000000000))) | 	err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 600000000000))) | ||||||
|  | |||||||
| @ -302,14 +302,20 @@ func (k Keeper) RemoveCdpOwnerIndex(ctx sdk.Context, cdp types.CDP) { | |||||||
| // IndexCdpByCollateralRatio sets the cdp id in the store, indexed by the collateral type and collateral to debt ratio
 | // IndexCdpByCollateralRatio sets the cdp id in the store, indexed by the collateral type and collateral to debt ratio
 | ||||||
| func (k Keeper) IndexCdpByCollateralRatio(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { | func (k Keeper) IndexCdpByCollateralRatio(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { | ||||||
| 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | ||||||
| 	db, _ := k.GetDenomPrefix(ctx, denom) | 	db, found := k.GetDenomPrefix(ctx, denom) | ||||||
|  | 	if !found { | ||||||
|  | 		panic(fmt.Sprintf("denom %s prefix not found", denom)) | ||||||
|  | 	} | ||||||
| 	store.Set(types.CollateralRatioKey(db, id, collateralRatio), types.GetCdpIDBytes(id)) | 	store.Set(types.CollateralRatioKey(db, id, collateralRatio), types.GetCdpIDBytes(id)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RemoveCdpCollateralRatioIndex deletes the cdp id from the store's index of cdps by collateral type and collateral to debt ratio
 | // RemoveCdpCollateralRatioIndex deletes the cdp id from the store's index of cdps by collateral type and collateral to debt ratio
 | ||||||
| func (k Keeper) RemoveCdpCollateralRatioIndex(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { | func (k Keeper) RemoveCdpCollateralRatioIndex(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { | ||||||
| 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | ||||||
| 	db, _ := k.GetDenomPrefix(ctx, denom) | 	db, found := k.GetDenomPrefix(ctx, denom) | ||||||
|  | 	if !found { | ||||||
|  | 		panic(fmt.Sprintf("denom %s prefix not found", denom)) | ||||||
|  | 	} | ||||||
| 	store.Delete(types.CollateralRatioKey(db, id, collateralRatio)) | 	store.Delete(types.CollateralRatioKey(db, id, collateralRatio)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -83,6 +83,7 @@ func NewPricefeedGenStateMulti() app.GenesisState { | |||||||
| 			Markets: []pricefeed.Market{ | 			Markets: []pricefeed.Market{ | ||||||
| 				{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, | 				{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, | ||||||
| 				{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, | 				{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, | ||||||
|  | 				{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		PostedPrices: []pricefeed.PostedPrice{ | 		PostedPrices: []pricefeed.PostedPrice{ | ||||||
| @ -98,6 +99,12 @@ func NewPricefeedGenStateMulti() app.GenesisState { | |||||||
| 				Price:         sdk.MustNewDecFromStr("0.25"), | 				Price:         sdk.MustNewDecFromStr("0.25"), | ||||||
| 				Expiry:        time.Now().Add(1 * time.Hour), | 				Expiry:        time.Now().Add(1 * time.Hour), | ||||||
| 			}, | 			}, | ||||||
|  | 			{ | ||||||
|  | 				MarketID:      "bnb:usd", | ||||||
|  | 				OracleAddress: sdk.AccAddress{}, | ||||||
|  | 				Price:         sdk.MustNewDecFromStr("17.25"), | ||||||
|  | 				Expiry:        time.Now().Add(1 * time.Hour), | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} | 	return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} | ||||||
| @ -105,7 +112,7 @@ func NewPricefeedGenStateMulti() app.GenesisState { | |||||||
| func NewCDPGenStateMulti() app.GenesisState { | func NewCDPGenStateMulti() app.GenesisState { | ||||||
| 	cdpGenesis := cdp.GenesisState{ | 	cdpGenesis := cdp.GenesisState{ | ||||||
| 		Params: cdp.Params{ | 		Params: cdp.Params{ | ||||||
| 			GlobalDebtLimit:              sdk.NewInt64Coin("usdx", 1000000000000), | 			GlobalDebtLimit:              sdk.NewInt64Coin("usdx", 1500000000000), | ||||||
| 			SurplusAuctionThreshold:      cdp.DefaultSurplusThreshold, | 			SurplusAuctionThreshold:      cdp.DefaultSurplusThreshold, | ||||||
| 			SurplusAuctionLot:            cdp.DefaultSurplusLot, | 			SurplusAuctionLot:            cdp.DefaultSurplusLot, | ||||||
| 			DebtAuctionThreshold:         cdp.DefaultDebtThreshold, | 			DebtAuctionThreshold:         cdp.DefaultDebtThreshold, | ||||||
| @ -136,6 +143,18 @@ func NewCDPGenStateMulti() app.GenesisState { | |||||||
| 					LiquidationMarketID: "btc:usd", | 					LiquidationMarketID: "btc:usd", | ||||||
| 					ConversionFactor:    i(8), | 					ConversionFactor:    i(8), | ||||||
| 				}, | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Denom:               "bnb", | ||||||
|  | 					LiquidationRatio:    sdk.MustNewDecFromStr("1.5"), | ||||||
|  | 					DebtLimit:           sdk.NewInt64Coin("usdx", 500000000000), | ||||||
|  | 					StabilityFee:        sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
 | ||||||
|  | 					LiquidationPenalty:  d("0.05"), | ||||||
|  | 					AuctionSize:         i(50000000000), | ||||||
|  | 					Prefix:              0x22, | ||||||
|  | 					SpotMarketID:        "bnb:usd", | ||||||
|  | 					LiquidationMarketID: "bnb:usd", | ||||||
|  | 					ConversionFactor:    i(8), | ||||||
|  | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			DebtParam: cdp.DebtParam{ | 			DebtParam: cdp.DebtParam{ | ||||||
| 				Denom:            "usdx", | 				Denom:            "usdx", | ||||||
|  | |||||||
| @ -45,7 +45,10 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, | |||||||
| // CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
 | // CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
 | ||||||
| func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterator { | func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterator { | ||||||
| 	store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) | 	store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) | ||||||
| 	db, _ := k.GetDenomPrefix(ctx, denom) | 	db, found := k.GetDenomPrefix(ctx, denom) | ||||||
|  | 	if !found { | ||||||
|  | 		panic(fmt.Sprintf("denom %s prefix not found", denom)) | ||||||
|  | 	} | ||||||
| 	return sdk.KVStorePrefixIterator(store, types.DenomIterKey(db)) | 	return sdk.KVStorePrefixIterator(store, types.DenomIterKey(db)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -53,7 +56,10 @@ func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterato | |||||||
| // matching denom and collateral:debt ratio LESS THAN targetRatio
 | // matching denom and collateral:debt ratio LESS THAN targetRatio
 | ||||||
| func (k Keeper) CdpCollateralRatioIndexIterator(ctx sdk.Context, denom string, targetRatio sdk.Dec) sdk.Iterator { | func (k Keeper) CdpCollateralRatioIndexIterator(ctx sdk.Context, denom string, targetRatio sdk.Dec) sdk.Iterator { | ||||||
| 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | 	store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) | ||||||
| 	db, _ := k.GetDenomPrefix(ctx, denom) | 	db, found := k.GetDenomPrefix(ctx, denom) | ||||||
|  | 	if !found { | ||||||
|  | 		panic(fmt.Sprintf("denom %s prefix not found", denom)) | ||||||
|  | 	} | ||||||
| 	return store.Iterator(types.CollateralRatioIterKey(db, sdk.ZeroDec()), types.CollateralRatioIterKey(db, targetRatio)) | 	return store.Iterator(types.CollateralRatioIterKey(db, sdk.ZeroDec()), types.CollateralRatioIterKey(db, targetRatio)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								x/cdp/keeper/math.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								x/cdp/keeper/math.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | package keeper | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"sort" | ||||||
|  | 
 | ||||||
|  | 	sdk "github.com/cosmos/cosmos-sdk/types" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
 | ||||||
|  | // It uses the largest remainder method: https://en.wikipedia.org/wiki/Largest_remainder_method
 | ||||||
|  | // See also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
 | ||||||
|  | // note - copied from auction, tests are located there.
 | ||||||
|  | func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int { | ||||||
|  | 	// Limit input to +ve numbers as algorithm hasn't been scoped to work with -ve numbers.
 | ||||||
|  | 	if amount.IsNegative() { | ||||||
|  | 		panic("negative amount") | ||||||
|  | 	} | ||||||
|  | 	if len(buckets) < 1 { | ||||||
|  | 		panic("no buckets") | ||||||
|  | 	} | ||||||
|  | 	for _, bucket := range buckets { | ||||||
|  | 		if bucket.IsNegative() { | ||||||
|  | 			panic("negative bucket") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 1) Split the amount by weights, recording whole number part and remainder
 | ||||||
|  | 
 | ||||||
|  | 	totalWeights := totalInts(buckets...) | ||||||
|  | 	if !totalWeights.IsPositive() { | ||||||
|  | 		panic("total weights must sum to > 0") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	quotients := make([]quoRem, len(buckets)) | ||||||
|  | 	for i := range buckets { | ||||||
|  | 		// amount * ( weight/total_weight )
 | ||||||
|  | 		q := amount.Mul(buckets[i]).Quo(totalWeights) | ||||||
|  | 		r := amount.Mul(buckets[i]).Mod(totalWeights) | ||||||
|  | 		quotients[i] = quoRem{index: i, quo: q, rem: r} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 2) Calculate total left over from remainders, and apportion it to buckets with the highest remainder (to minimize error)
 | ||||||
|  | 
 | ||||||
|  | 	// sort by decreasing remainder order
 | ||||||
|  | 	sort.Slice(quotients, func(i, j int) bool { | ||||||
|  | 		return quotients[i].rem.GT(quotients[j].rem) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// calculate total left over from remainders
 | ||||||
|  | 	allocated := sdk.ZeroInt() | ||||||
|  | 	for _, qr := range quotients { | ||||||
|  | 		allocated = allocated.Add(qr.quo) | ||||||
|  | 	} | ||||||
|  | 	leftToAllocate := amount.Sub(allocated) | ||||||
|  | 
 | ||||||
|  | 	// apportion according to largest remainder
 | ||||||
|  | 	results := make([]sdk.Int, len(quotients)) | ||||||
|  | 	for _, qr := range quotients { | ||||||
|  | 		results[qr.index] = qr.quo | ||||||
|  | 		if !leftToAllocate.IsZero() { | ||||||
|  | 			results[qr.index] = results[qr.index].Add(sdk.OneInt()) | ||||||
|  | 			leftToAllocate = leftToAllocate.Sub(sdk.OneInt()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return results | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type quoRem struct { | ||||||
|  | 	index int | ||||||
|  | 	quo   sdk.Int | ||||||
|  | 	rem   sdk.Int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // totalInts adds together sdk.Ints
 | ||||||
|  | func totalInts(is ...sdk.Int) sdk.Int { | ||||||
|  | 	total := sdk.ZeroInt() | ||||||
|  | 	for _, i := range is { | ||||||
|  | 		total = total.Add(i) | ||||||
|  | 	} | ||||||
|  | 	return total | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Kevin Davis
						Kevin Davis