Add rate limiting to bep3 assets (#623)

* feat: use only module account for bep3 txs

* wip: add time-based supply limits

* add tests and sims

* update genesis tests

* fix migration, committee tests

* update migrations

* fix: set previous block time in begin block

* update store decoder

* add additional bep3 params to committee

* revert incorrect rebase changes

* add migration test

* address review comments
This commit is contained in:
Kevin Davis 2020-08-26 22:05:27 -04:00 committed by GitHub
parent c0006ca8eb
commit 5fc85f10a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 14728 additions and 207 deletions

View File

@ -97,7 +97,7 @@ var (
cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
cdp.SavingsRateMacc: {supply.Minter},
bep3.ModuleName: nil,
bep3.ModuleName: {supply.Minter, supply.Burner},
kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
}

View File

@ -1,6 +1,8 @@
package v0_11
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
v0_11bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_11"
@ -10,6 +12,7 @@ import (
// MigrateBep3 migrates from a v0.9 (or v0.10) bep3 genesis state to a v0.11 bep3 genesis state
func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
var assetParams v0_11bep3.AssetParams
var assetSupplies v0_11bep3.AssetSupplies
v0_9Params := oldGenState.Params
for _, asset := range v0_9Params.SupportedAssets {
@ -19,28 +22,22 @@ func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
CoinID: asset.CoinID,
DeputyAddress: v0_9Params.BnbDeputyAddress,
FixedFee: v0_9Params.BnbDeputyFixedFee,
MinSwapAmount: v0_9Params.MinAmount,
MinSwapAmount: sdk.OneInt(), // set min swap to one - prevents accounts that hold zero bnb from creating spam txs
MaxSwapAmount: v0_9Params.MaxAmount,
MinBlockLock: v0_9Params.MinBlockLock,
MaxBlockLock: v0_9Params.MaxBlockLock,
SupplyLimit: v0_11bep3.AssetSupply{
SupplyLimit: sdk.NewCoin(asset.Denom, sdk.ZeroInt()),
CurrentSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()),
IncomingSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()),
OutgoingSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()),
SupplyLimit: v0_11bep3.SupplyLimit{
Limit: asset.Limit,
TimeLimited: false,
TimePeriod: time.Duration(0),
TimeBasedLimit: sdk.ZeroInt(),
},
}
assetParams = append(assetParams, v10AssetParam)
}
for _, supply := range oldGenState.AssetSupplies {
for _, asset := range assetParams {
if asset.Denom == supply.Denom {
asset.SupplyLimit.SupplyLimit = supply.SupplyLimit
asset.SupplyLimit.CurrentSupply = supply.CurrentSupply
asset.SupplyLimit.IncomingSupply = supply.IncomingSupply
asset.SupplyLimit.OutgoingSupply = supply.OutgoingSupply
}
}
newSupply := v0_11bep3.NewAssetSupply(supply.IncomingSupply, supply.OutgoingSupply, supply.CurrentSupply, sdk.NewCoin(supply.CurrentSupply.Denom, sdk.ZeroInt()), time.Duration(0))
assetSupplies = append(assetSupplies, newSupply)
}
var swaps v0_11bep3.AtomicSwaps
for _, oldSwap := range oldGenState.AtomicSwaps {
@ -63,6 +60,8 @@ func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
return v0_11bep3.GenesisState{
Params: v0_11bep3.Params{
AssetParams: assetParams},
AtomicSwaps: swaps,
AtomicSwaps: swaps,
Supplies: assetSupplies,
PreviousBlockTime: v0_11bep3.DefaultPreviousBlockTime,
}
}

View File

@ -0,0 +1,37 @@
package v0_11
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
v0_9bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_9"
)
func TestMain(m *testing.M) {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
app.SetBip44CoinType(config)
os.Exit(m.Run())
}
func TestMigrateBep3(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "bep3-v09.json"))
require.NoError(t, err)
var oldGenState v0_9bep3.GenesisState
cdc := app.MakeCodec()
require.NotPanics(t, func() {
cdc.MustUnmarshalJSON(bz, &oldGenState)
})
newGenState := MigrateBep3(oldGenState)
err = newGenState.Validate()
require.NoError(t, err)
}

13265
migrate/v0_11/testdata/bep3-v09.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import (
// BeginBlocker on every block expires outdated atomic swaps and removes closed
// swap from long term storage (default storage time of 1 week)
func BeginBlocker(ctx sdk.Context, k Keeper) {
k.UpdateTimeBasedSupplyLimits(ctx)
k.UpdateExpiredAtomicSwaps(ctx)
k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx)
}

View File

@ -120,6 +120,7 @@ var (
DefaultMaxAmount = types.DefaultMaxAmount
DefaultMinBlockLock = types.DefaultMinBlockLock
DefaultMaxBlockLock = types.DefaultMaxBlockLock
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
)
type (
@ -141,6 +142,7 @@ type (
AtomicSwaps = types.AtomicSwaps
SwapStatus = types.SwapStatus
SwapDirection = types.SwapDirection
SupplyLimit = types.SupplyLimit
AugmentedAtomicSwap = types.AugmentedAtomicSwap
AugmentedAtomicSwaps = types.AugmentedAtomicSwaps
)

View File

@ -20,6 +20,8 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
keeper.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
keeper.SetParams(ctx, gs.Params)
for _, supply := range gs.Supplies {
keeper.SetAssetSupply(ctx, supply, supply.GetDenom())
@ -91,17 +93,17 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
if err != nil {
panic(err)
}
if supply.CurrentSupply.Amount.GT(limit) {
panic(fmt.Sprintf("asset's current supply %s is over the supply limit %s", supply.CurrentSupply, limit))
if supply.CurrentSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's current supply %s is over the supply limit %s", supply.CurrentSupply, limit.Limit))
}
if supply.IncomingSupply.Amount.GT(limit) {
panic(fmt.Sprintf("asset's incoming supply %s is over the supply limit %s", supply.IncomingSupply, limit))
if supply.IncomingSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's incoming supply %s is over the supply limit %s", supply.IncomingSupply, limit.Limit))
}
if supply.IncomingSupply.Amount.Add(supply.CurrentSupply.Amount).GT(limit) {
panic(fmt.Sprintf("asset's incoming supply + current supply %s is over the supply limit %s", supply.IncomingSupply.Add(supply.CurrentSupply), limit))
if supply.IncomingSupply.Amount.Add(supply.CurrentSupply.Amount).GT(limit.Limit) {
panic(fmt.Sprintf("asset's incoming supply + current supply %s is over the supply limit %s", supply.IncomingSupply.Add(supply.CurrentSupply), limit.Limit))
}
if supply.OutgoingSupply.Amount.GT(limit) {
panic(fmt.Sprintf("asset's outgoing supply %s is over the supply limit %s", supply.OutgoingSupply, limit))
if supply.OutgoingSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's outgoing supply %s is over the supply limit %s", supply.OutgoingSupply, limit.Limit))
}
}
@ -112,5 +114,9 @@ func ExportGenesis(ctx sdk.Context, k Keeper) (data GenesisState) {
params := k.GetParams(ctx)
swaps := k.GetAllAtomicSwaps(ctx)
supplies := k.GetAllAssetSupplies(ctx)
return NewGenesisState(params, swaps, supplies)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = DefaultPreviousBlockTime
}
return NewGenesisState(params, swaps, supplies, previousBlockTime)
}

View File

@ -96,7 +96,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
bep3.AssetSupply{
IncomingSupply: c("bnb", 0),
OutgoingSupply: c("bnb", 0),
CurrentSupply: c("bnb", assetParam.SupplyLimit.Add(i(1)).Int64()),
CurrentSupply: c("bnb", assetParam.SupplyLimit.Limit.Add(i(1)).Int64()),
},
}
return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
@ -109,7 +109,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb")
overLimitAmount := assetParam.SupplyLimit.Add(i(1))
overLimitAmount := assetParam.SupplyLimit.Limit.Add(i(1))
// Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2)
@ -124,7 +124,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
// Set up asset supply with overlimit current supply
gs.Supplies = bep3.AssetSupplies{
bep3.AssetSupply{
IncomingSupply: c("bnb", assetParam.SupplyLimit.Add(i(1)).Int64()),
IncomingSupply: c("bnb", assetParam.SupplyLimit.Limit.Add(i(1)).Int64()),
OutgoingSupply: c("bnb", 0),
CurrentSupply: c("bnb", 0),
},
@ -139,7 +139,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb")
halfLimit := assetParam.SupplyLimit.Int64() / 2
halfLimit := assetParam.SupplyLimit.Limit.Int64() / 2
overHalfLimit := halfLimit + 1
// Set up an atomic swap with amount equal to the currently asset supply
@ -170,7 +170,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb")
overLimitAmount := assetParam.SupplyLimit.Add(i(1))
overLimitAmount := assetParam.SupplyLimit.Limit.Add(i(1))
// Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2)
@ -187,7 +187,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
bep3.AssetSupply{
IncomingSupply: c("bnb", 0),
OutgoingSupply: c("bnb", 0),
CurrentSupply: c("bnb", assetParam.SupplyLimit.Add(i(1)).Int64()),
CurrentSupply: c("bnb", assetParam.SupplyLimit.Limit.Add(i(1)).Int64()),
},
}
return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
@ -266,7 +266,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
name: "negative supported asset limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[0].SupplyLimit = i(-100)
gs.Params.AssetParams[0].SupplyLimit.Limit = i(-100)
return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
},
expectPass: false,

View File

