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.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner}, cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
cdp.SavingsRateMacc: {supply.Minter}, cdp.SavingsRateMacc: {supply.Minter},
bep3.ModuleName: nil, bep3.ModuleName: {supply.Minter, supply.Burner},
kavadist.ModuleName: {supply.Minter}, kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner}, issuance.ModuleAccountName: {supply.Minter, supply.Burner},
} }

View File

@ -1,6 +1,8 @@
package v0_11 package v0_11
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
v0_11bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_11" 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 // 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 { func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
var assetParams v0_11bep3.AssetParams var assetParams v0_11bep3.AssetParams
var assetSupplies v0_11bep3.AssetSupplies
v0_9Params := oldGenState.Params v0_9Params := oldGenState.Params
for _, asset := range v0_9Params.SupportedAssets { for _, asset := range v0_9Params.SupportedAssets {
@ -19,28 +22,22 @@ func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
CoinID: asset.CoinID, CoinID: asset.CoinID,
DeputyAddress: v0_9Params.BnbDeputyAddress, DeputyAddress: v0_9Params.BnbDeputyAddress,
FixedFee: v0_9Params.BnbDeputyFixedFee, 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, MaxSwapAmount: v0_9Params.MaxAmount,
MinBlockLock: v0_9Params.MinBlockLock, MinBlockLock: v0_9Params.MinBlockLock,
MaxBlockLock: v0_9Params.MaxBlockLock, MaxBlockLock: v0_9Params.MaxBlockLock,
SupplyLimit: v0_11bep3.AssetSupply{ SupplyLimit: v0_11bep3.SupplyLimit{
SupplyLimit: sdk.NewCoin(asset.Denom, sdk.ZeroInt()), Limit: asset.Limit,
CurrentSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()), TimeLimited: false,
IncomingSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()), TimePeriod: time.Duration(0),
OutgoingSupply: sdk.NewCoin(asset.Denom, sdk.ZeroInt()), TimeBasedLimit: sdk.ZeroInt(),
}, },
} }
assetParams = append(assetParams, v10AssetParam) assetParams = append(assetParams, v10AssetParam)
} }
for _, supply := range oldGenState.AssetSupplies { for _, supply := range oldGenState.AssetSupplies {
for _, asset := range assetParams { newSupply := v0_11bep3.NewAssetSupply(supply.IncomingSupply, supply.OutgoingSupply, supply.CurrentSupply, sdk.NewCoin(supply.CurrentSupply.Denom, sdk.ZeroInt()), time.Duration(0))
if asset.Denom == supply.Denom { assetSupplies = append(assetSupplies, newSupply)
asset.SupplyLimit.SupplyLimit = supply.SupplyLimit
asset.SupplyLimit.CurrentSupply = supply.CurrentSupply
asset.SupplyLimit.IncomingSupply = supply.IncomingSupply
asset.SupplyLimit.OutgoingSupply = supply.OutgoingSupply
}
}
} }
var swaps v0_11bep3.AtomicSwaps var swaps v0_11bep3.AtomicSwaps
for _, oldSwap := range oldGenState.AtomicSwaps { for _, oldSwap := range oldGenState.AtomicSwaps {
@ -64,5 +61,7 @@ func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
Params: v0_11bep3.Params{ Params: v0_11bep3.Params{
AssetParams: assetParams}, 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 // BeginBlocker on every block expires outdated atomic swaps and removes closed
// swap from long term storage (default storage time of 1 week) // swap from long term storage (default storage time of 1 week)
func BeginBlocker(ctx sdk.Context, k Keeper) { func BeginBlocker(ctx sdk.Context, k Keeper) {
k.UpdateTimeBasedSupplyLimits(ctx)
k.UpdateExpiredAtomicSwaps(ctx) k.UpdateExpiredAtomicSwaps(ctx)
k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx) k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx)
} }

View File

@ -120,6 +120,7 @@ var (
DefaultMaxAmount = types.DefaultMaxAmount DefaultMaxAmount = types.DefaultMaxAmount
DefaultMinBlockLock = types.DefaultMinBlockLock DefaultMinBlockLock = types.DefaultMinBlockLock
DefaultMaxBlockLock = types.DefaultMaxBlockLock DefaultMaxBlockLock = types.DefaultMaxBlockLock
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
) )
type ( type (
@ -141,6 +142,7 @@ type (
AtomicSwaps = types.AtomicSwaps AtomicSwaps = types.AtomicSwaps
SwapStatus = types.SwapStatus SwapStatus = types.SwapStatus
SwapDirection = types.SwapDirection SwapDirection = types.SwapDirection
SupplyLimit = types.SupplyLimit
AugmentedAtomicSwap = types.AugmentedAtomicSwap AugmentedAtomicSwap = types.AugmentedAtomicSwap
AugmentedAtomicSwaps = types.AugmentedAtomicSwaps 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)) panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
} }
keeper.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
keeper.SetParams(ctx, gs.Params) keeper.SetParams(ctx, gs.Params)
for _, supply := range gs.Supplies { for _, supply := range gs.Supplies {
keeper.SetAssetSupply(ctx, supply, supply.GetDenom()) keeper.SetAssetSupply(ctx, supply, supply.GetDenom())
@ -91,17 +93,17 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
if err != nil { if err != nil {
panic(err) panic(err)
} }
if supply.CurrentSupply.Amount.GT(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)) panic(fmt.Sprintf("asset's current supply %s is over the supply limit %s", supply.CurrentSupply, limit.Limit))
} }
if supply.IncomingSupply.Amount.GT(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)) 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) { 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)) 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) { if supply.OutgoingSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's outgoing supply %s is over the supply limit %s", supply.OutgoingSupply, 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) params := k.GetParams(ctx)
swaps := k.GetAllAtomicSwaps(ctx) swaps := k.GetAllAtomicSwaps(ctx)
supplies := k.GetAllAssetSupplies(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{ bep3.AssetSupply{
IncomingSupply: c("bnb", 0), IncomingSupply: c("bnb", 0),
OutgoingSupply: 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)} return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
@ -109,7 +109,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
gs := baseGenState(suite.addrs[0]) gs := baseGenState(suite.addrs[0])
// Set up overlimit amount // Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb") 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 // Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, addrs := app.GeneratePrivKeyAddressPairs(2)
@ -124,7 +124,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
// Set up asset supply with overlimit current supply // Set up asset supply with overlimit current supply
gs.Supplies = bep3.AssetSupplies{ gs.Supplies = bep3.AssetSupplies{
bep3.AssetSupply{ 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), OutgoingSupply: c("bnb", 0),
CurrentSupply: c("bnb", 0), CurrentSupply: c("bnb", 0),
}, },
@ -139,7 +139,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
gs := baseGenState(suite.addrs[0]) gs := baseGenState(suite.addrs[0])
// Set up overlimit amount // Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb") assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb")
halfLimit := assetParam.SupplyLimit.Int64() / 2 halfLimit := assetParam.SupplyLimit.Limit.Int64() / 2
overHalfLimit := halfLimit + 1 overHalfLimit := halfLimit + 1
// Set up an atomic swap with amount equal to the currently asset supply // 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]) gs := baseGenState(suite.addrs[0])
// Set up overlimit amount // Set up overlimit amount
assetParam, _ := suite.keeper.GetAsset(suite.ctx, "bnb") 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 // Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, addrs := app.GeneratePrivKeyAddressPairs(2)
@ -187,7 +187,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
bep3.AssetSupply{ bep3.AssetSupply{
IncomingSupply: c("bnb", 0), IncomingSupply: c("bnb", 0),
OutgoingSupply: 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)} return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
@ -266,7 +266,7 @@ func (suite *GenesisTestSuite) TestGenesisState() {
name: "negative supported asset limit", name: "negative supported asset limit",
genState: func() app.GenesisState { genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0]) 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)} return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)}
}, },
expectPass: false, expectPass: false,

