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

feat: add basket invariants #787

Merged
merged 9 commits into from
Feb 19, 2022
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
6 changes: 3 additions & 3 deletions x/ecocredit/basket/msg_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const creditTypeAbbrMaxLen = 3

var errBadReq = sdkerrors.ErrInvalidRequest

// first character must be alphabetic, the rest can be alphanumeric. We reduce length constraints by one to account for
// the first character being forced to alphabetic.
// first character must be alphabetic, the rest can be alphanumeric. We reduce length
// constraints by one to account for the first character being forced to alphabetic.
var reName = regexp.MustCompile(fmt.Sprintf("^[[:alpha:]][[:alnum:]]{%d,%d}$", nameMinLen-1, nameMaxLen-1))

// ValidateBasic does a stateless sanity check on the provided data.
Expand Down Expand Up @@ -109,7 +109,7 @@ func validateDateCriteria(d *DateCriteria) error {

// BasketDenom formats denom and display denom:
// * denom: eco.<m.Exponent><m.CreditTypeAbbrev>.<m.Name>
// * display denom: eco.<m.Exponent><m.CreditTypeAbbrev>.<m.Name>
// * display denom: eco.<m.CreditTypeAbbrev>.<m.Name>
// Returns error if MsgCrete.Exponent is not supported
func BasketDenom(name, creditTypeAbbrev string, exponent uint32) (string, string, error) {
const basketDenomPrefix = "eco."
Expand Down
2 changes: 2 additions & 0 deletions x/ecocredit/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type BankKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SetDenomMetaData(ctx sdk.Context, denomMetaData banktypes.Metadata)

GetSupply(ctx sdk.Context, denom string) sdk.Coin
}

type DistributionKeeper interface {
Expand Down
14 changes: 14 additions & 0 deletions x/ecocredit/mocks/expected_keepers.go

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

1 change: 0 additions & 1 deletion x/ecocredit/server/basket/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ func TestFeeToLow(t *testing.T) {
t.Parallel()

s := setupBase(t)

minFee := sdk.NewCoins(sdk.NewCoin("regen", sdk.NewInt(100)))

s.ecocreditKeeper.EXPECT().GetCreateBasketFee(gomock.Any()).Return(minFee)
Expand Down
98 changes: 98 additions & 0 deletions x/ecocredit/server/basket/invariants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package basket

import (
"context"
"fmt"
"sort"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"

basketv1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/basket/v1"
"github.com/regen-network/regen-ledger/types/math"
"github.com/regen-network/regen-ledger/x/ecocredit"
)

func (k Keeper) RegisterInvariants(ir sdk.InvariantRegistry) {
ir.RegisterRoute(ecocredit.ModuleName, "basket-supply", k.basketSupplyInvariant())
}

func (k Keeper) basketSupplyInvariant() sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
goCtx := sdk.WrapSDKContext(ctx)

bals, err := k.computeBasketBalances(goCtx)
if err != nil {
return err.Error(), true
}
return BasketSupplyInvariant(ctx, k.stateStore.BasketStore(), k.bankKeeper, bals)
}
}

type bankSupplyStore interface {
GetSupply(ctx sdk.Context, denom string) sdk.Coin
}

// BasketSupplyInvariant cross check the balance of baskets and bank
func BasketSupplyInvariant(ctx sdk.Context, store basketv1.BasketStore, bank bankSupplyStore, basketBalances map[uint64]math.Dec) (string, bool) {
goCtx := sdk.WrapSDKContext(ctx)

bids := make([]uint64, len(basketBalances))
i := 0
for bid := range basketBalances {
bids[i] = bid
i++
}
sort.Slice(bids, func(i, j int) bool { return bids[i] < bids[j] })

var inbalances []string
for _, bid := range bids {
bal := basketBalances[bid]
balInt, err := bal.BigInt()
if err != nil {
return fmt.Sprintf("Can't convert Dec to big.Int, %v", err), true
}
b, err := store.Get(goCtx, bid)
if err != nil {
return fmt.Sprintf("Can't get basket %v: %v", bid, err), true
}
c := bank.GetSupply(ctx, b.BasketDenom)
balSdkInt := sdk.NewIntFromBigInt(balInt)
if !c.Amount.Equal(balSdkInt) {
inbalances = append(inbalances, fmt.Sprintf("Basket denom %s is imbalanced, expected: %v, got %v",
b.BasketDenom, balSdkInt, c.Amount))
}
}
if len(inbalances) != 0 {
return strings.Join(inbalances, "\n"), true
}
return "", false
}

// computeBasketBalances returns a map from basket id to the total number of eco credits
func (k Keeper) computeBasketBalances(ctx context.Context) (map[uint64]math.Dec, error) {
it, err := k.stateStore.BasketBalanceStore().List(ctx, &basketv1.BasketBalancePrimaryKey{})
if err != nil {
return nil, fmt.Errorf("can't create basket balance iterator, %w", err)
}
balances := map[uint64]math.Dec{}
for it.Next() {
b, err := it.Value()
if err != nil {
return nil, fmt.Errorf("Can't get basket balance %w", err)
}
bal, err := math.NewDecFromString(b.Balance)
if err != nil {
return nil, fmt.Errorf("Can't decode balance %s as math.Dec: %w", b.Balance, err)
}
if a, ok := balances[b.BasketId]; ok {
if a, err = a.Add(bal); err != nil {
return nil, fmt.Errorf("Can't add balances: %w", err)
}
balances[b.BasketId] = a
} else {
balances[b.BasketId] = bal
}
}
return balances, nil
}
84 changes: 84 additions & 0 deletions x/ecocredit/server/basket/invariants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package basket_test

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

basketv1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/basket/v1"
"github.com/regen-network/regen-ledger/types/math"
"github.com/regen-network/regen-ledger/x/ecocredit/server/basket"
)