@ -40,9 +40,14 @@ func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
Params: bep3.Params{
AssetParams: bep3.AssetParams{
bep3.AssetParam{
Denom: "bnb",
CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000),
Denom: "bnb",
CoinID: 714,
SupplyLimit: bep3.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000),
@ -52,9 +57,14 @@ func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
MaxBlockLock: bep3.DefaultMaxBlockLock,
},
bep3.AssetParam{
Denom: "inc",
CoinID: 9999,
SupplyLimit: sdk.NewInt(100000000000),
Denom: "inc",
CoinID: 9999,
SupplyLimit: bep3.SupplyLimit{
Limit: sdk.NewInt(100000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000),
@ -70,13 +80,18 @@ func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
time.Duration(0),
),
bep3.NewAssetSupply(
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
time.Duration(0),
),
},
PreviousBlockTime: bep3.DefaultPreviousBlockTime,
}
return bep3Genesis
}
@ -92,7 +107,7 @@ func loadSwapAndSupply(addr sdk.AccAddress, index int) (bep3.AtomicSwap, bep3.As
TestRecipientOtherChain, 1, bep3.Open, true, bep3.Incoming)
supply := bep3.NewAssetSupply(coin, c(coin.Denom, 0),
c(coin.Denom, 0))
c(coin.Denom, 0), c(coin.Denom, 0), time.Duration(0))
return swap, supply
}

View File

@ -1,6 +1,8 @@
package keeper
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -18,13 +20,21 @@ func (k Keeper) IncrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) erro
if err != nil {
return err
}
supplyLimit := sdk.NewCoin(coin.Denom, limit)
supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
// Resulting current supply must be under asset's limit
if supplyLimit.IsLT(supply.CurrentSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, supply.CurrentSupply, supplyLimit)
}
if limit.TimeLimited {
timeBasedSupplyLimit := sdk.NewCoin(coin.Denom, limit.TimeBasedLimit)
if timeBasedSupplyLimit.IsLT(supply.TimeLimitedCurrentSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsTimeBasedSupplyLimit, "increase %s, current time-based asset supply %s, limit %s", coin, supply.TimeLimitedCurrentSupply, timeBasedSupplyLimit)
}
supply.TimeLimitedCurrentSupply = supply.TimeLimitedCurrentSupply.Add(coin)
}
supply.CurrentSupply = supply.CurrentSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
@ -62,11 +72,19 @@ func (k Keeper) IncrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) err
if err != nil {
return err
}
supplyLimit := sdk.NewCoin(coin.Denom, limit)
supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
if supplyLimit.IsLT(totalSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, totalSupply, supplyLimit)
}
if limit.TimeLimited {
timeLimitedTotalSupply := supply.TimeLimitedCurrentSupply.Add(supply.IncomingSupply)
timeBasedSupplyLimit := sdk.NewCoin(coin.Denom, limit.TimeBasedLimit)
if timeBasedSupplyLimit.IsLT(timeLimitedTotalSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsTimeBasedSupplyLimit, "increase %s, time-based asset supply %s, limit %s", coin, supply.TimeLimitedCurrentSupply, timeBasedSupplyLimit)
}
}
supply.IncomingSupply = supply.IncomingSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
@ -125,3 +143,42 @@ func (k Keeper) DecrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) err
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// CreateNewAssetSupply creates a new AssetSupply in the store for the input denom
func (k Keeper) CreateNewAssetSupply(ctx sdk.Context, denom string) types.AssetSupply {
supply := types.NewAssetSupply(
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()),
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()), time.Duration(0))
k.SetAssetSupply(ctx, supply, denom)
return supply
}
// UpdateTimeBasedSupplyLimits updates the time based supply for each asset, resetting it if the current time window has elapsed.
func (k Keeper) UpdateTimeBasedSupplyLimits(ctx sdk.Context) {
assets, found := k.GetAssets(ctx)
if !found {
return
}
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
}
timeElapsed := ctx.BlockTime().Sub(previousBlockTime)
for _, asset := range assets {
supply, found := k.GetAssetSupply(ctx, asset.Denom)
// if a new asset has been added by governance, create a new asset supply for it in the store
if !found {
supply = k.CreateNewAssetSupply(ctx, asset.Denom)
}
newTimeElapsed := supply.TimeElapsed + timeElapsed
if asset.SupplyLimit.TimeLimited && newTimeElapsed < asset.SupplyLimit.TimePeriod {
supply.TimeElapsed = newTimeElapsed
} else {
supply.TimeElapsed = time.Duration(0)
supply.TimeLimitedCurrentSupply = sdk.NewCoin(asset.Denom, sdk.ZeroInt())
}
k.SetAssetSupply(ctx, supply, asset.Denom)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}

View File