View File

@ -42,7 +42,12 @@ func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
bep3.AssetParam{ bep3.AssetParam{
Denom: "bnb", Denom: "bnb",
CoinID: 714, CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000), SupplyLimit: bep3.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: deputy, DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -54,7 +59,12 @@ func baseGenState(deputy sdk.AccAddress) bep3.GenesisState {
bep3.AssetParam{ bep3.AssetParam{
Denom: "inc", Denom: "inc",
CoinID: 9999, CoinID: 9999,
SupplyLimit: sdk.NewInt(100000000000), SupplyLimit: bep3.SupplyLimit{
Limit: sdk.NewInt(100000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: deputy, DeputyAddress: deputy,
FixedFee: sdk.NewInt(1000), 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()),
sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
time.Duration(0),
), ),
bep3.NewAssetSupply( bep3.NewAssetSupply(
sdk.NewCoin("inc", sdk.ZeroInt()), sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()), 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 return bep3Genesis
} }
@ -92,7 +107,7 @@ func loadSwapAndSupply(addr sdk.AccAddress, index int) (bep3.AtomicSwap, bep3.As
TestRecipientOtherChain, 1, bep3.Open, true, bep3.Incoming) TestRecipientOtherChain, 1, bep3.Open, true, bep3.Incoming)
supply := bep3.NewAssetSupply(coin, c(coin.Denom, 0), 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 return swap, supply
} }

