From 331793b982062514b3a6c98d214f8a63ed6bcd7c Mon Sep 17 00:00:00 2001 From: Jim Larson Date: Fri, 11 Jun 2021 20:35:25 -0700 Subject: [PATCH] feat: epoched reward distribution part 2 - send When fees are sent to vbank at the epoch, calculate a rate for distributing them to exhaust the reward pool by theend of the epoch. --- .../cosmos/proto/agoric/vbank/genesis.proto | 3 + golang/cosmos/proto/agoric/vbank/vbank.proto | 31 + .../proto/cosmos/base/v1beta1/coin.proto | 40 ++ golang/cosmos/x/vbank/README.md | 2 +- golang/cosmos/x/vbank/keeper/keeper.go | 44 +- golang/cosmos/x/vbank/module.go | 36 + golang/cosmos/x/vbank/types/genesis.pb.go | 80 ++- golang/cosmos/x/vbank/types/vbank.pb.go | 623 ++++++++++++++++++ golang/cosmos/x/vbank/vbank.go | 29 + golang/cosmos/x/vbank/vbank_test.go | 284 ++++++-- 10 files changed, 1106 insertions(+), 66 deletions(-) create mode 100644 golang/cosmos/proto/agoric/vbank/vbank.proto create mode 100644 golang/cosmos/third_party/proto/cosmos/base/v1beta1/coin.proto create mode 100644 golang/cosmos/x/vbank/types/vbank.pb.go diff --git a/golang/cosmos/proto/agoric/vbank/genesis.proto b/golang/cosmos/proto/agoric/vbank/genesis.proto index 2cade30b39e..680ed8f59f0 100644 --- a/golang/cosmos/proto/agoric/vbank/genesis.proto +++ b/golang/cosmos/proto/agoric/vbank/genesis.proto @@ -2,10 +2,13 @@ syntax = "proto3"; package agoric.vbank; import "gogoproto/gogo.proto"; +import "agoric/vbank/vbank.proto"; option go_package = "github.com/Agoric/agoric-sdk/golang/cosmos/x/vbank/types"; message GenesisState { option (gogoproto.equal) = false; + // parms defines all the parameters of the module. + Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/golang/cosmos/proto/agoric/vbank/vbank.proto b/golang/cosmos/proto/agoric/vbank/vbank.proto new file mode 100644 index 00000000000..21433f17af6 --- /dev/null +++ b/golang/cosmos/proto/agoric/vbank/vbank.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; +package agoric.vbank; + +import "gogoproto/gogo.proto"; +import "cosmos/base/v1beta1/coin.proto"; + +option go_package = "github.com/Agoric/agoric-sdk/golang/cosmos/x/vbank/types"; + +message Params { + option (gogoproto.equal) = true; + + // feeEpochDurationBlocks is the length of a fee epoch, in blocks. + // A value of zero has the same meaning as a value of one: + // the full fee buffer should be distributed immediately. + int64 feeEpochDurationBlocks = 1; +} + +message State { + option (gogoproto.equal) = true; + + // rewardPool is the current balance of rewards in the module account. + // NOTE: Tracking manually since there is no bank call for getting a + // module account balance by name. + repeated cosmos.base.v1beta1.Coin rewardPool = 1 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; + + // rewardRate is the amount of reward, if available, to send to the + // fee collector module on every block. + repeated cosmos.base.v1beta1.Coin rewardRate = 2 + [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; +} diff --git a/golang/cosmos/third_party/proto/cosmos/base/v1beta1/coin.proto b/golang/cosmos/third_party/proto/cosmos/base/v1beta1/coin.proto new file mode 100644 index 00000000000..fab75284b7f --- /dev/null +++ b/golang/cosmos/third_party/proto/cosmos/base/v1beta1/coin.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package cosmos.base.v1beta1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/cosmos/cosmos-sdk/types"; +option (gogoproto.goproto_stringer_all) = false; +option (gogoproto.stringer_all) = false; + +// Coin defines a token with a denomination and an amount. +// +// NOTE: The amount field is an Int which implements the custom method +// signatures required by gogoproto. +message Coin { + option (gogoproto.equal) = true; + + string denom = 1; + string amount = 2 [(gogoproto.customtype) = "Int", (gogoproto.nullable) = false]; +} + +// DecCoin defines a token with a denomination and a decimal amount. +// +// NOTE: The amount field is an Dec which implements the custom method +// signatures required by gogoproto. +message DecCoin { + option (gogoproto.equal) = true; + + string denom = 1; + string amount = 2 [(gogoproto.customtype) = "Dec", (gogoproto.nullable) = false]; +} + +// IntProto defines a Protobuf wrapper around an Int object. +message IntProto { + string int = 1 [(gogoproto.customtype) = "Int", (gogoproto.nullable) = false]; +} + +// DecProto defines a Protobuf wrapper around a Dec object. +message DecProto { + string dec = 1 [(gogoproto.customtype) = "Dec", (gogoproto.nullable) = false]; +} diff --git a/golang/cosmos/x/vbank/README.md b/golang/cosmos/x/vbank/README.md index 4a07f064a4c..a75905d8695 100644 --- a/golang/cosmos/x/vbank/README.md +++ b/golang/cosmos/x/vbank/README.md @@ -12,7 +12,7 @@ entirely at the ERTP level. ## Parameters - `feeCollectorName`: the module which handles fee distribution to stakers. -- `feeEpochDuration`: the duration (in seconds) over which fees should be given to the fee collector. +- `feeEpochDurationBlocks`: the duration (in blocks) over which fees should be given to the fee collector. ## State diff --git a/golang/cosmos/x/vbank/keeper/keeper.go b/golang/cosmos/x/vbank/keeper/keeper.go index 981743024c8..c7102555dbb 100644 --- a/golang/cosmos/x/vbank/keeper/keeper.go +++ b/golang/cosmos/x/vbank/keeper/keeper.go @@ -7,7 +7,9 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/x/vbank/types" ) -const genesis string = "genesis" +const genesisKey string = "genesis" +const paramsKey string = "params" +const stateKey string = "state" // Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine type Keeper struct { @@ -16,7 +18,6 @@ type Keeper struct { bankKeeper types.BankKeeper feeCollectorName string - // CallToController dispatches a message to the controlling process CallToController func(ctx sdk.Context, str string) (string, error) } @@ -40,7 +41,7 @@ func NewKeeper( func (k Keeper) GetGenesis(ctx sdk.Context) types.GenesisState { store := ctx.KVStore(k.storeKey) - bz := store.Get([]byte(genesis)) + bz := store.Get([]byte(genesisKey)) var gs types.GenesisState k.cdc.MustUnmarshalLengthPrefixed(bz, &gs) return gs @@ -48,7 +49,11 @@ func (k Keeper) GetGenesis(ctx sdk.Context) types.GenesisState { func (k Keeper) SetGenesis(ctx sdk.Context, data types.GenesisState) { store := ctx.KVStore(k.storeKey) - store.Set([]byte(genesis), k.cdc.MustMarshalLengthPrefixed(&data)) + store.Set([]byte(genesisKey), k.cdc.MustMarshalLengthPrefixed(&data)) + params := types.Params{ + FeeEpochDurationBlocks: data.GetParams().FeeEpochDurationBlocks, + } + k.SetParams(ctx, params) } func (k Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { @@ -64,9 +69,6 @@ func (k Keeper) StoreFeeCoins(ctx sdk.Context, amt sdk.Coins) error { } func (k Keeper) SendCoinsToFeeCollector(ctx sdk.Context, amt sdk.Coins) error { - if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, amt); err != nil { - return err - } return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, k.feeCollectorName, amt) } @@ -83,3 +85,31 @@ func (k Keeper) GrabCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) e } return k.bankKeeper.BurnCoins(ctx, types.ModuleName, amt) } + +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(paramsKey)) + params := types.Params{} + k.cdc.MustUnmarshal(bz, ¶ms) + return params +} + +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshal(¶ms) + store.Set([]byte(paramsKey), bz) +} + +func (k Keeper) GetState(ctx sdk.Context) types.State { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(stateKey)) + state := types.State{} + k.cdc.MustUnmarshal(bz, &state) + return state +} + +func (k Keeper) SetState(ctx sdk.Context, state types.State) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshal(&state) + store.Set([]byte(stateKey), bz) +} diff --git a/golang/cosmos/x/vbank/module.go b/golang/cosmos/x/vbank/module.go index 0782d4f4b66..2aeb7618f8f 100644 --- a/golang/cosmos/x/vbank/module.go +++ b/golang/cosmos/x/vbank/module.go @@ -3,6 +3,7 @@ package vbank import ( "encoding/json" stdlog "log" + "strings" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -25,6 +26,32 @@ var ( _ module.AppModuleBasic = AppModuleBasic{} ) +// minCoins returns the minimum of each denomination. +// The input coins should be sorted. +func minCoins(a, b sdk.Coins) sdk.Coins { + min := make([]sdk.Coin, 0) + for indexA, indexB := 0, 0; indexA < len(a) && indexB < len(b); { + coinA, coinB := a[indexA], b[indexB] + switch strings.Compare(coinA.Denom, coinB.Denom) { + case -1: // A < B + indexA++ + case 0: // A == B + minCoin := coinA + if coinB.IsLT(minCoin) { + minCoin = coinB + } + if !minCoin.IsZero() { + min = append(min, minCoin) + } + indexA++ + indexB++ + case 1: // A > B + indexB++ + } + } + return sdk.NewCoins(min...) +} + // app module Basics object type AppModuleBasic struct { cdc codec.Codec @@ -167,6 +194,15 @@ func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.V } } + // Distribute rewards. + state := am.keeper.GetState(ctx) + xfer := minCoins(state.RewardRate, state.RewardPool) + if !xfer.IsZero() { + am.keeper.SendCoinsToFeeCollector(ctx, xfer) + state.RewardPool = state.RewardPool.Sub(xfer) + am.keeper.SetState(ctx, state) + } + return []abci.ValidatorUpdate{} } diff --git a/golang/cosmos/x/vbank/types/genesis.pb.go b/golang/cosmos/x/vbank/types/genesis.pb.go index 7ad48ab4a79..e0f3f07fafa 100644 --- a/golang/cosmos/x/vbank/types/genesis.pb.go +++ b/golang/cosmos/x/vbank/types/genesis.pb.go @@ -24,6 +24,8 @@ var _ = math.Inf const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type GenesisState struct { + // parms defines all the parameters of the module. + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -59,6 +61,13 @@ func (m *GenesisState) XXX_DiscardUnknown() { var xxx_messageInfo_GenesisState proto.InternalMessageInfo +func (m *GenesisState) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + func init() { proto.RegisterType((*GenesisState)(nil), "agoric.vbank.GenesisState") } @@ -66,18 +75,21 @@ func init() { func init() { proto.RegisterFile("agoric/vbank/genesis.proto", fileDescriptor_8aaac686f3bede01) } var fileDescriptor_8aaac686f3bede01 = []byte{ - // 168 bytes of a gzipped FileDescriptorProto + // 210 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4a, 0x4c, 0xcf, 0x2f, 0xca, 0x4c, 0xd6, 0x2f, 0x4b, 0x4a, 0xcc, 0xcb, 0xd6, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x81, 0xc8, 0xe9, 0x81, 0xe5, 0xa4, 0x44, 0xd2, - 0xf3, 0xd3, 0xf3, 0xc1, 0x12, 0xfa, 0x20, 0x16, 0x44, 0x8d, 0x92, 0x08, 0x17, 0x8f, 0x3b, 0x44, - 0x53, 0x70, 0x49, 0x62, 0x49, 0xaa, 0x15, 0xcb, 0x8b, 0x05, 0xf2, 0x0c, 0x4e, 0x41, 0x27, 0x1e, - 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, - 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0x65, 0x91, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, - 0x97, 0x9c, 0x9f, 0xab, 0xef, 0x08, 0xb1, 0x1a, 0x62, 0x8b, 0x6e, 0x71, 0x4a, 0xb6, 0x7e, 0x7a, - 0x7e, 0x4e, 0x62, 0x5e, 0xba, 0x7e, 0x72, 0x7e, 0x71, 0x6e, 0x7e, 0xb1, 0x7e, 0x05, 0xd4, 0x55, - 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0x0b, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, - 0x54, 0xce, 0xef, 0x3b, 0xb2, 0x00, 0x00, 0x00, + 0xf3, 0xd3, 0xf3, 0xc1, 0x12, 0xfa, 0x20, 0x16, 0x44, 0x8d, 0x94, 0x04, 0x8a, 0x7e, 0x30, 0x09, + 0x91, 0x51, 0xf2, 0xe0, 0xe2, 0x71, 0x87, 0x18, 0x17, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0x64, 0xc4, + 0xc5, 0x56, 0x90, 0x58, 0x94, 0x98, 0x5b, 0x2c, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa2, + 0x87, 0x6c, 0xbc, 0x5e, 0x00, 0x58, 0xce, 0x89, 0xe5, 0xc4, 0x3d, 0x79, 0x86, 0x20, 0xa8, 0x4a, + 0x2b, 0x96, 0x17, 0x0b, 0xe4, 0x19, 0x9c, 0x82, 0x4e, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, + 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, + 0x8e, 0x21, 0xca, 0x22, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0xdf, 0x11, + 0xe2, 0x10, 0x88, 0xa1, 0xba, 0xc5, 0x29, 0xd9, 0xfa, 0xe9, 0xf9, 0x39, 0x89, 0x79, 0xe9, 0xfa, + 0xc9, 0xf9, 0xc5, 0xb9, 0xf9, 0xc5, 0xfa, 0x15, 0x50, 0x37, 0x96, 0x54, 0x16, 0xa4, 0x16, 0x27, + 0xb1, 0x81, 0x1d, 0x69, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x0a, 0xfe, 0xb6, 0xfd, 0x00, 0x01, + 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -100,6 +112,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa return len(dAtA) - i, nil } @@ -120,6 +142,8 @@ func (m *GenesisState) Size() (n int) { } var l int _ = l + l = m.Params.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -158,16 +182,46 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) if err != nil { return err } - if skippy < 0 { - return ErrInvalidLengthGenesis - } - if (iNdEx + skippy) < 0 { + if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthGenesis } if (iNdEx + skippy) > l { diff --git a/golang/cosmos/x/vbank/types/vbank.pb.go b/golang/cosmos/x/vbank/types/vbank.pb.go new file mode 100644 index 00000000000..90a37716628 --- /dev/null +++ b/golang/cosmos/x/vbank/types/vbank.pb.go @@ -0,0 +1,623 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: agoric/vbank/vbank.proto + +package types + +import ( + fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Params struct { + // feeEpochDurationBlocks is the length of a fee epoch, in blocks. + // A value of zero has the same meaning as a value of one: + // the full fee buffer should be distributed immediately. + FeeEpochDurationBlocks int64 `protobuf:"varint,1,opt,name=feeEpochDurationBlocks,proto3" json:"feeEpochDurationBlocks,omitempty"` +} + +func (m *Params) Reset() { *m = Params{} } +func (m *Params) String() string { return proto.CompactTextString(m) } +func (*Params) ProtoMessage() {} +func (*Params) Descriptor() ([]byte, []int) { + return fileDescriptor_5e89b3b9e5e671b4, []int{0} +} +func (m *Params) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Params) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Params.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Params) XXX_Merge(src proto.Message) { + xxx_messageInfo_Params.Merge(m, src) +} +func (m *Params) XXX_Size() int { + return m.Size() +} +func (m *Params) XXX_DiscardUnknown() { + xxx_messageInfo_Params.DiscardUnknown(m) +} + +var xxx_messageInfo_Params proto.InternalMessageInfo + +func (m *Params) GetFeeEpochDurationBlocks() int64 { + if m != nil { + return m.FeeEpochDurationBlocks + } + return 0 +} + +type State struct { + // rewardPool is the current balance of rewards in the module account. + // NOTE: Tracking manually since there is no bank call for getting a + // module account balance by name. + RewardPool github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,1,rep,name=rewardPool,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"rewardPool"` + // rewardRate is the amount of reward, if available, to send to the + // fee collector module on every block. + RewardRate github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,2,rep,name=rewardRate,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"rewardRate"` +} + +func (m *State) Reset() { *m = State{} } +func (m *State) String() string { return proto.CompactTextString(m) } +func (*State) ProtoMessage() {} +func (*State) Descriptor() ([]byte, []int) { + return fileDescriptor_5e89b3b9e5e671b4, []int{1} +} +func (m *State) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *State) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_State.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *State) XXX_Merge(src proto.Message) { + xxx_messageInfo_State.Merge(m, src) +} +func (m *State) XXX_Size() int { + return m.Size() +} +func (m *State) XXX_DiscardUnknown() { + xxx_messageInfo_State.DiscardUnknown(m) +} + +var xxx_messageInfo_State proto.InternalMessageInfo + +func (m *State) GetRewardPool() github_com_cosmos_cosmos_sdk_types.Coins { + if m != nil { + return m.RewardPool + } + return nil +} + +func (m *State) GetRewardRate() github_com_cosmos_cosmos_sdk_types.Coins { + if m != nil { + return m.RewardRate + } + return nil +} + +func init() { + proto.RegisterType((*Params)(nil), "agoric.vbank.Params") + proto.RegisterType((*State)(nil), "agoric.vbank.State") +} + +func init() { proto.RegisterFile("agoric/vbank/vbank.proto", fileDescriptor_5e89b3b9e5e671b4) } + +var fileDescriptor_5e89b3b9e5e671b4 = []byte{ + // 305 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x91, 0x3f, 0x4e, 0xc3, 0x30, + 0x18, 0xc5, 0x63, 0x0a, 0x1d, 0x0c, 0x53, 0x85, 0x50, 0xe8, 0xe0, 0x56, 0x9d, 0xba, 0x60, 0x53, + 0x90, 0x10, 0x62, 0xa3, 0xfc, 0x99, 0xab, 0xb0, 0xb1, 0x39, 0xae, 0x71, 0xa3, 0xb4, 0xf9, 0x22, + 0xdb, 0x2d, 0x70, 0x0b, 0x8e, 0xc0, 0xcc, 0x49, 0x3a, 0x76, 0x64, 0x02, 0x94, 0x0c, 0x70, 0x0c, + 0x14, 0x3b, 0x88, 0x2e, 0x8c, 0x2c, 0xb6, 0xe5, 0x67, 0xbf, 0xf7, 0x3e, 0xfd, 0x70, 0xc8, 0x15, + 0xe8, 0x44, 0xb0, 0x45, 0xcc, 0xb3, 0xd4, 0xaf, 0x34, 0xd7, 0x60, 0xa1, 0xb5, 0xe3, 0x15, 0xea, + 0xee, 0xda, 0xbb, 0x0a, 0x14, 0x38, 0x81, 0x55, 0x27, 0xff, 0xa6, 0x4d, 0x04, 0x98, 0x19, 0x18, + 0x16, 0x73, 0x23, 0xd9, 0x62, 0x10, 0x4b, 0xcb, 0x07, 0x4c, 0x40, 0x92, 0x79, 0xbd, 0x77, 0x8d, + 0x9b, 0x23, 0xae, 0xf9, 0xcc, 0xb4, 0x4e, 0xf0, 0xde, 0x9d, 0x94, 0x57, 0x39, 0x88, 0xc9, 0xe5, + 0x5c, 0x73, 0x9b, 0x40, 0x36, 0x9c, 0x82, 0x48, 0x4d, 0x88, 0xba, 0xa8, 0xdf, 0x88, 0xfe, 0x50, + 0xcf, 0x36, 0xbf, 0x9e, 0x3b, 0xa8, 0xf7, 0x89, 0xf0, 0xd6, 0x8d, 0xe5, 0x56, 0xb6, 0x52, 0x8c, + 0xb5, 0xbc, 0xe7, 0x7a, 0x3c, 0x02, 0x98, 0x86, 0xa8, 0xdb, 0xe8, 0x6f, 0x1f, 0xed, 0x53, 0x5f, + 0x83, 0x56, 0x35, 0x68, 0x5d, 0x83, 0x5e, 0x40, 0x92, 0x0d, 0x0f, 0x97, 0x6f, 0x9d, 0xe0, 0xe5, + 0xbd, 0xd3, 0x57, 0x89, 0x9d, 0xcc, 0x63, 0x2a, 0x60, 0xc6, 0xea, 0xce, 0x7e, 0x3b, 0x30, 0xe3, + 0x94, 0xd9, 0xc7, 0x5c, 0x1a, 0xf7, 0xc1, 0x44, 0x6b, 0xf6, 0xbf, 0x61, 0x11, 0xb7, 0x32, 0xdc, + 0xf8, 0xb7, 0xb0, 0xca, 0xde, 0x4f, 0x3a, 0x8c, 0x96, 0x05, 0x41, 0xab, 0x82, 0xa0, 0x8f, 0x82, + 0xa0, 0xa7, 0x92, 0x04, 0xab, 0x92, 0x04, 0xaf, 0x25, 0x09, 0x6e, 0x4f, 0xd7, 0x5c, 0xcf, 0x3d, + 0x34, 0x4f, 0xc8, 0xb9, 0x2a, 0x98, 0xf2, 0x4c, 0xfd, 0xc4, 0x3d, 0xd4, 0x3c, 0x5d, 0x56, 0xdc, + 0x74, 0x30, 0x8e, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x22, 0x50, 0xf4, 0xcf, 0xec, 0x01, 0x00, + 0x00, +} + +func (this *Params) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Params) + if !ok { + that2, ok := that.(Params) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.FeeEpochDurationBlocks != that1.FeeEpochDurationBlocks { + return false + } + return true +} +func (this *State) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*State) + if !ok { + that2, ok := that.(State) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.RewardPool) != len(that1.RewardPool) { + return false + } + for i := range this.RewardPool { + if !this.RewardPool[i].Equal(&that1.RewardPool[i]) { + return false + } + } + if len(this.RewardRate) != len(that1.RewardRate) { + return false + } + for i := range this.RewardRate { + if !this.RewardRate[i].Equal(&that1.RewardRate[i]) { + return false + } + } + return true +} +func (m *Params) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Params) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.FeeEpochDurationBlocks != 0 { + i = encodeVarintVbank(dAtA, i, uint64(m.FeeEpochDurationBlocks)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *State) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *State) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *State) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.RewardRate) > 0 { + for iNdEx := len(m.RewardRate) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.RewardRate[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintVbank(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.RewardPool) > 0 { + for iNdEx := len(m.RewardPool) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.RewardPool[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintVbank(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintVbank(dAtA []byte, offset int, v uint64) int { + offset -= sovVbank(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Params) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.FeeEpochDurationBlocks != 0 { + n += 1 + sovVbank(uint64(m.FeeEpochDurationBlocks)) + } + return n +} + +func (m *State) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.RewardPool) > 0 { + for _, e := range m.RewardPool { + l = e.Size() + n += 1 + l + sovVbank(uint64(l)) + } + } + if len(m.RewardRate) > 0 { + for _, e := range m.RewardRate { + l = e.Size() + n += 1 + l + sovVbank(uint64(l)) + } + } + return n +} + +func sovVbank(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozVbank(x uint64) (n int) { + return sovVbank(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Params) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowVbank + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Params: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Params: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field FeeEpochDurationBlocks", wireType) + } + m.FeeEpochDurationBlocks = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowVbank + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.FeeEpochDurationBlocks |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipVbank(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthVbank + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *State) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowVbank + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: State: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: State: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RewardPool", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowVbank + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthVbank + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthVbank + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RewardPool = append(m.RewardPool, types.Coin{}) + if err := m.RewardPool[len(m.RewardPool)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RewardRate", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowVbank + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthVbank + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthVbank + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RewardRate = append(m.RewardRate, types.Coin{}) + if err := m.RewardRate[len(m.RewardRate)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipVbank(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthVbank + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipVbank(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowVbank + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowVbank + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowVbank + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthVbank + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupVbank + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthVbank + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthVbank = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowVbank = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupVbank = fmt.Errorf("proto: unexpected end of group") +) diff --git a/golang/cosmos/x/vbank/vbank.go b/golang/cosmos/x/vbank/vbank.go index e09e29fd73a..0d47e72d2c4 100644 --- a/golang/cosmos/x/vbank/vbank.go +++ b/golang/cosmos/x/vbank/vbank.go @@ -69,6 +69,26 @@ func marshalBalanceUpdate(addressToBalance map[string]sdk.Coins) ([]byte, error) return json.Marshal(&event) } +// rewardRate calculates the rate for dispensing the pool of coins over +// the specified number of blocks. Fractions are rounded up. In other +// words, it returns the smallest Coins such that pool is exhausted +// after #blocks withdrawals. +func rewardRate(pool sdk.Coins, blocks int64) sdk.Coins { + coins := make([]sdk.Coin, 0) + if blocks > 0 { + for _, coin := range pool { + amt := coin.Amount.Int64() + if amt == 0 { + continue + } + // divide by blocks, rounding fractions up + rate := (amt-1)/blocks + 1 + coins = append(coins, sdk.NewInt64Coin(coin.GetDenom(), rate)) + } + } + return sdk.NewCoins(coins...) +} + func (ch portHandler) Receive(ctx *vm.ControllerContext, str string) (ret string, err error) { fmt.Println("vbank.go downcall", str) keeper := ch.keeper @@ -156,6 +176,15 @@ func (ch portHandler) Receive(ctx *vm.ControllerContext, str string) (ret string if err != nil { return "", err } + params := keeper.GetParams(ctx.Context) + blocks := params.FeeEpochDurationBlocks + if blocks < 1 { + blocks = 1 + } + state := keeper.GetState(ctx.Context) + state.RewardPool = state.RewardPool.Add(coins...) + state.RewardRate = rewardRate(state.RewardPool, blocks) + keeper.SetState(ctx.Context, state) // We don't supply the module balance, since the controller shouldn't know. ret = "true" diff --git a/golang/cosmos/x/vbank/vbank_test.go b/golang/cosmos/x/vbank/vbank_test.go index 1a22b5e0ba5..e00cf3a5b71 100644 --- a/golang/cosmos/x/vbank/vbank_test.go +++ b/golang/cosmos/x/vbank/vbank_test.go @@ -9,23 +9,26 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/app/params" "github.com/Agoric/agoric-sdk/golang/cosmos/vm" "github.com/Agoric/agoric-sdk/golang/cosmos/x/vbank/types" + "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/libs/log" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" ) var ( - priv1 = secp256k1.GenPrivKey() - priv2 = secp256k1.GenPrivKey() - priv3 = secp256k1.GenPrivKey() - priv4 = secp256k1.GenPrivKey() - addr1 = sdk.AccAddress(priv1.PubKey().Address()).String() - addr2 = sdk.AccAddress(priv2.PubKey().Address()).String() - addr3 = sdk.AccAddress(priv3.PubKey().Address()).String() - addr4 = sdk.AccAddress(priv4.PubKey().Address()).String() + vbankStoreKey = storetypes.NewKVStoreKey(StoreKey) + priv1 = secp256k1.GenPrivKey() + priv2 = secp256k1.GenPrivKey() + priv3 = secp256k1.GenPrivKey() + priv4 = secp256k1.GenPrivKey() + addr1 = sdk.AccAddress(priv1.PubKey().Address()).String() + addr2 = sdk.AccAddress(priv2.PubKey().Address()).String() + addr3 = sdk.AccAddress(priv3.PubKey().Address()).String() + addr4 = sdk.AccAddress(priv4.PubKey().Address()).String() ) // Normalized balance updates for order-insensitive comparisons. @@ -197,27 +200,35 @@ func (b *mockBank) SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, re return nil } -// makeTestKeeper creates a minimal Keeper for use in testing. -func makeTestKeeper(bank types.BankKeeper) Keeper { +// makeTestKit creates a minimal Keeper and Context for use in testing. +func makeTestKit(bank types.BankKeeper) (Keeper, sdk.Context) { encodingConfig := params.MakeEncodingConfig() cdc := encodingConfig.Marshaller - vbankStoreKey := storetypes.NewKVStoreKey(StoreKey) callToController := func(ctx sdk.Context, str string) (string, error) { return "", nil } - return NewKeeper(cdc, vbankStoreKey, bank, "feeCollectorName", callToController) + keeper := NewKeeper(cdc, vbankStoreKey, bank, "feeCollectorName", callToController) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(vbankStoreKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + ctx := sdk.NewContext(ms, tmproto.Header{}, false, log.NewNopLogger()) + + keeper.SetParams(ctx, types.Params{}) + keeper.SetState(ctx, types.State{}) + return keeper, ctx } func Test_Receive_GetBalance(t *testing.T) { bank := &mockBank{balance: map[string]sdk.Coin{ addr1: sdk.NewInt64Coin("quatloos", 123), }} - keeper := makeTestKeeper(bank) + keeper, ctx := makeTestKit(bank) ch := NewPortHandler(AppModule{}, keeper) - sdkCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) - ctx := &vm.ControllerContext{Context: sdkCtx} + ctlCtx := &vm.ControllerContext{Context: ctx} - ret, err := ch.Receive(ctx, `{ + ret, err := ch.Receive(ctlCtx, `{ "type": "VBANK_GET_BALANCE", "address": "`+addr1+`", "denom": "quatloos" @@ -241,12 +252,11 @@ func Test_Receive_Give(t *testing.T) { bank := &mockBank{balance: map[string]sdk.Coin{ addr1: sdk.NewInt64Coin("urun", 1000), }} - keeper := makeTestKeeper(bank) + keeper, ctx := makeTestKit(bank) ch := NewPortHandler(AppModule{}, keeper) - sdkCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) - ctx := &vm.ControllerContext{Context: sdkCtx} + ctlCtx := &vm.ControllerContext{Context: ctx} - ret, err := ch.Receive(ctx, `{ + ret, err := ch.Receive(ctlCtx, `{ "type": "VBANK_GIVE", "recipient": "`+addr1+`", "amount": "1000", @@ -274,27 +284,93 @@ func Test_Receive_Give(t *testing.T) { func Test_Receive_GiveToFeeCollector(t *testing.T) { bank := &mockBank{} - keeper := makeTestKeeper(bank) + keeper, ctx := makeTestKit(bank) ch := NewPortHandler(AppModule{}, keeper) - sdkCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) - ctx := &vm.ControllerContext{Context: sdkCtx} + ctlCtx := &vm.ControllerContext{Context: ctx} - ret, err := ch.Receive(ctx, `{ - "type": "VBANK_GIVE_TO_FEE_COLLECTOR", - "amount": "1000", - "denom": "urun" - }`) - if err != nil { - t.Fatalf("got error = %v", err) - } - if ret != `true` { - t.Errorf("got %v, want \"true\"", ret) - } - wantCalls := []string{ - "MintCoins vbank 1000urun", + tests := []struct { + name string + duration int64 + rewardPool sdk.Coins + feeAmount string + feeDenom string + wantMintCoins string + wantRate sdk.Coins + }{ + { + name: "durationUnconfigured", + duration: 0, + rewardPool: sdk.NewCoins(), + feeAmount: "1000", + feeDenom: "urun", + wantMintCoins: "1000urun", + wantRate: sdk.NewCoins(sdk.NewInt64Coin("urun", 1000)), + }, + { + name: "one", + duration: 1, + rewardPool: sdk.NewCoins(), + feeAmount: "1000", + feeDenom: "urun", + wantMintCoins: "1000urun", + wantRate: sdk.NewCoins(sdk.NewInt64Coin("urun", 1000)), + }, + { + name: "ten", + duration: 10, + rewardPool: sdk.NewCoins(), + feeAmount: "91", + feeDenom: "urun", + wantMintCoins: "91urun", + wantRate: sdk.NewCoins(sdk.NewInt64Coin("urun", 10)), + }, + { + name: "pool", + duration: 100, + rewardPool: sdk.NewCoins(sdk.NewInt64Coin("urun", 1000)), + feeAmount: "2000", + feeDenom: "urun", + wantMintCoins: "2000urun", + wantRate: sdk.NewCoins(sdk.NewInt64Coin("urun", 30)), + }, + { + name: "mixedDenom", + duration: 100, + rewardPool: sdk.NewCoins(sdk.NewInt64Coin("stickers", 1)), + feeAmount: "99", + feeDenom: "urun", + wantMintCoins: "99urun", + wantRate: sdk.NewCoins( + sdk.NewInt64Coin("urun", 1), + sdk.NewInt64Coin("stickers", 1), + ), + }, } - if !reflect.DeepEqual(bank.calls, wantCalls) { - t.Errorf("got calls %v, want %v", bank.calls, wantCalls) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bank.calls = []string{} + keeper.SetParams(ctx, types.Params{FeeEpochDurationBlocks: tt.duration}) + keeper.SetState(ctx, types.State{RewardPool: tt.rewardPool}) + + ret, err := ch.Receive(ctlCtx, + `{"type": "VBANK_GIVE_TO_FEE_COLLECTOR", "amount": "`+tt.feeAmount+`", "denom": "`+tt.feeDenom+`"}`) + if err != nil { + t.Fatalf("got error = %v", err) + } + if ret != `true` { + t.Errorf("got %v, want \"true\"", ret) + } + wantCalls := []string{ + "MintCoins vbank " + tt.wantMintCoins, + } + if !reflect.DeepEqual(bank.calls, wantCalls) { + t.Errorf("got calls %v, want %v", bank.calls, wantCalls) + } + state := keeper.GetState(ctx) + if !state.RewardRate.IsEqual(tt.wantRate) { + t.Errorf("got rate %v, want %v", state.RewardRate, tt.wantRate) + } + }) } } @@ -302,12 +378,11 @@ func Test_Receive_Grab(t *testing.T) { bank := &mockBank{balance: map[string]sdk.Coin{ addr1: sdk.NewInt64Coin("ubld", 1000), }} - keeper := makeTestKeeper(bank) + keeper, ctx := makeTestKit(bank) ch := NewPortHandler(AppModule{}, keeper) - sdkCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) - ctx := &vm.ControllerContext{Context: sdkCtx} + ctlCtx := &vm.ControllerContext{Context: ctx} - ret, err := ch.Receive(ctx, `{ + ret, err := ch.Receive(ctlCtx, `{ "type": "VBANK_GRAB", "sender": "`+addr1+`", "amount": "500", @@ -333,7 +408,7 @@ func Test_Receive_Grab(t *testing.T) { } } -func Test_EndBlock(t *testing.T) { +func Test_EndBlock_Events(t *testing.T) { bank := &mockBank{allBalances: map[string]sdk.Coins{ addr1: {sdk.NewInt64Coin("ubld", 1000)}, addr2: { @@ -341,7 +416,7 @@ func Test_EndBlock(t *testing.T) { sdk.NewInt64Coin("arcadeTokens", 7), }, }} - keeper := makeTestKeeper(bank) + keeper, ctx := makeTestKit(bank) msgsSent := []string{} keeper.CallToController = func(ctx sdk.Context, str string) (string, error) { msgsSent = append(msgsSent, str) @@ -367,7 +442,7 @@ func Test_EndBlock(t *testing.T) { }, } em := sdk.NewEventManagerWithHistory(events) - ctx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()).WithEventManager(em) + ctx = ctx.WithEventManager(em) updates := am.EndBlock(ctx, abci.RequestEndBlock{}) if len(updates) != 0 { @@ -397,3 +472,122 @@ func Test_EndBlock(t *testing.T) { t.Errorf("got sent message %v, want %v", gotMsg, wantMsg) } } + +func Test_EndBock_Rewards(t *testing.T) { + bank := &mockBank{ + allBalances: map[string]sdk.Coins{ + ModuleName: { + sdk.NewInt64Coin("urun", 1000), + sdk.NewInt64Coin("stickers", 10), + sdk.NewInt64Coin("puppies", 0), + }, + }, + } + keeper, ctx := makeTestKit(bank) + msgsSent := []string{} + keeper.CallToController = func(ctx sdk.Context, str string) (string, error) { + msgsSent = append(msgsSent, str) + return "", nil + } + am := NewAppModule(keeper) + + tests := []struct { + name string + pool sdk.Coins + rate sdk.Coins + wantPool sdk.Coins + wantXfer string + }{ + { + name: "noNothing", + pool: sdk.NewCoins(), + rate: sdk.NewCoins(), + wantPool: sdk.NewCoins(), + wantXfer: "", + }, + { + name: "noRate", + pool: sdk.NewCoins(sdk.NewInt64Coin("urun", 20)), + rate: sdk.NewCoins(), + wantPool: sdk.NewCoins(sdk.NewInt64Coin("urun", 20)), + wantXfer: "", + }, + { + name: "noPool", + pool: sdk.NewCoins(), + rate: sdk.NewCoins(sdk.NewInt64Coin("urun", 5)), + wantPool: sdk.NewCoins(), + wantXfer: "", + }, + { + name: "everything", + pool: sdk.NewCoins(sdk.NewInt64Coin("urun", 12)), + rate: sdk.NewCoins(sdk.NewInt64Coin("urun", 12)), + wantPool: sdk.NewCoins(), + wantXfer: "12urun", + }, + { + name: "easy", + pool: sdk.NewCoins(sdk.NewInt64Coin("urun", 4580)), + rate: sdk.NewCoins(sdk.NewInt64Coin("urun", 25)), + wantPool: sdk.NewCoins(sdk.NewInt64Coin("urun", 4555)), + wantXfer: "25urun", + }, + { + name: "hard", + pool: sdk.NewCoins( + sdk.NewInt64Coin("urun", 1000), + sdk.NewInt64Coin("stickers", 10), + sdk.NewInt64Coin("puppies", 1), + ), + rate: sdk.NewCoins( + sdk.NewInt64Coin("urun", 100), + sdk.NewInt64Coin("stickers", 20), + sdk.NewInt64Coin("arcadeTokens", 3), + ), + wantPool: sdk.NewCoins( + sdk.NewInt64Coin("urun", 900), + sdk.NewInt64Coin("puppies", 1), + ), + wantXfer: "10stickers,100urun", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msgsSent = []string{} + bank.calls = []string{} + state := types.State{ + RewardPool: tt.pool, + RewardRate: tt.rate, + } + keeper.SetState(ctx, state) + + updates := am.EndBlock(ctx, abci.RequestEndBlock{}) + if len(updates) != 0 { + t.Errorf("EndBlock() got %+v, want empty", updates) + } + + if len(msgsSent) != 0 { + t.Errorf("got messages sent = %v, want empty", msgsSent) + } + + state = keeper.GetState(ctx) + if !state.RewardPool.IsEqual(tt.wantPool) { + t.Errorf("got pool %v, want %v", state.RewardPool, tt.wantPool) + } + + if tt.wantXfer == "" { + if len(bank.calls) > 0 { + t.Errorf("got calls %v, want none", bank.calls) + } + } else { + wantCalls := []string{ + "SendCoinsFromModuleToModule vbank feeCollectorName " + tt.wantXfer, + } + if !reflect.DeepEqual(bank.calls, wantCalls) { + t.Errorf("got calls %v, want %v", bank.calls, wantCalls) + } + } + }) + } +}