@ -1,7 +1,9 @@
package keeper_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -37,15 +39,17 @@ func (suite *AssetTestSuite) SetupTest() {
keeper := tApp.GetBep3Keeper()
params := keeper.GetParams(ctx)
params.AssetParams[0].SupplyLimit = sdk.NewInt(50)
params.AssetParams[0].SupplyLimit.Limit = sdk.NewInt(50)
params.AssetParams[1].SupplyLimit.Limit = sdk.NewInt(100)
params.AssetParams[1].SupplyLimit.TimeBasedLimit = sdk.NewInt(15)
keeper.SetParams(ctx, params)
// Set asset supply with standard value for testing
supply := types.AssetSupply{
IncomingSupply: c("bnb", 5),
OutgoingSupply: c("bnb", 5),
CurrentSupply: c("bnb", 40),
}
keeper.SetAssetSupply(ctx, supply, supply.GetDenom())
supply := types.NewAssetSupply(c("bnb", 5), c("bnb", 5), c("bnb", 40), c("bnb", 0), time.Duration(0))
keeper.SetAssetSupply(ctx, supply, supply.IncomingSupply.Denom)
supply = types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0))
keeper.SetAssetSupply(ctx, supply, supply.IncomingSupply.Denom)
keeper.SetPreviousBlockTime(ctx, ctx.BlockTime())
suite.app = tApp
suite.ctx = ctx
@ -112,6 +116,64 @@ func (suite *AssetTestSuite) TestIncrementCurrentAssetSupply() {
}
}
func (suite *AssetTestSuite) TestIncrementTimeLimitedCurrentAssetSupply() {
type args struct {
coin sdk.Coin
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"normal",
args{
coin: c("inc", 5),
expectedSupply: types.AssetSupply{
IncomingSupply: c("inc", 10),
OutgoingSupply: c("inc", 5),
CurrentSupply: c("inc", 10),
TimeLimitedCurrentSupply: c("inc", 5),
TimeElapsed: time.Duration(0)},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over limit",
args{
coin: c("inc", 16),
expectedSupply: types.AssetSupply{},
},
errArgs{
expectPass: false,
contains: "asset supply over limit for current time period",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
supply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
suite.Require().Equal(tc.args.expectedSupply, supply)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *AssetTestSuite) TestDecrementCurrentAssetSupply() {
type args struct {
coin sdk.Coin
@ -229,6 +291,64 @@ func (suite *AssetTestSuite) TestIncrementIncomingAssetSupply() {
}
}
func (suite *AssetTestSuite) TestIncrementTimeLimitedIncomingAssetSupply() {
type args struct {
coin sdk.Coin
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"normal",
args{
coin: c("inc", 5),
expectedSupply: types.AssetSupply{
IncomingSupply: c("inc", 15),
OutgoingSupply: c("inc", 5),
CurrentSupply: c("inc", 5),
TimeLimitedCurrentSupply: c("inc", 0),
TimeElapsed: time.Duration(0)},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over limit",
args{
coin: c("inc", 6),
expectedSupply: types.AssetSupply{},
},
errArgs{
expectPass: false,
contains: "asset supply over limit for current time period",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
err := suite.keeper.IncrementIncomingAssetSupply(suite.ctx, tc.args.coin)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
supply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
suite.Require().Equal(tc.args.expectedSupply, supply)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *AssetTestSuite) TestDecrementIncomingAssetSupply() {
type args struct {
coin sdk.Coin
@ -405,6 +525,181 @@ func (suite *AssetTestSuite) TestDecrementOutgoingAssetSupply() {
}
}
func (suite *AssetTestSuite) TestUpdateTimeBasedSupplyLimits() {
type args struct {
asset string
duration time.Duration
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPanic bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"rate-limited increment time",
args{
asset: "inc",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Second),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited increment time half",
args{
asset: "inc",
duration: time.Minute * 30,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Minute*30),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change",
args{
asset: "inc",
duration: time.Hour + time.Second,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change exact",
args{
asset: "inc",
duration: time.Hour,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change big",
args{
asset: "inc",
duration: time.Hour * 4,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"non rate-limited increment time",
args{
asset: "bnb",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("bnb", 5), c("bnb", 5), c("bnb", 40), c("bnb", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"new asset increment time",
args{
asset: "lol",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("lol", 0), c("lol", 0), c("lol", 0), c("lol", 0), time.Second),
},
errArgs{
expectPanic: false,
contains: "",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
deputy, _ := sdk.AccAddressFromBech32(TestDeputy)
newParams := types.Params{
AssetParams: types.AssetParams{
types.AssetParam{
Denom: "bnb",
CoinID: 714,
SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdk.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
types.AssetParam{
Denom: "inc",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(10),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdk.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
types.AssetParam{
Denom: "lol",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(10),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdk.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
},
}
suite.keeper.SetParams(suite.ctx, newParams)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(tc.args.duration))
suite.NotPanics(
func() {
suite.keeper.UpdateTimeBasedSupplyLimits(suite.ctx)
},
)
if !tc.errArgs.expectPanic {
supply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.asset)
suite.Require().True(found)
suite.Require().Equal(tc.args.expectedSupply, supply)
}
})
}
}
func TestAssetTestSuite(t *testing.T) {
suite.Run(t, new(AssetTestSuite))
}

View File

@ -9,7 +9,6 @@ import (
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/bep3/types"
)
@ -32,12 +31,17 @@ func ts(minOffset int) int64 { return tmtime.Now().Add(time.Durat
func NewBep3GenStateMulti(deputyAddress sdk.AccAddress) app.GenesisState {
bep3Genesis := types.GenesisState{
Params: bep3.Params{
Params: types.Params{
AssetParams: types.AssetParams{
types.AssetParam{
Denom: "bnb",
CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000),
Denom: "bnb",
CoinID: 714,
SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000),
@ -47,33 +51,43 @@ func NewBep3GenStateMulti(deputyAddress sdk.AccAddress) app.GenesisState {
MaxBlockLock: types.DefaultMaxBlockLock,
},
types.AssetParam{
Denom: "inc",
CoinID: 9999,
SupplyLimit: sdk.NewInt(100),
Denom: "inc",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(100000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdk.NewInt(1000000000000),
MaxSwapAmount: sdk.NewInt(100000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
},
},
Supplies: bep3.AssetSupplies{
bep3.NewAssetSupply(
Supplies: types.AssetSupplies{
types.NewAssetSupply(
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
time.Duration(0),
),
bep3.NewAssetSupply(
types.NewAssetSupply(
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
time.Duration(0),
),
},
PreviousBlockTime: types.DefaultPreviousBlockTime,
}
return app.GenesisState{bep3.ModuleName: bep3.ModuleCdc.MustMarshalJSON(bep3Genesis)}
return app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(bep3Genesis)}
}
func atomicSwaps(ctx sdk.Context, count int) types.AtomicSwaps {
@ -112,5 +126,5 @@ func assetSupplies(count int) types.AssetSupplies {
}
func assetSupply(denom string) types.AssetSupply {
return types.NewAssetSupply(c(denom, 0), c(denom, 0), c(denom, 0))
return types.NewAssetSupply(c(denom, 0), c(denom, 0), c(denom, 0), c(denom, 0), time.Duration(0))
}

View File

@ -2,6 +2,7 @@ package keeper
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
@ -222,3 +223,20 @@ func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSuppli
})
return
}
// GetPreviousBlockTime get the blocktime for the previous block
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b := store.Get([]byte{})
if b == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
return blockTime, true
}
// SetPreviousBlockTime set the time of the previous block
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime))
}

View File

@ -310,7 +310,7 @@ func (suite *KeeperTestSuite) TestIterateAtomicSwapsLongtermStorage() {
func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
denom := "bnb"
// Put asset supply in store
assetSupply := types.NewAssetSupply(c(denom, 0), c(denom, 0), c(denom, 50000))
assetSupply := types.NewAssetSupply(c(denom, 0), c(denom, 0), c(denom, 50000), c(denom, 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, denom)
// Check asset in store
@ -327,9 +327,9 @@ func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
func (suite *KeeperTestSuite) TestGetAllAssetSupplies() {
// Put asset supply in store
assetSupply := types.NewAssetSupply(c("bnb", 0), c("bnb", 0), c("bnb", 50000))
assetSupply := types.NewAssetSupply(c("bnb", 0), c("bnb", 0), c("bnb", 50000), c("bnb", 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, "bnb")
assetSupply = types.NewAssetSupply(c("inc", 0), c("inc", 0), c("inc", 50000))
assetSupply = types.NewAssetSupply(c("inc", 0), c("inc", 0), c("inc", 50000), c("inc", 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, "inc")
supplies := suite.keeper.GetAllAssetSupplies(suite.ctx)

View File

@ -132,10 +132,10 @@ func (k Keeper) ValidateLiveAsset(ctx sdk.Context, coin sdk.Coin) error {
}
// GetSupplyLimit returns the supply limit for the input denom
func (k Keeper) GetSupplyLimit(ctx sdk.Context, denom string) (sdk.Int, error) {
func (k Keeper) GetSupplyLimit(ctx sdk.Context, denom string) (types.SupplyLimit, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return sdk.Int{}, err
return types.SupplyLimit{}, err
}
return asset.SupplyLimit, nil
}

View File

@ -4,6 +4,7 @@ import (
"encoding/hex"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -102,7 +103,7 @@ func (suite *QuerierTestSuite) TestQueryAssetSupply() {
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &supply))
expectedSupply := types.NewAssetSupply(c(denom, 1000),
c(denom, 0), c(denom, 0))
c(denom, 0), c(denom, 0), c(denom, 0), time.Duration(0))
suite.Equal(supply, expectedSupply)
}

View File

@ -79,7 +79,7 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times
err = k.IncrementIncomingAssetSupply(ctx, amount[0])
case types.Outgoing:
// Outoing swaps must have a height span within the accepted range
// Outgoing swaps must have a height span within the accepted range
if heightSpan < asset.MinBlockLock || heightSpan > asset.MaxBlockLock {
return sdkerrors.Wrapf(types.ErrInvalidHeightSpan, "height span %d outside range [%d, %d]", heightSpan, asset.MinBlockLock, asset.MaxBlockLock)
}
@ -88,6 +88,11 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times
return sdkerrors.Wrap(types.ErrInsufficientAmount, amount[0].String())
}
err = k.IncrementOutgoingAssetSupply(ctx, amount[0])
if err != nil {
return err
}
// Transfer coins to module - only needed for outgoing swaps
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount)
default:
err = fmt.Errorf("invalid swap direction: %s", direction.String())
}
@ -95,12 +100,6 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times
return err
}
// Transfer coins to module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount)
if err != nil {
return err
}
// Store the details of the swap
expireHeight := uint64(ctx.BlockHeight()) + heightSpan
atomicSwap := types.NewAtomicSwap(amount, randomNumberHash, expireHeight, timestamp, sender,
@ -161,6 +160,16 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b
if err != nil {
return err
}
// incoming case - coins should be MINTED, then sent to user
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, atomicSwap.Amount)
if err != nil {
return err
}
// Send intended recipient coins
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount)
if err != nil {
return err
}
case types.Outgoing:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
@ -170,16 +179,15 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b
if err != nil {
return err
}
// outgoing case - coins should be burned
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, atomicSwap.Amount)
if err != nil {
return err
}
default:
return fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
}
// Send intended recipient coins
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount)
if err != nil {
return err
}
// Complete swap
atomicSwap.Status = types.Completed
atomicSwap.ClosedBlock = ctx.BlockHeight()
@ -221,6 +229,11 @@ func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []
err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
case types.Outgoing:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
// Refund coins to original swap sender for outgoing swaps
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount)
default:
err = fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
}
@ -229,12 +242,6 @@ func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []
return err
}
// Refund coins to original swap sender
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount)
if err != nil {
return err
}
// Complete swap
atomicSwap.Status = types.Completed
atomicSwap.ClosedBlock = ctx.BlockHeight()

View File

@ -34,8 +34,10 @@ type AtomicSwapTestSuite struct {
}
const (
STARING_BNB_BALANCE = int64(3000000000000)
BNB_DENOM = "bnb"
STARING_BNB_BALANCE = int64(3000000000000)
BNB_DENOM = "bnb"
OTHER_DENOM = "inc"
STARING_OTHER_BALANCE = int64(3000000000000)
)
func (suite *AtomicSwapTestSuite) SetupTest() {
@ -49,7 +51,7 @@ func (suite *AtomicSwapTestSuite) SetupTest() {
// Create and load 20 accounts with bnb tokens
coins := []sdk.Coins{}
for i := 0; i < 20; i++ {
coins = append(coins, cs(c(BNB_DENOM, STARING_BNB_BALANCE)))
coins = append(coins, cs(c(BNB_DENOM, STARING_BNB_BALANCE), c(OTHER_DENOM, STARING_OTHER_BALANCE)))
}
_, addrs := app.GeneratePrivKeyAddressPairs(20)
deputy := addrs[0]
@ -58,11 +60,16 @@ func (suite *AtomicSwapTestSuite) SetupTest() {
// Initialize genesis state
tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(deputy))
keeper := tApp.GetBep3Keeper()
params := keeper.GetParams(ctx)
params.AssetParams[1].Active = true
keeper.SetParams(ctx, params)
suite.app = tApp
suite.ctx = ctx
suite.deputy = deputy
suite.addrs = addrs
suite.keeper = suite.app.GetBep3Keeper()
suite.keeper = keeper
// Load a random module account to test blacklisting
i := 0
@ -138,6 +145,42 @@ func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() {
true,
true,
},
{
"incoming swap rate limited",
currentTmTime.Add(time.Minute * 10),
args{
randomNumberHash: suite.randomNumberHashes[12],
timestamp: suite.timestamps[12],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c("inc", 50000000000)),
crossChain: true,
direction: types.Incoming,
},
true,
true,
},
{
"incoming swap over rate limit",
currentTmTime.Add(time.Minute * 10),
args{
randomNumberHash: suite.randomNumberHashes[13],
timestamp: suite.timestamps[13],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c("inc", 50000000001)),
crossChain: true,
direction: types.Incoming,
},
false,
false,
},
{
"outgoing swap",
currentTmTime,
@ -396,14 +439,13 @@ func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() {
if tc.expectPass {
suite.NoError(err)
// Check coins moved
suite.Equal(senderBalancePre.Sub(tc.args.coins[0].Amount), senderBalancePost)
// Check incoming/outgoing asset supply increased
switch tc.args.direction {
case types.Incoming:
suite.Equal(assetSupplyPre.IncomingSupply.Add(tc.args.coins[0]), assetSupplyPost.IncomingSupply)
case types.Outgoing:
// Check coins moved
suite.Equal(senderBalancePre.Sub(tc.args.coins[0].Amount), senderBalancePost)
suite.Equal(assetSupplyPre.OutgoingSupply.Add(tc.args.coins[0]), assetSupplyPost.OutgoingSupply)
default:
suite.Fail("should not have invalid direction")
@ -460,8 +502,10 @@ func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() {
func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
suite.SetupTest()
currentTmTime := tmtime.Now()
invalidRandomNumber, _ := types.GenerateSecureRandomNumber()
type args struct {
coins sdk.Coins
swapID []byte
randomNumber []byte
direction types.SwapDirection
@ -476,6 +520,18 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
"normal incoming swap",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.Incoming,
},
true,
},
{
"normal incoming swap rate-limited",
suite.ctx.WithBlockTime(currentTmTime.Add(time.Minute * 10)),
args{
coins: cs(c(OTHER_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.Incoming,
@ -486,6 +542,7 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
"normal outgoing swap",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.Outgoing,
@ -496,6 +553,7 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
"invalid random number",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: invalidRandomNumber[:],
direction: types.Incoming,
@ -506,6 +564,7 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
"wrong swap ID",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: types.CalculateSwapID(suite.randomNumberHashes[3], suite.addrs[6], TestRecipientOtherChain),
randomNumber: []byte{},
direction: types.Outgoing,
@ -516,6 +575,7 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
"past expiration",
suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 2000),
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.Incoming,
@ -528,21 +588,20 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
suite.GenerateSwapDetails()
suite.Run(tc.name, func() {
expectedRecipient := suite.addrs[5]
expectedClaimAmount := cs(c(BNB_DENOM, 50000))
sender := suite.deputy
// Set sender to other and increment current asset supply for outgoing swap
if tc.args.direction == types.Outgoing {
sender = suite.addrs[6]
expectedRecipient = suite.deputy
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedClaimAmount[0])
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coins[0])
suite.Nil(err)
}
// Create atomic swap
err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i],
types.DefaultMinBlockLock, sender, expectedRecipient, TestSenderOtherChain, TestRecipientOtherChain,
expectedClaimAmount, true)
tc.args.coins, true)
suite.NoError(err)
realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain)
@ -569,40 +628,40 @@ func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
// Load expected recipient's account prior to claim attempt
ak := suite.app.GetAccountKeeper()
expectedRecipientAccPre := ak.GetAccount(tc.claimCtx, expectedRecipient)
expectedRecipientBalancePre := expectedRecipientAccPre.GetCoins().AmountOf(expectedClaimAmount[0].Denom)
expectedRecipientBalancePre := expectedRecipientAccPre.GetCoins().AmountOf(tc.args.coins[0].Denom)
// Load asset supplies prior to claim attempt
assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.claimCtx, expectedClaimAmount[0].Denom)
assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.claimCtx, tc.args.coins[0].Denom)
// Attempt to claim atomic swap
err = suite.keeper.ClaimAtomicSwap(tc.claimCtx, expectedRecipient, claimSwapID, claimRandomNumber)
// Load expected recipient's account after the claim attempt
expectedRecipientAccPost := ak.GetAccount(tc.claimCtx, expectedRecipient)
expectedRecipientBalancePost := expectedRecipientAccPost.GetCoins().AmountOf(expectedClaimAmount[0].Denom)
expectedRecipientBalancePost := expectedRecipientAccPost.GetCoins().AmountOf(tc.args.coins[0].Denom)
// Load asset supplies after the claim attempt
assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.claimCtx, expectedClaimAmount[0].Denom)
assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.claimCtx, tc.args.coins[0].Denom)
if tc.expectPass {
suite.NoError(err)
// Check coins moved
suite.Equal(expectedRecipientBalancePre.Add(expectedClaimAmount[0].Amount), expectedRecipientBalancePost)
// Check asset supply changes
switch tc.args.direction {
case types.Incoming:
// Check coins moved
suite.Equal(expectedRecipientBalancePre.Add(tc.args.coins[0].Amount), expectedRecipientBalancePost)
// Check incoming supply decreased
suite.True(assetSupplyPre.IncomingSupply.Amount.Sub(expectedClaimAmount[0].Amount).Equal(assetSupplyPost.IncomingSupply.Amount))
suite.True(assetSupplyPre.IncomingSupply.Amount.Sub(tc.args.coins[0].Amount).Equal(assetSupplyPost.IncomingSupply.Amount))
// Check current supply increased
suite.Equal(assetSupplyPre.CurrentSupply.Add(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.CurrentSupply.Add(tc.args.coins[0]), assetSupplyPost.CurrentSupply)
// Check outgoing supply not changed
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.Outgoing:
// Check incoming supply not changed
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
// Check current supply decreased
suite.Equal(assetSupplyPre.CurrentSupply.Sub(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.CurrentSupply.Sub(tc.args.coins[0]), assetSupplyPost.CurrentSupply)
// Check outgoing supply decreased
suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedClaimAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply))
suite.True(assetSupplyPre.OutgoingSupply.Sub(tc.args.coins[0]).IsEqual(assetSupplyPost.OutgoingSupply))
default:
suite.Fail("should not have invalid direction")
}
@ -732,8 +791,6 @@ func (suite *AtomicSwapTestSuite) TestRefundAtomicSwap() {
if tc.expectPass {
suite.NoError(err)
// Check coins moved
suite.Equal(originalSenderBalancePre.Add(expectedRefundAmount[0].Amount), originalSenderBalancePost)
// Check asset supply changes
switch tc.args.direction {
@ -744,6 +801,8 @@ func (suite *AtomicSwapTestSuite) TestRefundAtomicSwap() {
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.Outgoing:
// Check coins moved
suite.Equal(originalSenderBalancePre.Add(expectedRefundAmount[0].Amount), originalSenderBalancePost)
// Check incoming, current supply not changed
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)

View File

@ -1,14 +1,65 @@
package v0_11
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/tendermint/tendermint/crypto/tmhash"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtime "github.com/tendermint/tendermint/types/time"
)
var (
AddrByteCount = 20
RandomNumberHashLength = 32
RandomNumberLength = 32
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
)
// GenesisState - all bep3 state that must be provided at genesis
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
Supplies AssetSupplies `json:"supplies" yaml:"supplies"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
}
// Validate validates genesis inputs. It returns error if validation of any input fails.
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
ids := map[string]bool{}
for _, swap := range gs.AtomicSwaps {
if ids[hex.EncodeToString(swap.GetSwapID())] {
return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(swap.GetSwapID()))
}
if err := swap.Validate(); err != nil {
return err
}
ids[hex.EncodeToString(swap.GetSwapID())] = true
}
supplyDenoms := map[string]bool{}
for _, supply := range gs.Supplies {
if err := supply.Validate(); err != nil {
return err
}
if supplyDenoms[supply.GetDenom()] {
return fmt.Errorf("found duplicate denom in asset supplies %s", supply.GetDenom())
}
supplyDenoms[supply.GetDenom()] = true
}
return nil
}
// Params governance parameters for the bep3 module
@ -16,11 +67,83 @@ type Params struct {
AssetParams AssetParams `json:"asset_params" yaml:"asset_params"`
}
// Validate ensure that params have valid values
func (p Params) Validate() error {
return validateAssetParams(p.AssetParams)
}
func validateAssetParams(i interface{}) error {
assetParams, ok := i.(AssetParams)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
coinDenoms := make(map[string]bool)
for _, asset := range assetParams {
if err := sdk.ValidateDenom(asset.Denom); err != nil {
return fmt.Errorf(fmt.Sprintf("asset denom invalid: %s", asset.Denom))
}
if asset.CoinID < 0 {
return fmt.Errorf(fmt.Sprintf("asset %s coin id must be a non negative integer", asset.Denom))
}
if asset.SupplyLimit.Limit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply limit: %s", asset.Denom, asset.SupplyLimit.Limit))
}
if asset.SupplyLimit.TimeBasedLimit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply time limit: %s", asset.Denom, asset.SupplyLimit.TimeBasedLimit))
}
if asset.SupplyLimit.TimeBasedLimit.GT(asset.SupplyLimit.Limit) {
return fmt.Errorf(fmt.Sprintf("asset %s cannot have supply time limit > supply limit: %s>%s", asset.Denom, asset.SupplyLimit.TimeBasedLimit, asset.SupplyLimit.Limit))
}
_, found := coinDenoms[asset.Denom]
if found {
return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate denom", asset.Denom))
}
coinDenoms[asset.Denom] = true
if asset.DeputyAddress.Empty() {
return fmt.Errorf("deputy address cannot be empty for %s", asset.Denom)
}
if len(asset.DeputyAddress.Bytes()) != sdk.AddrLen {
return fmt.Errorf("%s deputy address invalid bytes length got %d, want %d", asset.Denom, len(asset.DeputyAddress.Bytes()), sdk.AddrLen)
}
if asset.FixedFee.IsNegative() {
return fmt.Errorf("asset %s cannot have a negative fixed fee %s", asset.Denom, asset.FixedFee)
}
if asset.MinBlockLock > asset.MaxBlockLock {
return fmt.Errorf("asset %s has minimum block lock > maximum block lock %d > %d", asset.Denom, asset.MinBlockLock, asset.MaxBlockLock)
}
if !asset.MinSwapAmount.IsPositive() {
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive minimum swap amount, got %s", asset.Denom, asset.MinSwapAmount))
}
if !asset.MaxSwapAmount.IsPositive() {
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive maximum swap amount, got %s", asset.Denom, asset.MaxSwapAmount))
}
if asset.MinSwapAmount.GT(asset.MaxSwapAmount) {
return fmt.Errorf("asset %s has minimum swap amount > maximum swap amount %s > %s", asset.Denom, asset.MinSwapAmount, asset.MaxSwapAmount)
}
}
return nil
}
// AssetParam parameters that must be specified for each bep3 asset
type AssetParam struct {
Denom string `json:"denom" yaml:"denom"` // name of the asset
CoinID int `json:"coin_id" yaml:"coin_id"` // SLIP-0044 registered coin type - see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
SupplyLimit AssetSupply `json:"supply_limit" yaml:"supply_limit"` // asset supply limit
SupplyLimit SupplyLimit `json:"supply_limit" yaml:"supply_limit"` // asset supply limit
Active bool `json:"active" yaml:"active"` // denotes if asset is available or paused
DeputyAddress sdk.AccAddress `json:"deputy_address" yaml:"deputy_address"` // the address of the relayer process
FixedFee sdk.Int `json:"incoming_swap_fixed_fee" yaml:"incoming_swap_fixed_fee"` // the fixed fee charged by the relayer process for incoming swaps
@ -30,17 +153,93 @@ type AssetParam struct {
MaxBlockLock uint64 `json:"max_block_lock" yaml:"max_block_lock"` // Maximum swap block lock
}
// SupplyLimit parameters that control the absolute and time-based limits for an assets's supply
type SupplyLimit struct {
Limit sdk.Int `json:"limit" yaml:"limit"` // the absolute supply limit for an asset
TimeLimited bool `json:"time_limited" yaml:"time_limited"` // boolean for if the supply is also limited by time
TimePeriod time.Duration `json:"time_period" yaml:"time_period"` // the duration for which the supply time limit applies
TimeBasedLimit sdk.Int `json:"time_based_limit" yaml:"time_based_limit"` // the supply limit for an asset for each time period
}
// AssetParams array of AssetParam
type AssetParams []AssetParam
// AssetSupply contains information about an asset's supply
type AssetSupply struct {
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
SupplyLimit sdk.Coin `json:"supply_limit" yaml:"supply_limit"`
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
TimeLimitedCurrentSupply sdk.Coin `json:"time_limited_current_supply" yaml:"time_limited_current_supply"`
TimeElapsed time.Duration `json:"time_elapsed" yaml:"time_elapsed"`
}
// NewAssetSupply initializes a new AssetSupply
func NewAssetSupply(incomingSupply, outgoingSupply, currentSupply, timeLimitedSupply sdk.Coin, timeElapsed time.Duration) AssetSupply {
return AssetSupply{
IncomingSupply: incomingSupply,
OutgoingSupply: outgoingSupply,
CurrentSupply: currentSupply,
TimeLimitedCurrentSupply: timeLimitedSupply,
TimeElapsed: timeElapsed,
}
}
// Validate performs a basic validation of an asset supply fields.
func (a AssetSupply) Validate() error {
if !a.IncomingSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "incoming supply %s", a.IncomingSupply)
}
if !a.OutgoingSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "outgoing supply %s", a.OutgoingSupply)
}
if !a.CurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "current supply %s", a.CurrentSupply)
}
if !a.TimeLimitedCurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "time-limited current supply %s", a.CurrentSupply)
}
denom := a.CurrentSupply.Denom
if (a.IncomingSupply.Denom != denom) ||
(a.OutgoingSupply.Denom != denom) ||
(a.TimeLimitedCurrentSupply.Denom != denom) {
return fmt.Errorf("asset supply denoms do not match %s %s %s %s", a.CurrentSupply.Denom, a.IncomingSupply.Denom, a.OutgoingSupply.Denom, a.TimeLimitedCurrentSupply.Denom)
}
return nil
}
// Equal returns if two asset supplies are equal
func (a AssetSupply) Equal(b AssetSupply) bool {
if a.GetDenom() != b.GetDenom() {
return false
}
return (a.IncomingSupply.IsEqual(b.IncomingSupply) &&
a.CurrentSupply.IsEqual(b.CurrentSupply) &&
a.OutgoingSupply.IsEqual(b.OutgoingSupply) &&
a.TimeLimitedCurrentSupply.IsEqual(b.TimeLimitedCurrentSupply) &&
a.TimeElapsed == b.TimeElapsed)
}
// String implements stringer
func (a AssetSupply) String() string {
return fmt.Sprintf(`
asset supply:
Incoming supply: %s
Outgoing supply: %s
Current supply: %s
Time-limited current cupply: %s
Time elapsed: %s
`,
a.IncomingSupply, a.OutgoingSupply, a.CurrentSupply, a.TimeLimitedCurrentSupply, a.TimeElapsed)
}
// GetDenom getter method for the denom of the asset supply
func (a AssetSupply) GetDenom() string {
return a.CurrentSupply.Denom
}
// AssetSupplies is a slice of AssetSupply
type AssetSupplies []AssetSupply
// AtomicSwap contains the information for an atomic swap
type AtomicSwap struct {
Amount sdk.Coins `json:"amount" yaml:"amount"`
@ -57,6 +256,73 @@ type AtomicSwap struct {
Direction SwapDirection `json:"direction" yaml:"direction"`
}
// CalculateSwapID calculates the hash of a RandomNumberHash, sdk.AccAddress, and string
func CalculateSwapID(randomNumberHash []byte, sender sdk.AccAddress, senderOtherChain string) []byte {
senderOtherChain = strings.ToLower(senderOtherChain)
data := randomNumberHash
data = append(data, sender.Bytes()...)
data = append(data, []byte(senderOtherChain)...)
return tmhash.Sum(data)
}
// GetSwapID calculates the ID of an atomic swap
func (a AtomicSwap) GetSwapID() tmbytes.HexBytes {
return CalculateSwapID(a.RandomNumberHash, a.Sender, a.SenderOtherChain)
}
// GetCoins returns the swap's amount as sdk.Coins
func (a AtomicSwap) GetCoins() sdk.Coins {
return sdk.NewCoins(a.Amount...)
}
// Validate performs a basic validation of an atomic swap fields.
func (a AtomicSwap) Validate() error {
if !a.Amount.IsValid() {
return fmt.Errorf("invalid amount: %s", a.Amount)
}
if !a.Amount.IsAllPositive() {
return fmt.Errorf("the swapped out coin must be positive: %s", a.Amount)
}
if len(a.RandomNumberHash) != RandomNumberHashLength {
return fmt.Errorf("the length of random number hash should be %d", RandomNumberHashLength)
}
if a.ExpireHeight == 0 {
return errors.New("expire height cannot be 0")
}
if a.Timestamp == 0 {
return errors.New("timestamp cannot be 0")
}
if a.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender cannot be empty")
}
if a.Recipient.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "recipient cannot be empty")
}
if len(a.Sender) != AddrByteCount {
return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Sender))
}
if len(a.Recipient) != AddrByteCount {
return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Recipient))
}
// NOTE: These adresses may not have a bech32 prefix.
if strings.TrimSpace(a.SenderOtherChain) == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender other chain cannot be blank")
}
if strings.TrimSpace(a.RecipientOtherChain) == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "recipient other chain cannot be blank")
}
if a.Status == Completed && a.ClosedBlock == 0 {
return errors.New("closed block cannot be 0")
}
if a.Status == NULL || a.Status > 3 {
return errors.New("invalid swap status")
}
if a.Direction == INVALID || a.Direction > 2 {
return errors.New("invalid swap direction")
}
return nil
}
// AtomicSwaps is a slice of AtomicSwap
type AtomicSwaps []AtomicSwap
@ -71,6 +337,50 @@ const (
Expired SwapStatus = 0x03
)
// MarshalJSON marshals the SwapStatus
func (status SwapStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(status.String())
}
// UnmarshalJSON unmarshals the SwapStatus
func (status *SwapStatus) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
*status = NewSwapStatusFromString(s)
return nil
}
// NewSwapStatusFromString converts string to SwapStatus type
func NewSwapStatusFromString(str string) SwapStatus {
switch str {
case "Open", "open":
return Open
case "Completed", "completed":
return Completed
case "Expired", "expired":
return Expired
default:
return NULL
}
}
// String returns the string representation of a SwapStatus
func (status SwapStatus) String() string {
switch status {
case Open:
return "Open"
case Completed:
return "Completed"
case Expired:
return "Expired"
default:
return "NULL"
}
}
// SwapDirection is the direction of an AtomicSwap
type SwapDirection byte
@ -79,3 +389,52 @@ const (
Incoming SwapDirection = 0x01
Outgoing SwapDirection = 0x02
)
// NewSwapDirectionFromString converts string to SwapDirection type
func NewSwapDirectionFromString(str string) SwapDirection {
switch str {
case "Incoming", "incoming", "inc", "I", "i":
return Incoming
case "Outgoing", "outgoing", "out", "O", "o":
return Outgoing
default:
return INVALID
}
}
// String returns the string representation of a SwapDirection
func (direction SwapDirection) String() string {
switch direction {
case Incoming:
return "Incoming"
case Outgoing:
return "Outgoing"
default:
return "INVALID"
}
}
// MarshalJSON marshals the SwapDirection
func (direction SwapDirection) MarshalJSON() ([]byte, error) {
return json.Marshal(direction.String())
}
// UnmarshalJSON unmarshals the SwapDirection
func (direction *SwapDirection) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
*direction = NewSwapDirectionFromString(s)
return nil
}
// IsValid returns true if the swap direction is valid and false otherwise.
func (direction SwapDirection) IsValid() bool {
if direction == Incoming ||
direction == Outgoing {
return true
}
return false
}

View File

@ -1,6 +1,8 @@
package v0_9
import (
"encoding/json"
sdk "github.com/cosmos/cosmos-sdk/types"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
)
@ -141,6 +143,50 @@ const (
Expired SwapStatus = 0x03
)
// MarshalJSON marshals the SwapStatus
func (status SwapStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(status.String())
}
// UnmarshalJSON unmarshals the SwapStatus
func (status *SwapStatus) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
*status = NewSwapStatusFromString(s)
return nil
}
// NewSwapStatusFromString converts string to SwapStatus type
func NewSwapStatusFromString(str string) SwapStatus {
switch str {
case "Open", "open":
return Open
case "Completed", "completed":
return Completed
case "Expired", "expired":
return Expired
default:
return NULL
}
}
// String returns the string representation of a SwapStatus
func (status SwapStatus) String() string {
switch status {
case Open:
return "Open"
case Completed:
return "Completed"
case Expired:
return "Expired"
default:
return "NULL"
}
}
// SwapDirection is the direction of an AtomicSwap
type SwapDirection byte
@ -149,3 +195,52 @@ const (
Incoming SwapDirection = 0x01
Outgoing SwapDirection = 0x02
)
// NewSwapDirectionFromString converts string to SwapDirection type
func NewSwapDirectionFromString(str string) SwapDirection {
switch str {
case "Incoming", "incoming", "inc", "I", "i":
return Incoming
case "Outgoing", "outgoing", "out", "O", "o":
return Outgoing
default:
return INVALID
}
}
// String returns the string representation of a SwapDirection
func (direction SwapDirection) String() string {
switch direction {
case Incoming:
return "Incoming"
case Outgoing:
return "Outgoing"
default:
return "INVALID"
}
}
// MarshalJSON marshals the SwapDirection
func (direction SwapDirection) MarshalJSON() ([]byte, error) {
return json.Marshal(direction.String())
}
// UnmarshalJSON unmarshals the SwapDirection
func (direction *SwapDirection) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
*direction = NewSwapDirectionFromString(s)
return nil
}
// IsValid returns true if the swap direction is valid and false otherwise.
func (direction SwapDirection) IsValid() bool {
if direction == Incoming ||
direction == Outgoing {
return true
}
return false
}

View File

@ -3,6 +3,7 @@ package simulation
import (
"bytes"
"fmt"
"time"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
"github.com/tendermint/tendermint/libs/kv"
@ -31,6 +32,11 @@ func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &supplyA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &supplyB)
return fmt.Sprintf("%s\n%s", supplyA, supplyB)
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))

View File

@ -3,6 +3,7 @@ package simulation
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
@ -22,12 +23,13 @@ func makeTestCodec() (cdc *codec.Codec) {
return
}
func TestDecodeDistributionStore(t *testing.T) {
func TestDecodeBep3Store(t *testing.T) {
cdc := makeTestCodec()
prevBlockTime := time.Now().UTC()
oneCoin := sdk.NewCoin("coin", sdk.OneInt())
swap := types.NewAtomicSwap(sdk.Coins{oneCoin}, nil, 10, 100, nil, nil, "otherChainSender", "otherChainRec", 200, types.Completed, true, types.Outgoing)
supply := types.AssetSupply{IncomingSupply: oneCoin, OutgoingSupply: oneCoin, CurrentSupply: oneCoin}
supply := types.AssetSupply{IncomingSupply: oneCoin, OutgoingSupply: oneCoin, CurrentSupply: oneCoin, TimeLimitedCurrentSupply: oneCoin, TimeElapsed: time.Duration(0)}
bz := tmbytes.HexBytes([]byte{1, 2})
kvPairs := kv.Pairs{
@ -35,6 +37,7 @@ func TestDecodeDistributionStore(t *testing.T) {
kv.Pair{Key: types.AssetSupplyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(supply)},
kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz},
kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz},
kv.Pair{Key: types.PreviousBlockTimeKey, Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
@ -46,6 +49,7 @@ func TestDecodeDistributionStore(t *testing.T) {
{"AssetSupply", fmt.Sprintf("%v\n%v", supply, supply)},
{"AtomicSwapByBlock", fmt.Sprintf("%s\n%s", bz, bz)},
{"AtomicSwapLongtermStorage", fmt.Sprintf("%s\n%s", bz, bz)},
{"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
{"other", ""},
}
for i, tt := range tests {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -61,7 +62,7 @@ func GenSupplyLimit(r *rand.Rand, max int) sdk.Int {
func GenAssetSupply(r *rand.Rand, denom string) types.AssetSupply {
return types.NewAssetSupply(
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()),
sdk.NewCoin(denom, sdk.ZeroInt()))
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()), time.Duration(0))
}
// GenMinBlockLock randomized MinBlockLock
@ -97,10 +98,22 @@ func genSupportedAsset(r *rand.Rand, denom string) types.AssetParam {
minSwapAmount := GenMinSwapAmount(r)
minBlockLock := GenMinBlockLock(r)
timeLimited := r.Float32() < 0.5
timeBasedLimit := sdk.ZeroInt()
if timeLimited {
// set time-based limit to between 10 and 25% of the total limit
min := int(limit.Quo(sdk.NewInt(10)).Int64())
max := int(limit.Quo(sdk.NewInt(4)).Int64())
timeBasedLimit = sdk.NewInt(int64(simulation.RandIntBetween(r, min, max)))
}
return types.AssetParam{
Denom: denom,
CoinID: int(coinID.Int64()),
SupplyLimit: limit,
Denom: denom,
CoinID: int(coinID.Int64()),
SupplyLimit: types.SupplyLimit{
Limit: limit,
TimeLimited: timeLimited,
TimePeriod: time.Hour * 24,
TimeBasedLimit: timeBasedLimit},
Active: true,
DeputyAddress: GenRandBnbDeputy(r).Address,
FixedFee: GenRandFixedFee(r),
@ -145,7 +158,8 @@ func loadRandomBep3GenState(simState *module.SimulationState) types.GenesisState
Params: types.Params{
AssetParams: supportedAssets,
},
Supplies: supplies,
Supplies: supplies,
PreviousBlockTime: types.DefaultPreviousBlockTime,
}
return bep3Genesis
@ -161,7 +175,7 @@ func loadAuthGenState(simState *module.SimulationState, bep3Genesis types.Genesi
if !found {
panic("deputy address not found in available accounts")
}
assetCoin := sdk.NewCoins(sdk.NewCoin(asset.Denom, asset.SupplyLimit))
assetCoin := sdk.NewCoins(sdk.NewCoin(asset.Denom, asset.SupplyLimit.Limit))
if err := deputy.SetCoins(deputy.GetCoins().Add(assetCoin...)); err != nil {
panic(err)
}

View File

@ -127,15 +127,27 @@ func SimulateMsgCreateAtomicSwap(ak types.AccountKeeper, k keeper.Keeper) simula
// Get maximum valid amount
maximumAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(asset.Denom)
assetSupply, foundAssetSupply := k.GetAssetSupply(ctx, asset.Denom)
if !foundAssetSupply {
return noOpMsg, nil, fmt.Errorf("no asset supply found for %s", asset.Denom)
}
// The maximum amount for outgoing swaps is limited by the asset's current supply
if recipient.Address.Equals(asset.DeputyAddress) {
assetSupply, foundAssetSupply := k.GetAssetSupply(ctx, asset.Denom)
if !foundAssetSupply {
return noOpMsg, nil, fmt.Errorf("no asset supply found for %s", asset.Denom)
}
if maximumAmount.GT(assetSupply.CurrentSupply.Amount.Sub(assetSupply.OutgoingSupply.Amount)) {
maximumAmount = assetSupply.CurrentSupply.Amount.Sub(assetSupply.OutgoingSupply.Amount)
}
} else {
// the maximum amount for incoming swaps in limited by the asset's incoming supply + current supply (rate-limited if applicable) + swap amount being less than the supply limit
var currentRemainingSupply sdk.Int
if asset.SupplyLimit.TimeLimited {
currentRemainingSupply = asset.SupplyLimit.Limit.Sub(assetSupply.IncomingSupply.Amount).Sub(assetSupply.TimeLimitedCurrentSupply.Amount)
} else {
currentRemainingSupply = asset.SupplyLimit.Limit.Sub(assetSupply.IncomingSupply.Amount).Sub(assetSupply.CurrentSupply.Amount)
}
if currentRemainingSupply.LT(maximumAmount) {
maximumAmount = currentRemainingSupply
}
}
// The maximum amount for all swaps is limited by the total max limit
@ -147,7 +159,7 @@ func SimulateMsgCreateAtomicSwap(ak types.AccountKeeper, k keeper.Keeper) simula
amount := maximumAmount.Quo(sdk.NewInt(int64(simulation.RandIntBetween(r, 50, 1000))))
minAmountPlusFee := asset.MinSwapAmount.Add(asset.FixedFee)
if amount.LT(minAmountPlusFee) {
return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (all funds exhausted for asset %s)", asset.Denom), "", false, nil), nil, nil
return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (account funds exhausted for asset %s)", asset.Denom), "", false, nil), nil, nil
}
coins := sdk.NewCoins(sdk.NewCoin(asset.Denom, amount))
@ -242,7 +254,7 @@ func operationClaimAtomicSwap(ak types.AccountKeeper, k keeper.Keeper, swapID []
if !found {
return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (could not claim - asset supply not found %s)", swap.Amount[0].Denom), "", false, nil), nil, nil
}
if asset.SupplyLimit.LT(supply.CurrentSupply.Amount.Add(swap.Amount[0].Amount)) {
if asset.SupplyLimit.Limit.LT(supply.CurrentSupply.Amount.Add(swap.Amount[0].Amount)) {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}

View File

@ -43,4 +43,6 @@ var (
ErrInvalidAmount = sdkerrors.Register(ModuleName, 18, "amount is outside acceptable range")
// ErrInvalidSwapAccount error for when a swap involves an invalid account
ErrInvalidSwapAccount = sdkerrors.Register(ModuleName, 19, "atomic swap has invalid account")
// ErrExceedsTimeBasedSupplyLimit error for when the proposed supply increase would put the supply above limit for the current time period
ErrExceedsTimeBasedSupplyLimit = sdkerrors.Register(ModuleName, 20, "asset supply over limit for current time period")
)

View File

@ -13,6 +13,8 @@ type SupplyKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
// AccountKeeper defines the expected account keeper (noalias)

View File

@ -4,21 +4,24 @@ import (
"bytes"
"encoding/hex"
"fmt"
"time"
)
// GenesisState - all bep3 state that must be provided at genesis
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
Supplies AssetSupplies `json:"supplies" yaml:"supplies"`
Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
Supplies AssetSupplies `json:"supplies" yaml:"supplies"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
}
// NewGenesisState creates a new GenesisState object
func NewGenesisState(params Params, swaps AtomicSwaps, supplies AssetSupplies) GenesisState {
func NewGenesisState(params Params, swaps AtomicSwaps, supplies AssetSupplies, previousBlockTime time.Time) GenesisState {
return GenesisState{
Params: params,
AtomicSwaps: swaps,
Supplies: supplies,
Params: params,
AtomicSwaps: swaps,
Supplies: supplies,
PreviousBlockTime: previousBlockTime,
}
}
@ -28,6 +31,7 @@ func DefaultGenesisState() GenesisState {
DefaultParams(),
AtomicSwaps{},
AssetSupplies{},
DefaultPreviousBlockTime,
)
}

View File

@ -2,6 +2,7 @@ package types_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -24,13 +25,15 @@ func (suite *GenesisTestSuite) SetupTest() {
coin := sdk.NewCoin("kava", sdk.OneInt())
suite.swaps = atomicSwaps(10)
supply := types.NewAssetSupply(coin, coin, coin)
supply := types.NewAssetSupply(coin, coin, coin, coin, time.Duration(0))
suite.supplies = types.AssetSupplies{supply}
}
func (suite *GenesisTestSuite) TestValidate() {
type args struct {
swaps types.AtomicSwaps
swaps types.AtomicSwaps
supplies types.AssetSupplies
previousBlockTime time.Time
}
testCases := []struct {
name string
@ -40,31 +43,60 @@ func (suite *GenesisTestSuite) TestValidate() {
{
"default",
args{
swaps: types.AtomicSwaps{},
swaps: types.AtomicSwaps{},
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"with swaps",
args{
swaps: suite.swaps,
swaps: suite.swaps,
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"with supplies",
args{
swaps: types.AtomicSwaps{},
supplies: suite.supplies,
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"invalid supply",
args{
swaps: types.AtomicSwaps{},
supplies: types.AssetSupplies{types.AssetSupply{IncomingSupply: sdk.Coin{"Invalid", sdk.ZeroInt()}}},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"duplicate swaps",
args{
swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]},
swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"invalid swap",
args{
swaps: types.AtomicSwaps{types.AtomicSwap{Amount: sdk.Coins{sdk.Coin{Denom: "Invalid Denom", Amount: sdk.NewInt(-1)}}}},
swaps: types.AtomicSwaps{types.AtomicSwap{Amount: sdk.Coins{sdk.Coin{Denom: "Invalid Denom", Amount: sdk.NewInt(-1)}}}},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"blocktime not set",
args{
swaps: types.AtomicSwaps{},
},
true,
},
}
for _, tc := range testCases {
@ -73,7 +105,7 @@ func (suite *GenesisTestSuite) TestValidate() {
if tc.name == "default" {
gs = types.DefaultGenesisState()
} else {
gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, suite.supplies)
gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies, tc.args.previousBlockTime)
}
err := gs.Validate()

View File

@ -30,6 +30,7 @@ var (
AtomicSwapByBlockPrefix = []byte{0x01} // prefix for keys of the AtomicSwapsByBlock index
AtomicSwapLongtermStoragePrefix = []byte{0x02} // prefix for keys of the AtomicSwapLongtermStorage index
AssetSupplyPrefix = []byte{0x03}
PreviousBlockTimeKey = []byte{0x04}
)
// GetAtomicSwapByHeightKey is used by the AtomicSwapByBlock index and AtomicSwapLongtermStorage index

View File

@ -2,9 +2,16 @@ package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
bech32MainPrefix = "kava"
)
// Parameter keys
@ -16,6 +23,7 @@ var (
DefaultMaxAmount sdk.Int = sdk.NewInt(1000000000000) // 10,000 BNB
DefaultMinBlockLock uint64 = 220
DefaultMaxBlockLock uint64 = 270
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
)
// Params governance parameters for bep3 module
@ -47,7 +55,7 @@ func DefaultParams() Params {
type AssetParam struct {
Denom string `json:"denom" yaml:"denom"` // name of the asset
CoinID int `json:"coin_id" yaml:"coin_id"` // SLIP-0044 registered coin type - see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
SupplyLimit sdk.Int `json:"supply_limit" yaml:"supply_limit"` // asset supply limit
SupplyLimit SupplyLimit `json:"supply_limit" yaml:"supply_limit"` // asset supply limit
Active bool `json:"active" yaml:"active"` // denotes if asset is available or paused
DeputyAddress sdk.AccAddress `json:"deputy_address" yaml:"deputy_address"` // the address of the relayer process
FixedFee sdk.Int `json:"fixed_fee" yaml:"fixed_fee"` // the fixed fee charged by the relayer process for outgoing swaps
@ -59,7 +67,7 @@ type AssetParam struct {
// NewAssetParam returns a new AssetParam
func NewAssetParam(
denom string, coinID int, limit sdk.Int, active bool,
denom string, coinID int, limit SupplyLimit, active bool,
deputyAddr sdk.AccAddress, fixedFee sdk.Int, minSwapAmount sdk.Int,
maxSwapAmount sdk.Int, minBlockLock uint64, maxBlockLock uint64,
) AssetParam {
@ -106,6 +114,28 @@ func (aps AssetParams) String() string {
return out
}
// SupplyLimit parameters that control the absolute and time-based limits for an assets's supply
type SupplyLimit struct {
Limit sdk.Int `json:"limit" yaml:"limit"` // the absolute supply limit for an asset
TimeLimited bool `json:"time_limited" yaml:"time_limited"` // boolean for if the supply is also limited by time
TimePeriod time.Duration `json:"time_period" yaml:"time_period"` // the duration for which the supply time limit applies
TimeBasedLimit sdk.Int `json:"time_based_limit" yaml:"time_based_limit"` // the supply limit for an asset for each time period
}
// String implements fmt.Stringer
func (sl SupplyLimit) String() string {
return fmt.Sprintf(`%s
%t
%s
%s
`, sl.Limit, sl.TimeLimited, sl.TimePeriod, sl.TimeBasedLimit)
}
// Equals returns true if two supply limits are equal
func (sl SupplyLimit) Equals(sl2 SupplyLimit) bool {
return sl.Limit.Equal(sl2.Limit) && sl.TimeLimited == sl2.TimeLimited && sl.TimePeriod == sl2.TimePeriod && sl.TimeBasedLimit.Equal(sl2.TimeBasedLimit)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
@ -134,20 +164,28 @@ func validateAssetParams(i interface{}) error {
coinDenoms := make(map[string]bool)
for _, asset := range assetParams {
if err := sdk.ValidateDenom(asset.Denom); err != nil {
return fmt.Errorf("asset denom invalid: %s", asset.Denom)
return fmt.Errorf(fmt.Sprintf("asset denom invalid: %s", asset.Denom))
}
if asset.CoinID < 0 {
return fmt.Errorf("asset %s coin id must be a non negative integer", asset.Denom)
return fmt.Errorf(fmt.Sprintf("asset %s coin id must be a non negative integer", asset.Denom))
}
if asset.SupplyLimit.IsNegative() {
return fmt.Errorf("asset %s has invalid (negative) supply limit: %s", asset.Denom, asset.SupplyLimit)
if asset.SupplyLimit.Limit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply limit: %s", asset.Denom, asset.SupplyLimit.Limit))
}
if asset.SupplyLimit.TimeBasedLimit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply time limit: %s", asset.Denom, asset.SupplyLimit.TimeBasedLimit))
}
if asset.SupplyLimit.TimeBasedLimit.GT(asset.SupplyLimit.Limit) {
return fmt.Errorf(fmt.Sprintf("asset %s cannot have supply time limit > supply limit: %s>%s", asset.Denom, asset.SupplyLimit.TimeBasedLimit, asset.SupplyLimit.Limit))
}
_, found := coinDenoms[asset.Denom]
if found {
return fmt.Errorf("asset %s cannot have duplicate denom", asset.Denom)
return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate denom", asset.Denom))
}
coinDenoms[asset.Denom] = true
@ -169,11 +207,11 @@ func validateAssetParams(i interface{}) error {
}
if !asset.MinSwapAmount.IsPositive() {
return fmt.Errorf("asset %s must have a positive minimum swap amount, got %s", asset.Denom, asset.MinSwapAmount)
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive minimum swap amount, got %s", asset.Denom, asset.MinSwapAmount))
}
if !asset.MaxSwapAmount.IsPositive() {
return fmt.Errorf("asset %s must have a positive maximum swap amount, got %s", asset.Denom, asset.MaxSwapAmount)
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive maximum swap amount, got %s", asset.Denom, asset.MaxSwapAmount))
}
if asset.MinSwapAmount.GT(asset.MaxSwapAmount) {

View File

@ -2,6 +2,7 @@ package types_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -14,7 +15,7 @@ import (
type ParamsTestSuite struct {
suite.Suite
addr sdk.AccAddress
supply []sdk.Int
supply []types.SupplyLimit
}
func (suite *ParamsTestSuite) SetupTest() {
@ -22,7 +23,19 @@ func (suite *ParamsTestSuite) SetupTest() {
app.SetBech32AddressPrefixes(config)
_, addrs := app.GeneratePrivKeyAddressPairs(1)
suite.addr = addrs[0]
suite.supply = append(suite.supply, sdk.NewInt(10000000000000), sdk.NewInt(10000000000000))
supply1 := types.SupplyLimit{
Limit: sdk.NewInt(10000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
}
supply2 := types.SupplyLimit{
Limit: sdk.NewInt(10000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(100000000000),
TimePeriod: time.Hour * 24,
}
suite.supply = append(suite.supply, supply1, supply2)
return
}
@ -57,6 +70,17 @@ func (suite *ParamsTestSuite) TestParamValidation() {
expectPass: true,
expectedErr: "",
},
{
name: "valid single asset time limited",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[1], true,
suite.addr, sdk.NewInt(1000), sdk.NewInt(100000000), sdk.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: true,
expectedErr: "",
},
{
name: "valid multi asset",
args: args{
@ -166,13 +190,38 @@ func (suite *ParamsTestSuite) TestParamValidation() {
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
sdk.NewInt(-10000000000000), true,
types.SupplyLimit{sdk.NewInt(-10000000000000), false, time.Hour, sdk.ZeroInt()}, true,
suite.addr, sdk.NewInt(1000), sdk.NewInt(100000000), sdk.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "invalid (negative) supply limit",
},
{
name: "negative asset time limit",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
types.SupplyLimit{sdk.NewInt(10000000000000), false, time.Hour, sdk.NewInt(-10000000000000)}, true,
suite.addr, sdk.NewInt(1000), sdk.NewInt(100000000), sdk.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "invalid (negative) supply time limit",
},
{
name: "asset time limit greater than overall limit",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
types.SupplyLimit{sdk.NewInt(10000000000000), true, time.Hour, sdk.NewInt(100000000000000)},
true,
suite.addr, sdk.NewInt(1000), sdk.NewInt(100000000), sdk.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "supply time limit > supply limit",
},
{
name: "duplicate denom",
args: args{
@ -201,7 +250,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
suite.Require().Error(err, tc.name)
suite.Require().Contains(err.Error(), tc.expectedErr)
}
})
}
}

View File

@ -2,6 +2,7 @@ package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -9,17 +10,21 @@ import (
// AssetSupply contains information about an asset's supply
type AssetSupply struct {
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
TimeLimitedCurrentSupply sdk.Coin `json:"time_limited_current_supply" yaml:"time_limited_current_supply"`
TimeElapsed time.Duration `json:"time_elapsed" yaml:"time_elapsed"`
}
// NewAssetSupply initializes a new AssetSupply
func NewAssetSupply(incomingSupply, outgoingSupply, currentSupply sdk.Coin) AssetSupply {
func NewAssetSupply(incomingSupply, outgoingSupply, currentSupply, timeLimitedSupply sdk.Coin, timeElapsed time.Duration) AssetSupply {
return AssetSupply{
IncomingSupply: incomingSupply,
OutgoingSupply: outgoingSupply,
CurrentSupply: currentSupply,
IncomingSupply: incomingSupply,
OutgoingSupply: outgoingSupply,
CurrentSupply: currentSupply,
TimeLimitedCurrentSupply: timeLimitedSupply,
TimeElapsed: timeElapsed,
}
}
@ -34,19 +39,28 @@ func (a AssetSupply) Validate() error {
if !a.CurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "current supply %s", a.CurrentSupply)
}
if !a.TimeLimitedCurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "time-limited current supply %s", a.CurrentSupply)
}
denom := a.CurrentSupply.Denom
if (a.IncomingSupply.Denom != denom) ||
(a.OutgoingSupply.Denom != denom) {
return fmt.Errorf("asset supply denoms do not match %s %s %s", a.CurrentSupply.Denom, a.IncomingSupply.Denom, a.OutgoingSupply.Denom)
(a.OutgoingSupply.Denom != denom) ||
(a.TimeLimitedCurrentSupply.Denom != denom) {
return fmt.Errorf("asset supply denoms do not match %s %s %s %s", a.CurrentSupply.Denom, a.IncomingSupply.Denom, a.OutgoingSupply.Denom, a.TimeLimitedCurrentSupply.Denom)
}
return nil
}
// Equal returns if two asset supplies are equal
func (a AssetSupply) Equal(b AssetSupply) bool {
if a.GetDenom() != b.GetDenom() {
return false
}
return (a.IncomingSupply.IsEqual(b.IncomingSupply) &&
a.CurrentSupply.IsEqual(b.CurrentSupply) &&
a.OutgoingSupply.IsEqual(b.OutgoingSupply))
a.OutgoingSupply.IsEqual(b.OutgoingSupply) &&
a.TimeLimitedCurrentSupply.IsEqual(b.TimeLimitedCurrentSupply) &&
a.TimeElapsed == b.TimeElapsed)
}
// String implements stringer
@ -56,8 +70,10 @@ func (a AssetSupply) String() string {
Incoming supply: %s
Outgoing supply: %s
Current supply: %s
Time-limited current cupply: %s
Time elapsed: %s
`,
a.IncomingSupply, a.OutgoingSupply, a.CurrentSupply)
a.IncomingSupply, a.OutgoingSupply, a.CurrentSupply, a.TimeLimitedCurrentSupply, a.TimeElapsed)
}
// GetDenom getter method for the denom of the asset supply

View File

@ -2,6 +2,7 @@ package types
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
@ -17,7 +18,7 @@ func TestAssetSupplyValidate(t *testing.T) {
}{
{
msg: "valid asset",
asset: NewAssetSupply(coin, coin, coin),
asset: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
expPass: true,
},
{
@ -42,6 +43,27 @@ func TestAssetSupplyValidate(t *testing.T) {
},
false,
},
{
"invalid time limitedcurrent supply",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: coin,
CurrentSupply: coin,
TimeLimitedCurrentSupply: invalidCoin,
},
false,
},
{
"non matching denoms",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: coin,
CurrentSupply: coin,
TimeLimitedCurrentSupply: sdk.NewCoin("lol", sdk.ZeroInt()),
TimeElapsed: time.Hour,
},
false,
},
}
for _, tc := range testCases {
@ -53,3 +75,47 @@ func TestAssetSupplyValidate(t *testing.T) {
}
}
}
func TestAssetSupplyEquality(t *testing.T) {
coin := sdk.NewCoin("test", sdk.OneInt())
coin2 := sdk.NewCoin("other", sdk.OneInt())
testCases := []struct {
name string
asset1 AssetSupply
asset2 AssetSupply
expPass bool
}{
{
name: "equal",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
expPass: true,
},
{
name: "not equal duration",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin, coin, coin, coin, time.Duration(1)),
expPass: false,
},
{
name: "not equal coin amount",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(sdk.NewCoin("test", sdk.ZeroInt()), coin, coin, coin, time.Duration(1)),
expPass: false,
},
{
name: "not equal coin denom",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin2, coin2, coin2, coin2, time.Duration(1)),
expPass: false,
},
}
for _, tc := range testCases {
if tc.expPass {
require.True(t, tc.asset1.Equal(tc.asset2), tc.name)
} else {
require.False(t, tc.asset1.Equal(tc.asset2), tc.name)
}
}
}

View File

@ -2,6 +2,7 @@ package keeper_test
import (
"testing"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -83,9 +84,14 @@ func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
// bep3 Asset Params
testAPs := bep3types.AssetParams{
bep3types.AssetParam{
Denom: "bnb",
CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000),
Denom: "bnb",
CoinID: 714,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: testDeputy,
FixedFee: sdk.NewInt(1000),
@ -95,9 +101,14 @@ func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
MaxBlockLock: bep3types.DefaultMaxBlockLock,
},
bep3types.AssetParam{
Denom: "inc",
CoinID: 9999,
SupplyLimit: sdk.NewInt(100),
Denom: "inc",
CoinID: 9999,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(100000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: testDeputy,
FixedFee: sdk.NewInt(1000),

View File

@ -5,6 +5,7 @@ import (
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation"
@ -58,7 +59,7 @@ func RandomizedGenState(simState *module.SimulationState) {
[]types.Proposal{},
[]types.Vote{},
)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, []byte{})
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState)
}

View File

@ -1,6 +1,8 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
@ -159,9 +161,14 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
deputyAddress := sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1")))
testAPs := bep3types.AssetParams{
bep3types.AssetParam{
Denom: "btc",
CoinID: 0,
SupplyLimit: sdk.NewInt(100),
Denom: "btc",
CoinID: 0,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000),
@ -171,9 +178,14 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
MaxBlockLock: bep3types.DefaultMaxBlockLock,
},
bep3types.AssetParam{
Denom: "bnb",
CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000),
Denom: "bnb",
CoinID: 714,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000),
@ -183,9 +195,14 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
MaxBlockLock: bep3types.DefaultMaxBlockLock,
},
bep3types.AssetParam{
Denom: "xrp",
CoinID: 414,
SupplyLimit: sdk.NewInt(350000000000000),
Denom: "xrp",
CoinID: 414,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000),
@ -200,10 +217,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
updatedTestAPs[1] = testAPs[0]
updatedTestAPs[2] = testAPs[2]
updatedTestAPs[0].SupplyLimit = i(1000) // btc
updatedTestAPs[1].Active = false // bnb
updatedTestAPs[2].SupplyLimit = i(1000) // xrp
updatedTestAPs[2].Active = false // xrp
updatedTestAPs[0].SupplyLimit.Limit = i(1000) // btc
updatedTestAPs[1].Active = false // bnb
updatedTestAPs[2].SupplyLimit.Limit = i(1000) // xrp
updatedTestAPs[2].Active = false // xrp
updatedTestAPs[2].MinBlockLock = uint64(210) // xrp
updatedTestAPs[2].MaxSwapAmount = sdk.NewInt(10000000000000)
testcases := []struct {
name string
@ -224,10 +243,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
Limit: true,
},
{ // allow all fields
Denom: "xrp",
CoinID: true,
Limit: true,
Active: true,
Denom: "xrp",
CoinID: true,
Limit: true,
Active: true,
MaxSwapAmount: true,
MinBlockLock: true,
},
},
current: testAPs[:2],
@ -265,9 +286,11 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
Limit: true,
},
{
Denom: "xrp",
Limit: true,
Active: true,
Denom: "xrp",
Limit: true,
Active: true,
MaxSwapAmount: true,
MinBlockLock: true,
},
},
current: testAPs,
@ -596,9 +619,14 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
func (suite *PermissionsTestSuite) TestAllowedAssetParam_Allows() {
testAP := bep3types.AssetParam{
Denom: "usdx",
CoinID: 999,
SupplyLimit: sdk.NewInt(1000000000),
Denom: "usdx",
CoinID: 999,
SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))),
FixedFee: sdk.NewInt(1000),
@ -611,11 +639,11 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParam_Allows() {
newCoinidAP.CoinID = 0
newLimitAP := testAP
newLimitAP.SupplyLimit = i(1000)
newLimitAP.SupplyLimit.Limit = i(1000)
newCoinidAndLimitAP := testAP
newCoinidAndLimitAP.CoinID = 0
newCoinidAndLimitAP.SupplyLimit = i(1000)
newCoinidAndLimitAP.SupplyLimit.Limit = i(1000)
testcases := []struct {
name string

View File

@ -465,19 +465,25 @@ func (aaps AllowedAssetParams) Allows(current, incoming bep3types.AssetParams) b
return allAllowed
}
// AllowedAssetParam bep3 asset parameters that can be changed by committee
type AllowedAssetParam struct {
Denom string `json:"denom" yaml:"denom"`
CoinID bool `json:"coin_id" yaml:"coin_id"`
Limit bool `json:"limit" yaml:"limit"`
Active bool `json:"active" yaml:"active"`
Denom string `json:"denom" yaml:"denom"`
CoinID bool `json:"coin_id" yaml:"coin_id"`
Limit bool `json:"limit" yaml:"limit"`
Active bool `json:"active" yaml:"active"`
MaxSwapAmount bool `json:"max_swap_amount" yaml:"max_swap_amount"`
MinBlockLock bool `json:"min_block_lock" yaml:"min_block_lock"`
}
// Allows bep3 AssetParam parameters than can be changed by committee
func (aap AllowedAssetParam) Allows(current, incoming bep3types.AssetParam) bool {
allowed := ((aap.Denom == current.Denom) && (aap.Denom == incoming.Denom)) && // require denoms to be all equal
((current.CoinID == incoming.CoinID) || aap.CoinID) &&
(current.SupplyLimit.Equal(incoming.SupplyLimit) || aap.Limit) &&
((current.Active == incoming.Active) || aap.Active)
(current.SupplyLimit.Equals(incoming.SupplyLimit) || aap.Limit) &&
((current.Active == incoming.Active) || aap.Active) &&
((current.MaxSwapAmount.Equal(incoming.MaxSwapAmount)) || aap.MaxSwapAmount) &&
((current.MinBlockLock == incoming.MinBlockLock) || aap.MinBlockLock)
return allowed
}