View File

@ -1,6 +1,8 @@
package keeper package keeper
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 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 { if err != nil {
return err return err
} }
supplyLimit := sdk.NewCoin(coin.Denom, limit) supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
// Resulting current supply must be under asset's limit // Resulting current supply must be under asset's limit
if supplyLimit.IsLT(supply.CurrentSupply.Add(coin)) { if supplyLimit.IsLT(supply.CurrentSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, supply.CurrentSupply, supplyLimit) 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) supply.CurrentSupply = supply.CurrentSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom) k.SetAssetSupply(ctx, supply, coin.Denom)
return nil return nil
@ -62,11 +72,19 @@ func (k Keeper) IncrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) err
if err != nil { if err != nil {
return err return err
} }
supplyLimit := sdk.NewCoin(coin.Denom, limit) supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
if supplyLimit.IsLT(totalSupply.Add(coin)) { if supplyLimit.IsLT(totalSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, totalSupply, supplyLimit) 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) supply.IncomingSupply = supply.IncomingSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom) k.SetAssetSupply(ctx, supply, coin.Denom)
return nil return nil
@ -125,3 +143,42 @@ func (k Keeper) DecrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) err
k.SetAssetSupply(ctx, supply, coin.Denom) k.SetAssetSupply(ctx, supply, coin.Denom)
return nil 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 package keeper_test
import ( import (
"strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -37,15 +39,17 @@ func (suite *AssetTestSuite) SetupTest() {
keeper := tApp.GetBep3Keeper() keeper := tApp.GetBep3Keeper()
params := keeper.GetParams(ctx) 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) keeper.SetParams(ctx, params)
// Set asset supply with standard value for testing // Set asset supply with standard value for testing
supply := types.AssetSupply{ supply := types.NewAssetSupply(c("bnb", 5), c("bnb", 5), c("bnb", 40), c("bnb", 0), time.Duration(0))
IncomingSupply: c("bnb", 5), keeper.SetAssetSupply(ctx, supply, supply.IncomingSupply.Denom)
OutgoingSupply: c("bnb", 5),
CurrentSupply: c("bnb", 40), 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.SetAssetSupply(ctx, supply, supply.GetDenom()) keeper.SetPreviousBlockTime(ctx, ctx.BlockTime())
suite.app = tApp suite.app = tApp
suite.ctx = ctx 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() { func (suite *AssetTestSuite) TestDecrementCurrentAssetSupply() {
type args struct { type args struct {
coin sdk.Coin 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() { func (suite *AssetTestSuite) TestDecrementIncomingAssetSupply() {
type args struct { type args struct {
coin sdk.Coin 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) { func TestAssetTestSuite(t *testing.T) {
suite.Run(t, new(AssetTestSuite)) suite.Run(t, new(AssetTestSuite))
} }

View File

@ -9,7 +9,6 @@ import (
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/bep3/types" "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 { func NewBep3GenStateMulti(deputyAddress sdk.AccAddress) app.GenesisState {
bep3Genesis := types.GenesisState{ bep3Genesis := types.GenesisState{
Params: bep3.Params{ Params: types.Params{
AssetParams: types.AssetParams{ AssetParams: types.AssetParams{
types.AssetParam{ types.AssetParam{
Denom: "bnb", Denom: "bnb",
CoinID: 714, CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000), SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -49,31 +53,41 @@ func NewBep3GenStateMulti(deputyAddress sdk.AccAddress) app.GenesisState {
types.AssetParam{ types.AssetParam{
Denom: "inc", Denom: "inc",
CoinID: 9999, CoinID: 9999,
SupplyLimit: sdk.NewInt(100), SupplyLimit: types.SupplyLimit{
Limit: sdk.NewInt(100000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false, Active: false,
DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
MinSwapAmount: sdk.OneInt(), MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdk.NewInt(1000000000000), MaxSwapAmount: sdk.NewInt(100000000000),
MinBlockLock: types.DefaultMinBlockLock, MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock, MaxBlockLock: types.DefaultMaxBlockLock,
}, },
}, },
}, },
Supplies: bep3.AssetSupplies{ Supplies: types.AssetSupplies{
bep3.NewAssetSupply( types.NewAssetSupply(
sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()), 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()),
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 { 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 { 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 ( import (
"fmt" "fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix" "github.com/cosmos/cosmos-sdk/store/prefix"
@ -222,3 +223,20 @@ func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSuppli
}) })
return 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() { func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
denom := "bnb" denom := "bnb"
// Put asset supply in store // 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) suite.keeper.SetAssetSupply(suite.ctx, assetSupply, denom)
// Check asset in store // Check asset in store
@ -327,9 +327,9 @@ func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
func (suite *KeeperTestSuite) TestGetAllAssetSupplies() { func (suite *KeeperTestSuite) TestGetAllAssetSupplies() {
// Put asset supply in store // 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") 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") suite.keeper.SetAssetSupply(suite.ctx, assetSupply, "inc")
supplies := suite.keeper.GetAllAssetSupplies(suite.ctx) 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 // 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) asset, err := k.GetAsset(ctx, denom)
if err != nil { if err != nil {
return sdk.Int{}, err return types.SupplyLimit{}, err
} }
return asset.SupplyLimit, nil return asset.SupplyLimit, nil
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -102,7 +103,7 @@ func (suite *QuerierTestSuite) TestQueryAssetSupply() {
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &supply)) suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &supply))
expectedSupply := types.NewAssetSupply(c(denom, 1000), 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) 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]) err = k.IncrementIncomingAssetSupply(ctx, amount[0])
case types.Outgoing: 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 { if heightSpan < asset.MinBlockLock || heightSpan > asset.MaxBlockLock {
return sdkerrors.Wrapf(types.ErrInvalidHeightSpan, "height span %d outside range [%d, %d]", heightSpan, asset.MinBlockLock, asset.MaxBlockLock) return sdkerrors.Wrapf(types.ErrInvalidHeightSpan, "height span %d outside range [%d, %d]", heightSpan, asset.MinBlockLock, asset.MaxBlockLock)
} }
@ -88,15 +88,14 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times
return sdkerrors.Wrap(types.ErrInsufficientAmount, amount[0].String()) return sdkerrors.Wrap(types.ErrInsufficientAmount, amount[0].String())
} }
err = k.IncrementOutgoingAssetSupply(ctx, amount[0]) err = k.IncrementOutgoingAssetSupply(ctx, amount[0])
default:
err = fmt.Errorf("invalid swap direction: %s", direction.String())
}
if err != nil { if err != nil {
return err return err
} }
// Transfer coins to module - only needed for outgoing swaps
// Transfer coins to module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount) err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount)
default:
err = fmt.Errorf("invalid swap direction: %s", direction.String())
}
if err != nil { if err != nil {
return err return err
} }
@ -161,6 +160,16 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b
if err != nil { if err != nil {
return err 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: case types.Outgoing:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil { if err != nil {
@ -170,15 +179,14 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b
if err != nil { if err != nil {
return err return err
} }
default: // outgoing case - coins should be burned
return fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String()) err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, atomicSwap.Amount)
}
// Send intended recipient coins
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount)
if err != nil { if err != nil {
return err return err
} }
default:
return fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
}
// Complete swap // Complete swap
atomicSwap.Status = types.Completed atomicSwap.Status = types.Completed
@ -221,6 +229,11 @@ func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []
err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0]) err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
case types.Outgoing: case types.Outgoing:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) 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: default:
err = fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String()) 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 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 // Complete swap
atomicSwap.Status = types.Completed atomicSwap.Status = types.Completed
atomicSwap.ClosedBlock = ctx.BlockHeight() atomicSwap.ClosedBlock = ctx.BlockHeight()

View File

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

View File

@ -1,14 +1,65 @@
package v0_11 package v0_11
import ( import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" 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" 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 // GenesisState - all bep3 state that must be provided at genesis
type GenesisState struct { type GenesisState struct {
Params Params `json:"params" yaml:"params"` Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"` 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 // Params governance parameters for the bep3 module
@ -16,11 +67,83 @@ type Params struct {
AssetParams AssetParams `json:"asset_params" yaml:"asset_params"` 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 // AssetParam parameters that must be specified for each bep3 asset
type AssetParam struct { type AssetParam struct {
Denom string `json:"denom" yaml:"denom"` // name of the asset 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 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 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 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 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,6 +153,14 @@ type AssetParam struct {
MaxBlockLock uint64 `json:"max_block_lock" yaml:"max_block_lock"` // Maximum swap block lock 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 // AssetParams array of AssetParam
type AssetParams []AssetParam type AssetParams []AssetParam
@ -38,9 +169,77 @@ type AssetSupply struct {
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"` IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"` OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"` CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
SupplyLimit sdk.Coin `json:"supply_limit" yaml:"supply_limit"` 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 // AtomicSwap contains the information for an atomic swap
type AtomicSwap struct { type AtomicSwap struct {
Amount sdk.Coins `json:"amount" yaml:"amount"` Amount sdk.Coins `json:"amount" yaml:"amount"`
@ -57,6 +256,73 @@ type AtomicSwap struct {
Direction SwapDirection `json:"direction" yaml:"direction"` 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 // AtomicSwaps is a slice of AtomicSwap
type AtomicSwaps []AtomicSwap type AtomicSwaps []AtomicSwap
@ -71,6 +337,50 @@ const (
Expired SwapStatus = 0x03 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 // SwapDirection is the direction of an AtomicSwap
type SwapDirection byte type SwapDirection byte
@ -79,3 +389,52 @@ const (
Incoming SwapDirection = 0x01 Incoming SwapDirection = 0x01
Outgoing SwapDirection = 0x02 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 package v0_9
import ( import (
"encoding/json"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
tmbytes "github.com/tendermint/tendermint/libs/bytes" tmbytes "github.com/tendermint/tendermint/libs/bytes"
) )
@ -141,6 +143,50 @@ const (
Expired SwapStatus = 0x03 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 // SwapDirection is the direction of an AtomicSwap
type SwapDirection byte type SwapDirection byte
@ -149,3 +195,52 @@ const (
Incoming SwapDirection = 0x01 Incoming SwapDirection = 0x01
Outgoing SwapDirection = 0x02 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 ( import (
"bytes" "bytes"
"fmt" "fmt"
"time"
tmbytes "github.com/tendermint/tendermint/libs/bytes" tmbytes "github.com/tendermint/tendermint/libs/bytes"
"github.com/tendermint/tendermint/libs/kv" "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(kvA.Value, &supplyA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &supplyB) cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &supplyB)
return fmt.Sprintf("%s\n%s", supplyA, 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: default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1])) panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))

View File

@ -3,6 +3,7 @@ package simulation
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -22,12 +23,13 @@ func makeTestCodec() (cdc *codec.Codec) {
return return
} }
func TestDecodeDistributionStore(t *testing.T) { func TestDecodeBep3Store(t *testing.T) {
cdc := makeTestCodec() cdc := makeTestCodec()
prevBlockTime := time.Now().UTC()
oneCoin := sdk.NewCoin("coin", sdk.OneInt()) 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) 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}) bz := tmbytes.HexBytes([]byte{1, 2})
kvPairs := kv.Pairs{ 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.AssetSupplyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(supply)},
kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz}, kv.Pair{Key: types.AtomicSwapByBlockPrefix, Value: bz},
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}}, 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)}, {"AssetSupply", fmt.Sprintf("%v\n%v", supply, supply)},
{"AtomicSwapByBlock", fmt.Sprintf("%s\n%s", bz, bz)}, {"AtomicSwapByBlock", fmt.Sprintf("%s\n%s", bz, bz)},
{"AtomicSwapLongtermStorage", fmt.Sprintf("%s\n%s", bz, bz)}, {"AtomicSwapLongtermStorage", fmt.Sprintf("%s\n%s", bz, bz)},
{"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
{"other", ""}, {"other", ""},
} }
for i, tt := range tests { for i, tt := range tests {

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"strings" "strings"
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" 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 { func GenAssetSupply(r *rand.Rand, denom string) types.AssetSupply {
return types.NewAssetSupply( 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())) sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()), time.Duration(0))
} }
// GenMinBlockLock randomized MinBlockLock // GenMinBlockLock randomized MinBlockLock
@ -97,10 +98,22 @@ func genSupportedAsset(r *rand.Rand, denom string) types.AssetParam {
minSwapAmount := GenMinSwapAmount(r) minSwapAmount := GenMinSwapAmount(r)
minBlockLock := GenMinBlockLock(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{ return types.AssetParam{
Denom: denom, Denom: denom,
CoinID: int(coinID.Int64()), CoinID: int(coinID.Int64()),
SupplyLimit: limit, SupplyLimit: types.SupplyLimit{
Limit: limit,
TimeLimited: timeLimited,
TimePeriod: time.Hour * 24,
TimeBasedLimit: timeBasedLimit},
Active: true, Active: true,
DeputyAddress: GenRandBnbDeputy(r).Address, DeputyAddress: GenRandBnbDeputy(r).Address,
FixedFee: GenRandFixedFee(r), FixedFee: GenRandFixedFee(r),
@ -146,6 +159,7 @@ func loadRandomBep3GenState(simState *module.SimulationState) types.GenesisState
AssetParams: supportedAssets, AssetParams: supportedAssets,
}, },
Supplies: supplies, Supplies: supplies,
PreviousBlockTime: types.DefaultPreviousBlockTime,
} }
return bep3Genesis return bep3Genesis
@ -161,7 +175,7 @@ func loadAuthGenState(simState *module.SimulationState, bep3Genesis types.Genesi
if !found { if !found {
panic("deputy address not found in available accounts") 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 { if err := deputy.SetCoins(deputy.GetCoins().Add(assetCoin...)); err != nil {
panic(err) panic(err)
} }

View File

@ -127,15 +127,27 @@ func SimulateMsgCreateAtomicSwap(ak types.AccountKeeper, k keeper.Keeper) simula
// Get maximum valid amount // Get maximum valid amount
maximumAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(asset.Denom) maximumAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(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) assetSupply, foundAssetSupply := k.GetAssetSupply(ctx, asset.Denom)
if !foundAssetSupply { if !foundAssetSupply {
return noOpMsg, nil, fmt.Errorf("no asset supply found for %s", asset.Denom) 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) {
if maximumAmount.GT(assetSupply.CurrentSupply.Amount.Sub(assetSupply.OutgoingSupply.Amount)) { if maximumAmount.GT(assetSupply.CurrentSupply.Amount.Sub(assetSupply.OutgoingSupply.Amount)) {
maximumAmount = 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 // 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)))) amount := maximumAmount.Quo(sdk.NewInt(int64(simulation.RandIntBetween(r, 50, 1000))))
minAmountPlusFee := asset.MinSwapAmount.Add(asset.FixedFee) minAmountPlusFee := asset.MinSwapAmount.Add(asset.FixedFee)
if amount.LT(minAmountPlusFee) { 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)) coins := sdk.NewCoins(sdk.NewCoin(asset.Denom, amount))
@ -242,7 +254,7 @@ func operationClaimAtomicSwap(ak types.AccountKeeper, k keeper.Keeper, swapID []
if !found { 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 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 return simulation.NoOpMsg(types.ModuleName), nil, nil
} }

View File

@ -43,4 +43,6 @@ var (
ErrInvalidAmount = sdkerrors.Register(ModuleName, 18, "amount is outside acceptable range") ErrInvalidAmount = sdkerrors.Register(ModuleName, 18, "amount is outside acceptable range")
// ErrInvalidSwapAccount error for when a swap involves an invalid account // ErrInvalidSwapAccount error for when a swap involves an invalid account
ErrInvalidSwapAccount = sdkerrors.Register(ModuleName, 19, "atomic swap has 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 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 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) // AccountKeeper defines the expected account keeper (noalias)

View File

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

View File

@ -2,6 +2,7 @@ package types_test
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -24,13 +25,15 @@ func (suite *GenesisTestSuite) SetupTest() {
coin := sdk.NewCoin("kava", sdk.OneInt()) coin := sdk.NewCoin("kava", sdk.OneInt())
suite.swaps = atomicSwaps(10) 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} suite.supplies = types.AssetSupplies{supply}
} }
func (suite *GenesisTestSuite) TestValidate() { func (suite *GenesisTestSuite) TestValidate() {
type args struct { type args struct {
swaps types.AtomicSwaps swaps types.AtomicSwaps
supplies types.AssetSupplies
previousBlockTime time.Time
} }
testCases := []struct { testCases := []struct {
name string name string
@ -41,6 +44,7 @@ func (suite *GenesisTestSuite) TestValidate() {
"default", "default",
args{ args{
swaps: types.AtomicSwaps{}, swaps: types.AtomicSwaps{},
previousBlockTime: types.DefaultPreviousBlockTime,
}, },
true, true,
}, },
@ -48,13 +52,33 @@ func (suite *GenesisTestSuite) TestValidate() {
"with swaps", "with swaps",
args{ args{
swaps: suite.swaps, swaps: suite.swaps,
previousBlockTime: types.DefaultPreviousBlockTime,
}, },
true, 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", "duplicate swaps",
args{ args{
swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]}, swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]},
previousBlockTime: types.DefaultPreviousBlockTime,
}, },
false, false,
}, },
@ -62,9 +86,17 @@ func (suite *GenesisTestSuite) TestValidate() {
"invalid swap", "invalid swap",
args{ 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, false,
}, },
{
"blocktime not set",
args{
swaps: types.AtomicSwaps{},
},
true,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -73,7 +105,7 @@ func (suite *GenesisTestSuite) TestValidate() {
if tc.name == "default" { if tc.name == "default" {
gs = types.DefaultGenesisState() gs = types.DefaultGenesisState()
} else { } 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() err := gs.Validate()

View File

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

View File

@ -2,9 +2,16 @@ package types
import ( import (
"fmt" "fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
bech32MainPrefix = "kava"
) )
// Parameter keys // Parameter keys
@ -16,6 +23,7 @@ var (
DefaultMaxAmount sdk.Int = sdk.NewInt(1000000000000) // 10,000 BNB DefaultMaxAmount sdk.Int = sdk.NewInt(1000000000000) // 10,000 BNB
DefaultMinBlockLock uint64 = 220 DefaultMinBlockLock uint64 = 220
DefaultMaxBlockLock uint64 = 270 DefaultMaxBlockLock uint64 = 270
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
) )
// Params governance parameters for bep3 module // Params governance parameters for bep3 module
@ -47,7 +55,7 @@ func DefaultParams() Params {
type AssetParam struct { type AssetParam struct {
Denom string `json:"denom" yaml:"denom"` // name of the asset 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 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 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 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 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 // NewAssetParam returns a new AssetParam
func NewAssetParam( 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, deputyAddr sdk.AccAddress, fixedFee sdk.Int, minSwapAmount sdk.Int,
maxSwapAmount sdk.Int, minBlockLock uint64, maxBlockLock uint64, maxSwapAmount sdk.Int, minBlockLock uint64, maxBlockLock uint64,
) AssetParam { ) AssetParam {
@ -106,6 +114,28 @@ func (aps AssetParams) String() string {
return out 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 // ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable { func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{}) return params.NewKeyTable().RegisterParamSet(&Params{})
@ -134,20 +164,28 @@ func validateAssetParams(i interface{}) error {
coinDenoms := make(map[string]bool) coinDenoms := make(map[string]bool)
for _, asset := range assetParams { for _, asset := range assetParams {
if err := sdk.ValidateDenom(asset.Denom); err != nil { 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 { 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() { if asset.SupplyLimit.Limit.IsNegative() {
return fmt.Errorf("asset %s has invalid (negative) supply limit: %s", asset.Denom, asset.SupplyLimit) 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] _, found := coinDenoms[asset.Denom]
if found { 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 coinDenoms[asset.Denom] = true
@ -169,11 +207,11 @@ func validateAssetParams(i interface{}) error {
} }
if !asset.MinSwapAmount.IsPositive() { 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() { 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) { if asset.MinSwapAmount.GT(asset.MaxSwapAmount) {

View File

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

View File

@ -2,6 +2,7 @@ package types
import ( import (
"fmt" "fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -12,14 +13,18 @@ type AssetSupply struct {
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"` IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"` OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_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 // 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{ return AssetSupply{
IncomingSupply: incomingSupply, IncomingSupply: incomingSupply,
OutgoingSupply: outgoingSupply, OutgoingSupply: outgoingSupply,
CurrentSupply: currentSupply, CurrentSupply: currentSupply,
TimeLimitedCurrentSupply: timeLimitedSupply,
TimeElapsed: timeElapsed,
} }
} }
@ -34,19 +39,28 @@ func (a AssetSupply) Validate() error {
if !a.CurrentSupply.IsValid() { if !a.CurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "current supply %s", a.CurrentSupply) 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 denom := a.CurrentSupply.Denom
if (a.IncomingSupply.Denom != denom) || if (a.IncomingSupply.Denom != denom) ||
(a.OutgoingSupply.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.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 return nil
} }
// Equal returns if two asset supplies are equal // Equal returns if two asset supplies are equal
func (a AssetSupply) Equal(b AssetSupply) bool { func (a AssetSupply) Equal(b AssetSupply) bool {
if a.GetDenom() != b.GetDenom() {
return false
}
return (a.IncomingSupply.IsEqual(b.IncomingSupply) && return (a.IncomingSupply.IsEqual(b.IncomingSupply) &&
a.CurrentSupply.IsEqual(b.CurrentSupply) && 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 // String implements stringer
@ -56,8 +70,10 @@ func (a AssetSupply) String() string {
Incoming supply: %s Incoming supply: %s
Outgoing supply: %s Outgoing supply: %s
Current 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 // GetDenom getter method for the denom of the asset supply

View File

@ -2,6 +2,7 @@ package types
import ( import (
"testing" "testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -17,7 +18,7 @@ func TestAssetSupplyValidate(t *testing.T) {
}{ }{
{ {
msg: "valid asset", msg: "valid asset",
asset: NewAssetSupply(coin, coin, coin), asset: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
expPass: true, expPass: true,
}, },
{ {
@ -42,6 +43,27 @@ func TestAssetSupplyValidate(t *testing.T) {
}, },
false, 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 { 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 ( import (
"testing" "testing"
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
@ -85,7 +86,12 @@ func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
bep3types.AssetParam{ bep3types.AssetParam{
Denom: "bnb", Denom: "bnb",
CoinID: 714, CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: testDeputy, DeputyAddress: testDeputy,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -97,7 +103,12 @@ func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
bep3types.AssetParam{ bep3types.AssetParam{
Denom: "inc", Denom: "inc",
CoinID: 9999, CoinID: 9999,
SupplyLimit: sdk.NewInt(100), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(100000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false, Active: false,
DeputyAddress: testDeputy, DeputyAddress: testDeputy,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),

View File

@ -5,6 +5,7 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation" "github.com/cosmos/cosmos-sdk/x/simulation"
@ -58,7 +59,7 @@ func RandomizedGenState(simState *module.SimulationState) {
[]types.Proposal{}, []types.Proposal{},
[]types.Vote{}, []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) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState)
} }

View File

@ -1,6 +1,8 @@
package types package types
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
bep3types "github.com/kava-labs/kava/x/bep3/types" bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types" cdptypes "github.com/kava-labs/kava/x/cdp/types"
@ -161,7 +163,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
bep3types.AssetParam{ bep3types.AssetParam{
Denom: "btc", Denom: "btc",
CoinID: 0, CoinID: 0,
SupplyLimit: sdk.NewInt(100), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false, Active: false,
DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -173,7 +180,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
bep3types.AssetParam{ bep3types.AssetParam{
Denom: "bnb", Denom: "bnb",
CoinID: 714, CoinID: 714,
SupplyLimit: sdk.NewInt(350000000000000), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -185,7 +197,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
bep3types.AssetParam{ bep3types.AssetParam{
Denom: "xrp", Denom: "xrp",
CoinID: 414, CoinID: 414,
SupplyLimit: sdk.NewInt(350000000000000), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -200,10 +217,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
updatedTestAPs[1] = testAPs[0] updatedTestAPs[1] = testAPs[0]
updatedTestAPs[2] = testAPs[2] updatedTestAPs[2] = testAPs[2]
updatedTestAPs[0].SupplyLimit = i(1000) // btc updatedTestAPs[0].SupplyLimit.Limit = i(1000) // btc
updatedTestAPs[1].Active = false // bnb updatedTestAPs[1].Active = false // bnb
updatedTestAPs[2].SupplyLimit = i(1000) // xrp updatedTestAPs[2].SupplyLimit.Limit = i(1000) // xrp
updatedTestAPs[2].Active = false // xrp updatedTestAPs[2].Active = false // xrp
updatedTestAPs[2].MinBlockLock = uint64(210) // xrp
updatedTestAPs[2].MaxSwapAmount = sdk.NewInt(10000000000000)
testcases := []struct { testcases := []struct {
name string name string
@ -228,6 +247,8 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
CoinID: true, CoinID: true,
Limit: true, Limit: true,
Active: true, Active: true,
MaxSwapAmount: true,
MinBlockLock: true,
}, },
}, },
current: testAPs[:2], current: testAPs[:2],
@ -268,6 +289,8 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
Denom: "xrp", Denom: "xrp",
Limit: true, Limit: true,
Active: true, Active: true,
MaxSwapAmount: true,
MinBlockLock: true,
}, },
}, },
current: testAPs, current: testAPs,
@ -598,7 +621,12 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParam_Allows() {
testAP := bep3types.AssetParam{ testAP := bep3types.AssetParam{
Denom: "usdx", Denom: "usdx",
CoinID: 999, CoinID: 999,
SupplyLimit: sdk.NewInt(1000000000), SupplyLimit: bep3types.SupplyLimit{
Limit: sdk.NewInt(350000000000000),
TimeLimited: true,
TimeBasedLimit: sdk.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: true, Active: true,
DeputyAddress: sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))), DeputyAddress: sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))),
FixedFee: sdk.NewInt(1000), FixedFee: sdk.NewInt(1000),
@ -611,11 +639,11 @@ func (suite *PermissionsTestSuite) TestAllowedAssetParam_Allows() {
newCoinidAP.CoinID = 0 newCoinidAP.CoinID = 0
newLimitAP := testAP newLimitAP := testAP
newLimitAP.SupplyLimit = i(1000) newLimitAP.SupplyLimit.Limit = i(1000)
newCoinidAndLimitAP := testAP newCoinidAndLimitAP := testAP
newCoinidAndLimitAP.CoinID = 0 newCoinidAndLimitAP.CoinID = 0
newCoinidAndLimitAP.SupplyLimit = i(1000) newCoinidAndLimitAP.SupplyLimit.Limit = i(1000)
testcases := []struct { testcases := []struct {
name string name string

View File

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