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

refactor(x/ecocredit): update credit to basket conversion and docs #1559

Merged
merged 10 commits into from
Nov 1, 2022
21 changes: 10 additions & 11 deletions api/regen/ecocredit/basket/v1/tx.pulsar.go

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

66 changes: 56 additions & 10 deletions api/regen/ecocredit/basket/v1/tx_grpc.pb.go

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

55 changes: 38 additions & 17 deletions proto/regen/ecocredit/basket/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,37 @@ option go_package = "github.com/regen-network/regen-ledger/x/ecocredit/basket/ty

// Msg is the regen.ecocredit.basket.v1 Msg service.
service Msg {

// Create creates a bank denom which wraps credits.
// Create creates a basket that can hold different types of ecocredits that
// meet the basket's criteria. Upon depositing ecocredits into the basket,
// basket tokens are minted and sent to depositor using the Cosmos SDK Bank
// module. This allows basket tokens to be utilized within IBC. Basket tokens
// are fully fungible with other basket tokens within the same basket. The
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
// basket token denom is derived from the basket name, credit type
// abbreviation, and credit type precision (i.e. basket name "foo", credit
// type exponent 6, and credit type abbreviation "C" generates the denom
// eco.uC.foo). Baskets can limit credit acceptance criteria based on a
// combination of credit type, credit classes, and credit batch start date.
// Credits can be taken from the basket in exchange for basket tokens. Taken
// credits will be immediately retired, unless disable_auto_retire is set to
// false, which would cause credits to be sent to the taker's tradable
// balance. If the basket fee governance parameter is set, a fee of equal or
ryanchristo marked this conversation as resolved.
Show resolved Hide resolved
// greater value must be provided in the request. Only the amount specified in
// the fee parameter will be charged, even if a greater value fee is provided.
// Fees from creating a basket are burned.
rpc Create(MsgCreate) returns (MsgCreateResponse);

// Put puts credits into a basket in return for basket tokens.
// Put deposits credits into the basket from the holder's tradable balance in
// exchange for basket tokens. The amount of tokens received is calculated by
// the following formula: sum(credits_deposited) * 10^credit_type_exponent.
// The credits being deposited MUST adhere to the criteria of the basket.
rpc Put(MsgPut) returns (MsgPutResponse);

// Take takes credits from a basket starting from the oldest
// credits first.
// Take exchanges basket tokens for credits from the specified basket. Credits
// are taken deterministically, ordered by oldest batch start date to the most
// recent batch start date. If the basket has disable_auto_retire set to
// false, both retirement_jurisdiction and retire_on_take must be set, and the
// taken credits will be retired immediately upon receipt. Otherwise, credits
// may be received as tradable or retired, based on the request.
rpc Take(MsgTake) returns (MsgTakeResponse);

// UpdateBasketFee is a governance method that allows for updating the basket
Expand All @@ -29,7 +51,7 @@ service Msg {
// Since Revision 2
rpc UpdateBasketFee(MsgUpdateBasketFee) returns (MsgUpdateBasketFeeResponse);

// UpdateCurator updates basket curator
// UpdateCurator updates basket curator.
//
// Since Revision 2
rpc UpdateCurator(MsgUpdateCurator) returns (MsgUpdateCuratorResponse);
Expand Down Expand Up @@ -80,11 +102,10 @@ message MsgCreate {
// At most, only one of the fields in the date_criteria should be set.
DateCriteria date_criteria = 8;

// fee is the basket creation fee. A fee is not required if the list of fees
// in Params.basket_fee is empty. The provided fee must be one of the fees
// listed in Params.basket_fee. The provided amount can be greater than
// or equal to the listed amount but the basket creator will only be charged
// the listed amount (i.e. the minimum amount).
// fee is the basket creation fee. A fee is not required if no fee exists
// in the basket fee parameter. The fee must be greater than or equal to the
// fee param. The curator will be charged the amount specified in the fee
// parameter, even if a greater amount is provided.
//
// Note (Since Revision 1): Although this field supports a list of fees, the
// basket creator must provide no more than one fee (i.e. one Coin in a list
Expand Down Expand Up @@ -115,9 +136,7 @@ message MsgPut {
string basket_denom = 2;

// credits are credits to add to the basket. If they do not match the basket's
// admission criteria the operation will fail. If there are any "dust" credits
// left over when converting credits to basket tokens, these credits will
// not be converted to basket tokens and instead remain with the owner.
// admission criteria, the operation will fail.
repeated BasketCredit credits = 3;
}

Expand All @@ -142,7 +161,7 @@ message MsgTake {
string amount = 3;

// retirement_location is the optional retirement jurisdiction for the
// credits which will be used only if retire_on_take is true for this basket.
// credits which will be used only if retire_on_take is true.
//
// Deprecated (Since Revision 1): This field will be removed in the next
// version in favor of retirement_jurisdiction. Only one of these need to be
Expand All @@ -151,11 +170,13 @@ message MsgTake {

// retire_on_take is a boolean that dictates whether the ecocredits
// received in exchange for the basket tokens will be received as
// retired or tradable credits.
// retired or tradable credits. If the basket has disable_auto_retire set to
// false, retire_on_take MUST be set to true, and a retirement jurisdiction
// must be provided.
bool retire_on_take = 5;

// retirement_jurisdiction is the optional retirement jurisdiction for the
// credits which will be used only if retire_on_take is true for this basket.
// credits which will be used only if retire_on_take is true.
//
// Since Revision 1
string retirement_jurisdiction = 6;
Expand Down
21 changes: 2 additions & 19 deletions x/ecocredit/basket/keeper/msg_put.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ func (k Keeper) Put(ctx context.Context, req *types.MsgPut) (*types.MsgPutRespon
return nil, err
}
// get the amount of basket tokens to give to the depositor
tokens, err := creditAmountToBasketCoins(amt, creditType.Precision, basket.BasketDenom)
tokens, err := creditAmountToBasketCoin(amt, creditType.Precision, basket.BasketDenom)
if err != nil {
return nil, err
}
// update the total amount received so far
amountReceived = amountReceived.Add(tokens[0].Amount)
amountReceived = amountReceived.Add(tokens.Amount)

if err = sdkCtx.EventManager().EmitTypedEvent(&basetypes.EventTransfer{
Sender: ownerString,
Expand Down Expand Up @@ -217,20 +217,3 @@ func (k Keeper) transferToBasket(ctx context.Context, sender sdk.AccAddress, amt
}
return nil
}

// creditAmountToBasketCoins calculates the tokens to award to the depositor
func creditAmountToBasketCoins(creditAmt regenmath.Dec, exp uint32, denom string) (sdk.Coins, error) {
var coins sdk.Coins
multiplier := regenmath.NewDecFinite(1, int32(exp))
tokenAmt, err := multiplier.MulExact(creditAmt)
if err != nil {
return coins, err
}

amtInt, err := tokenAmt.BigInt()
if err != nil {
return coins, err
}

return sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(amtInt))}, nil
}
19 changes: 19 additions & 0 deletions x/ecocredit/basket/keeper/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package keeper
import (
"context"

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

api "github.com/regen-network/regen-ledger/api/regen/ecocredit/basket/v1"
"github.com/regen-network/regen-ledger/types/math"
)
Expand Down Expand Up @@ -56,3 +58,20 @@ func (k Keeper) GetBasketBalanceMap(ctx context.Context) (map[uint64]math.Dec, e

return batchKeyToBalance, nil
}

// creditAmountToBasketCoin calculates the coins to mint to the credit depositor using the following formula:
// coinAmount = creditAmt * (1 * 10^exp)
func creditAmountToBasketCoin(creditAmt math.Dec, exp uint32, denom string) (sdk.Coin, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified this to 1 Coin, since we never returned more than one

multiplier := math.NewDecFinite(1, int32(exp))
tokenAmt, err := multiplier.MulExact(creditAmt)
if err != nil {
return sdk.Coin{}, err
}

amtInt, err := tokenAmt.BigInt()
if err != nil {
return sdk.Coin{}, err
}

return sdk.NewCoin(denom, sdk.NewIntFromBigInt(amtInt)), nil
}
55 changes: 55 additions & 0 deletions x/ecocredit/basket/keeper/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"fmt"
"math/big"
"testing"

"github.com/golang/mock/gomock"
Expand Down Expand Up @@ -87,6 +89,59 @@ func TestGetBasketBalances(t *testing.T) {
require.Equal(t, IDToBalance[2], amtToDeposit)
}

func FuzzCreditAmountToBasketCoin(f *testing.F) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a fuzz test to ensure we are properly converting credit decimal amounts to basket token integers.


// prefixLen returns the amount we need to adjust our length calculations by.
// for example, 0.23 * 10^2 produces 23, equal to the exponent. however, 1.12 * 10^2 produces 112, so we must adjust.
// similarly, 0.003 * 10^3 produces just 3, so we also consider subtracting from the adjustment.
prefixLen := func(d string) int {
prefixLen := 0
// first we want to capture situations where the whole number has many digits (i.e. 12423.402) as this makes the resulting number LARGER
if d[0] != '0' {
for _, c := range d {
if c != '.' {
prefixLen++
} else {
break
}
}
return prefixLen
}
// then we capture situations where we have many zeroes (i.e. 0.0023) as this makes the resulting number SMALLER.
count := 0
for i := 2; i < len(d)-1; i++ {
if d[i] == '0' {
count--
} else {
break
}
}
return count
}

f.Add(uint32(6), 1.123456)
f.Fuzz(func(t *testing.T, exponent uint32, dec float64) {
if exponent > 24 || exponent == 0 {
t.Skip()
}
trimmedFloat := big.NewFloat(dec).Text('f', int(exponent))
creditDec, err := math.NewPositiveFixedDecFromString(trimmedFloat, exponent)
if err != nil {
t.Skip()
}
assert.NilError(t, err)
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
if len(creditDec.String()) >= 36 { // decimals strings with length >= 36 create rounding errors.
t.Skip()
}
coin, err := creditAmountToBasketCoin(creditDec, exponent, "foo")
assert.NilError(t, err, fmt.Sprintf("error converting %s with exponent %d", creditDec.String(), exponent))
add := prefixLen(creditDec.String())
expected := int(exponent) + add
assert.Check(t, len(coin.Amount.String()) == expected, fmt.Sprintf("expected %v to have length %d given decimal %s and exponent %d with prefix length %d", coin, expected, creditDec.String(), exponent, add))

})
}

func initBatch(t *testing.T, s *baseSuite, pid uint64, denom string, startDate *timestamppb.Timestamp) {
assert.NilError(t, s.baseStore.BatchTable().Insert(s.ctx, &baseapi.Batch{
ProjectKey: pid,
Expand Down
Loading