type BasketWithSupply struct {
supply int64
b basketv1.Basket
}

type BankSupplyMock map[string]sdk.Coin

func (bs BankSupplyMock) GetSupply(_ sdk.Context, denom string) sdk.Coin {
if c, ok := bs[denom]; ok {
return c
}
return sdk.NewInt64Coin(denom, 0)
}

func TestBasketSupplyInvarint(t *testing.T) {
require := require.New(t)
s := setupBase(t)

baskets := []BasketWithSupply{
{10, basketv1.Basket{BasketDenom: "bb1", Name: "b1"}},
{20, basketv1.Basket{BasketDenom: "bb2", Name: "b2"}},
}
store := s.stateStore.BasketStore()
basketBalances := map[uint64]math.Dec{}
correctBalances := BankSupplyMock{}
for _, b := range baskets {
id, err := store.InsertReturningID(s.ctx, &b.b)
require.NoError(err)
basketBalances[id] = math.NewDecFromInt64(b.supply)
correctBalances[b.b.BasketDenom] = sdk.NewInt64Coin(b.b.BasketDenom, b.supply)
}

tcs := []struct {
name string
bank BankSupplyMock
msg string
}{
{"no bank supply",
BankSupplyMock{}, "imbalanced"},
{"partial bank supply",
BankSupplyMock{"bb1": newCoin("bb1", 10)}, "bb2 is imbalanced"},
{"smaller bank supply",
BankSupplyMock{"bb1": newCoin("bb1", 8)}, "bb1 is imbalanced"},
{"smaller bank supply2",
BankSupplyMock{"bb1": newCoin("bb1", 10), "bb2": newCoin("bb2", 10)}, "bb2 is imbalanced"},
{"bigger bank supply",
BankSupplyMock{"bb1": newCoin("bb1", 10), "bb2": newCoin("bb2", 30)}, "bb2 is imbalanced"},

{"all good",
correctBalances, ""},
{"more denoms",
BankSupplyMock{"bb1": newCoin("bb1", 10), "bb2": newCoin("bb2", 20), "other": newCoin("other", 100)}, ""},
}

for _, tc := range tcs {
tc.bank.GetSupply(s.sdkCtx, "abc")

msg, _ := basket.BasketSupplyInvariant(s.sdkCtx, store, tc.bank, basketBalances)
if tc.msg != "" {
require.Contains(msg, tc.msg, tc.name)
} else {
require.Empty(msg, tc.name)
}
}

t.Log(baskets)
}

func newCoin(denom string, a int64) sdk.Coin {
return sdk.NewInt64Coin(denom, a)
}
28 changes: 16 additions & 12 deletions x/ecocredit/server/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package server
import (
"context"
"fmt"

"github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/regen-network/regen-ledger/types/math"
"github.com/regen-network/regen-ledger/x/ecocredit"
baskettypes "github.com/regen-network/regen-ledger/x/ecocredit/basket"
Expand All @@ -14,6 +16,7 @@ import (
func (s serverImpl) RegisterInvariants(ir sdk.InvariantRegistry) {
ir.RegisterRoute(ecocredit.ModuleName, "tradable-supply", s.tradableSupplyInvariant())
ir.RegisterRoute(ecocredit.ModuleName, "retired-supply", s.retiredSupplyInvariant())
s.basketKeeper.RegisterInvariants(ir)
}

func (s serverImpl) tradableSupplyInvariant() sdk.Invariant {
Expand All @@ -30,7 +33,7 @@ func (s serverImpl) getBasketBalanceMap(ctx context.Context) map[string]math.Dec
if err != nil {
panic(err)
}
basketBalances := make(map[string]math.Dec) // map of batch_denom to balance
batchBalances := make(map[string]math.Dec) // map of a basket batch_denom to balance
for _, basket := range res.Baskets {
res, err := s.basketKeeper.BasketBalances(ctx, &baskettypes.QueryBasketBalancesRequest{BasketDenom: basket.BasketDenom})
if err != nil {
Expand All @@ -41,54 +44,55 @@ func (s serverImpl) getBasketBalanceMap(ctx context.Context) map[string]math.Dec
if err != nil {
panic(err)
}
if existingBal, ok := basketBalances[bal.BatchDenom]; ok {
if existingBal, ok := batchBalances[bal.BatchDenom]; ok {
existingBal, err = existingBal.Add(amount)
if err != nil {
panic(err)
}
basketBalances[bal.BatchDenom] = existingBal
batchBalances[bal.BatchDenom] = existingBal
} else {
basketBalances[bal.BatchDenom] = amount
batchBalances[bal.BatchDenom] = amount
}
}
}
return basketBalances
return batchBalances
}

func tradableSupplyInvariant(store types.KVStore, basketBalances map[string]math.Dec) (string, bool) {
var (
msg string
broken bool
)
calTradableSupplies := make(map[string]math.Dec)
// sum of tradeable eco credits with credits locked in baskets
sumBatchSupplies := make(map[string]math.Dec) // map batch denom => balance

ecocredit.IterateBalances(store, ecocredit.TradableBalancePrefix, func(_, denom, b string) bool {
balance, err := math.NewNonNegativeDecFromString(b)
if err != nil {
broken = true
msg += fmt.Sprintf("error while parsing tradable balance %v", err)
}
if supply, ok := calTradableSupplies[denom]; ok {
if supply, ok := sumBatchSupplies[denom]; ok {
supply, err := math.SafeAddBalance(supply, balance)
if err != nil {
broken = true
msg += fmt.Sprintf("error adding credit batch tradable supply %v", err)
}
calTradableSupplies[denom] = supply
sumBatchSupplies[denom] = supply
} else {
calTradableSupplies[denom] = balance
sumBatchSupplies[denom] = balance
}

return false
})

for denom, amt := range basketBalances {
if amount, ok := calTradableSupplies[denom]; ok {
if amount, ok := sumBatchSupplies[denom]; ok {
amount, err := math.SafeAddBalance(amount, amt)
if err != nil {
panic(err)
}
calTradableSupplies[denom] = amount
sumBatchSupplies[denom] = amount
} else {
panic("unknown denom in basket")
}
Expand All @@ -100,7 +104,7 @@ func tradableSupplyInvariant(store types.KVStore, basketBalances map[string]math
broken = true
msg += fmt.Sprintf("error while parsing tradable supply for denom: %s", denom)
}
if s1, ok := calTradableSupplies[denom]; ok {
if s1, ok := sumBatchSupplies[denom]; ok {
if supply.Cmp(s1) != 0 {
broken = true
msg += fmt.Sprintf("tradable supply is incorrect for %s credit batch, expected %v, got %v", denom, supply, s1)
Expand Down
2 changes: 0 additions & 2 deletions x/ecocredit/server/invariants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,7 @@ func TestTradableSupplyInvariants(t *testing.T) {
ctx, storeKey := setupStore(t)
store := ctx.KVStore(storeKey)
t.Run(tc.msg, func(t *testing.T) {

initBalances(t, store, tc.balances)

initSupply(t, store, tc.supply)

msg, broken := tradableSupplyInvariant(store, tc.basketBalance)
Expand Down
8 changes: 5 additions & 3 deletions x/ecocredit/server/testsuite/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"github.com/regen-network/regen-ledger/x/ecocredit"
)

const basketFeeDenom = "bfee"

type IntegrationTestSuite struct {
suite.Suite

Expand Down Expand Up @@ -93,7 +95,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
Precision: 6,
},
)
s.basketFee = sdk.NewInt64Coin("foo", 20)
s.basketFee = sdk.NewInt64Coin(basketFeeDenom, 20)
ecocreditParams.BasketCreationFee = sdk.NewCoins(s.basketFee)
s.paramSpace.SetParamSet(s.sdkCtx, &ecocreditParams)

Expand All @@ -116,7 +118,7 @@ func (s *IntegrationTestSuite) TestBasketScenario() {
classId, batchDenom := s.createClassAndIssueBatch(user, user, "bazcredits", userTotalCreditBalance.String(), "2020-01-01", "2022-01-01")

// fund account to create a basket
balanceBefore := sdk.NewInt64Coin("foo", 30000)
balanceBefore := sdk.NewInt64Coin(basketFeeDenom, 30000)
s.fundAccount(user, sdk.NewCoins(balanceBefore))
s.mockDist.EXPECT().FundCommunityPool(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(interface{}, interface{}, interface{}) error {
err := s.bankKeeper.SendCoinsFromAccountToModule(s.sdkCtx, user, ecocredit.ModuleName, sdk.NewCoins(s.basketFee))
Expand Down Expand Up @@ -144,7 +146,7 @@ func (s *IntegrationTestSuite) TestBasketScenario() {

// assert the fee was paid - the fee mechanism was mocked, but we still call the same underlying SendFromAccountToModule
// function so the result is the same
balanceAfter := s.getUserBalance(user, "foo")
balanceAfter := s.getUserBalance(user, basketFeeDenom)
require.Equal(balanceAfter.Add(s.basketFee), balanceBefore)

// put some BAZ credits in the basket
Expand Down