Merge pull request #375 from Kava-Labs/develop

feat: merge cdp modules to master
This commit is contained in:
Kevin Davis 2020-02-20 16:55:06 -05:00 committed by GitHub
commit e3b1f7e24d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
177 changed files with 19534 additions and 26 deletions

View File

@ -96,6 +96,13 @@ jobs:
target: start-remote-sims
description: "Test multi-seed simulation (long)"
broken-link-check:
executor: golang
steps:
- make:
target: link-check
description: "Check url links are not broken"
workflows:
version: 2
@ -116,4 +123,7 @@ workflows:
# These filters ensure that the long sim only runs during release
filters:
branches:
only: "master"
only: "master"
- broken-link-check:
requires:
- setup-dependencies

5
.gitignore vendored
View File

@ -8,9 +8,12 @@
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
# Output of the go coverage tool
*.out
# Exclude build files
vendor
build
# IDE files
*.vscode

View File

@ -94,6 +94,15 @@ go.sum: go.mod
clean:
rm -rf build/
########################################
### Linting
# Check url links in the repo are not broken.
# This tool checks local markdown links as well.
# Set to exclude riot links as they trigger false positives
link-check:
@go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*"
########################################
### Testing

View File

@ -23,19 +23,22 @@
</div>
Reference implementation of Kava, a blockchain for cross-chain DeFi. Built using the [cosmos-sdk](https://github.com/cosmos/cosmos-sdk).
## Quick Start
```sh
make install
```
Reference implementation of Kava, a blockchain for cross-chain DeFi. Built using the [comsos-sdk](https://github.com/cosmos/cosmos-sdk).
## Mainnet
Note, the current recommended version of the software for mainnet is v0.3.2.
Note, the current recommended version of the software for mainnet is v0.3.2. The master branch of this repository contains considerable development work since the last mainnet release and is __not__ runnable on mainnet.
To join the latest testnet, head over to the [testnet repo](https://github.com/Kava-Labs/kava-testnets).
### Installation
```sh
git checkout v0.3.2
make install
```
## Testnet
The recommended version of the software for kava-testnet-4000 is v0.4.1. For further information on joining the testnet, head over to the [testnet repo](https://github.com/Kava-Labs/kava-testnets).
## License

View File

@ -4,6 +4,9 @@ import (
"io"
"os"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
abci "github.com/tendermint/tendermint/abci/types"
@ -56,6 +59,9 @@ var (
crisis.AppModuleBasic{},
slashing.AppModuleBasic{},
supply.AppModuleBasic{},
auction.AppModuleBasic{},
cdp.AppModuleBasic{},
pricefeed.AppModuleBasic{},
)
// module account permissions
@ -67,6 +73,9 @@ var (
staking.NotBondedPoolName: {supply.Burner, supply.Staking},
gov.ModuleName: {supply.Burner},
validatorvesting.ModuleName: {supply.Burner},
auction.ModuleName: nil,
cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
}
)
@ -82,17 +91,20 @@ type App struct {
tkeys map[string]*sdk.TransientStoreKey
// keepers from all the modules
accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
supplyKeeper supply.Keeper
stakingKeeper staking.Keeper
slashingKeeper slashing.Keeper
mintKeeper mint.Keeper
distrKeeper distr.Keeper
govKeeper gov.Keeper
crisisKeeper crisis.Keeper
paramsKeeper params.Keeper
vvKeeper validatorvesting.Keeper
accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
supplyKeeper supply.Keeper
stakingKeeper staking.Keeper
slashingKeeper slashing.Keeper
mintKeeper mint.Keeper
distrKeeper distr.Keeper
govKeeper gov.Keeper
crisisKeeper crisis.Keeper
paramsKeeper params.Keeper
vvKeeper validatorvesting.Keeper
auctionKeeper auction.Keeper
cdpKeeper cdp.Keeper
pricefeedKeeper pricefeed.Keeper
// the module manager
mm *module.Manager
@ -116,6 +128,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
bam.MainStoreKey, auth.StoreKey, staking.StoreKey,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, validatorvesting.StoreKey,
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -137,6 +150,9 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
slashingSubspace := app.paramsKeeper.Subspace(slashing.DefaultParamspace)
govSubspace := app.paramsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable())
crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace)
auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace)
cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace)
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -208,6 +224,25 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.bankKeeper,
app.supplyKeeper,
&stakingKeeper)
app.pricefeedKeeper = pricefeed.NewKeeper(
app.cdc,
keys[pricefeed.StoreKey],
pricefeedSubspace,
pricefeed.DefaultCodespace)
// NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType)
app.auctionKeeper = auction.NewKeeper(
app.cdc,
keys[auction.StoreKey],
app.supplyKeeper,
auctionSubspace)
app.cdpKeeper = cdp.NewKeeper(
app.cdc,
keys[cdp.StoreKey],
cdpSubspace,
app.pricefeedKeeper,
app.auctionKeeper,
app.supplyKeeper,
cdp.DefaultCodespace)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -228,14 +263,17 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
auction.NewAppModule(app.auctionKeeper, app.supplyKeeper),
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
pricefeed.NewAppModule(app.pricefeedKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant.
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName)
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, cdp.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName, auction.ModuleName)
// Note: genutils must occur after staking so that pools are properly
// initialized with tokens from genesis accounts.
@ -246,6 +284,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, // TODO is this order ok?
)
app.mm.RegisterInvariants(&app.crisisKeeper)

135
app/test_common.go Normal file
View File

@ -0,0 +1,135 @@
package app
import (
"math/rand"
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/libs/log"
tmdb "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/crisis"
"github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/mint"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
// TestApp is a simple wrapper around an App. It exposes internal keepers for use in integration tests.
// This file also contains test helpers. Ideally they would be in separate package.
// Basic Usage:
// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution.
// Advanced Usage:
// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)).
// However this leads to a lot of duplicated logic similar to InitGenesis methods.
// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state.
// and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state
// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below.
type TestApp struct {
App
}
func NewTestApp() TestApp {
db := tmdb.NewMemDB()
app := NewApp(log.NewNopLogger(), db, nil, true, 0)
return TestApp{App: *app}
}
func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper }
func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper }
func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper }
func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper }
func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper }
func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper }
func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper }
func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper }
func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper }
func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper }
func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper }
func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper }
func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper }
func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
// Create a default genesis state and overwrite with provided values
genesisState := NewDefaultGenesisState()
for _, state := range genesisStates {
for k, v := range state {
genesisState[k] = v
}
}
// Initialize the chain
stateBytes, err := codec.MarshalJSONIndent(tApp.cdc, genesisState)
if err != nil {
panic(err)
}
tApp.InitChain(
abci.RequestInitChain{
Validators: []abci.ValidatorUpdate{},
AppStateBytes: stateBytes,
},
)
tApp.Commit()
tApp.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: tApp.LastBlockHeight() + 1}})
return tApp
}
func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) {
acc := tApp.GetAccountKeeper().GetAccount(ctx, owner)
require.NotNilf(t, acc, "account with address '%s' doesn't exist", owner)
require.Equal(t, expectedCoins, acc.GetCoins())
}
// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map.
func NewAuthGenState(addresses []sdk.AccAddress, coins []sdk.Coins) GenesisState {
// Create GenAccounts
accounts := authexported.GenesisAccounts{}
for i := range addresses {
accounts = append(accounts, auth.NewBaseAccount(addresses[i], coins[i], nil, 0, 0))
}
// Create the auth genesis state
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}
// GeneratePrivKeyAddressPairsFromRand generates (deterministically) a total of n private keys and addresses.
// TODO only generate secp256 keys?
func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.AccAddress) {
r := rand.New(rand.NewSource(12345)) // make the generation deterministic
keys = make([]crypto.PrivKey, n)
addrs = make([]sdk.AccAddress, n)
for i := 0; i < n; i++ {
secret := make([]byte, 32)
_, err := r.Read(secret)
if err != nil {
panic("Could not read randomness")
}
if r.Int63()%2 == 0 {
keys[i] = secp256k1.GenPrivKeySecp256k1(secret)
} else {
keys[i] = ed25519.GenPrivKeyFromSecret(secret)
}
addrs[i] = sdk.AccAddress(keys[i].PubKey().Address())
}
return
}

4
contrib/README.md Normal file
View File

@ -0,0 +1,4 @@
# Contrib
Resources and examples for running and interacting with the kava blockchain.

View File

@ -16,5 +16,3 @@
<string>kava</string>
</dict>
</plist>
QuantaFrontier.com

View File

@ -0,0 +1,62 @@
# Testnet-4000
Resources and examples for running and interacting with kava-testnet-4000
## Rest server requests
### Setup
Before making a request, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request:
```bash
kvcli q auth account $(kvcli keys show accB -a)
```
If testing locally, start the Kava rest server:
```bash
kvcli rest-server
```
Now we'll create an unsigned request, sign it, and broadcast it to the Kava blockchain via the rest server. Note that if you're using the mainnet or testnet, the host IP address will need to be updated to point at an active rest server instead of http://127.0.0.1.
### Create CDP example request
Format the base request in create-cdp.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the CDP creation request's params 'owner', 'collateral', and 'principal'. An example formatted request can be found in `example-create-cdp.json`.
```bash
# Create an unsigned request
curl -H "Content-Type: application/json" -X PUT -d @./contrib/requests/create-cdp.json http://127.0.0.1:1317/cdp | jq > ./contrib/requests/create-cdp-unsigned.json
# Sign the request
kvcli tx sign ./contrib/requests/create-cdp-unsigned.json --from accB --offline --chain-id testing --sequence 1 --account-number 2 | jq > ./contrib/requests/broadcast-create-cdp.json
# Broadcast the request
kvcli tx broadcast ./contrib/requests/broadcast-create-cdp.json
```
Congratulations, you've just created a CDP on Kava using the rest server!
### Post market price example request
Note that only market oracles can post prices, other senders will have their transactions rejected by Kava.
Format the base request in post-price.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the post price request's params 'from', 'market_id', 'price', and 'expiry'. An example formatted request can be found in `example-post-price.json`.
```bash
# Create an unsigned request
curl -H "Content-Type: application/json" -X PUT -d @./contrib/requests/post-price.json http://127.0.0.1:1317/pricefeed/postprice | jq > ./contrib/requests/post-price-unsigned.json
# Sign the request
kvcli tx sign ./contrib/requests/post-price-unsigned.json --from validator --offline --chain-id testing --sequence 96 --account-number 0 | jq > ./contrib/requests/broadcast-post-price.json
# Broadcast the request
kvcli tx broadcast ./contrib/requests/broadcast-post-price.json
```
Congratulations, you've just posted a current market price on Kava using the rest server!
## Governance proposals
Example governance proposals are located in `/proposal_examples`.

View File

@ -0,0 +1,360 @@
{
"genesis_time": "2020-01-22T22:39:50.083273Z",
"chain_id": "testing",
"consensus_params": {
"block": {
"max_bytes": "200000",
"max_gas": "2000000",
"time_iota_ms": "1000"
},
"evidence": {
"max_age": "1000000"
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"app_hash": "",
"app_state": {
"slashing": {
"missed_blocks": {},
"params": {
"downtime_jail_duration": "600000000000",
"max_evidence_age": "3600000000000",
"min_signed_per_window": "0.010000000000000000",
"signed_blocks_window": "1000",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.000100000000000000"
},
"signing_infos": {}
},
"params": null,
"bank": {
"send_enabled": true
},
"crisis": {
"constant_fee": {
"amount": "1333000000",
"denom": "ukava"
}
},
"cdp": {
"cdps": [],
"debt_denom": "debt",
"deposits": [],
"gov_denom": "ukava",
"params": {
"circuit_breaker": false,
"collateral_params": [
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 0,
"stability_fee": "1.0000000007829977"
},
{
"auction_size": "10000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "10000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.1",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
],
"debt_auction_threshold": "1000000000",
"debt_params": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"denom": "usdx",
"reference_asset": "usd"
}
],
"global_debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"surplus_auction_threshold": "1000000000"
},
"previous_block_time": "1970-01-01T00:00:00Z",
"starting_cdp_id": "1"
},
"auction": {
"auctions": [],
"next_auction_id": "0",
"params": {
"bid_duration": "150000000000",
"max_auction_duration": "400000000000"
}
},
"genutil": {
"gentxs": [
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgCreateValidator",
"value": {
"description": {
"moniker": "validator",
"identity": "",
"website": "",
"security_contact": "",
"details": ""
},
"commission": {
"rate": "0.100000000000000000",
"max_rate": "0.200000000000000000",
"max_change_rate": "0.010000000000000000"
},
"min_self_delegation": "1",
"delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt",
"value": {
"denom": "ukava",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw=="
}
],
"memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656"
}
}
]
},
"supply": {
"supply": []
},
"validatorvesting": {
"previous_block_time": "1970-01-01T00:00:00Z"
},
"mint": {
"minter": {
"annual_provisions": "0.000000000000000000",
"inflation": "0.020000000000000000"
},
"params": {
"blocks_per_year": "6311520",
"goal_bonded": "0.670000000000000000",
"inflation_max": "0.130000000000000000",
"inflation_min": "0.010000000000000000",
"inflation_rate_change": "0.130000000000000000",
"mint_denom": "ukava"
}
},
"distribution": {
"base_proposer_reward": "0.010000000000000000",
"bonus_proposer_reward": "0.040000000000000000",
"community_tax": "0.000000000000000000",
"delegator_starting_infos": [],
"delegator_withdraw_infos": [],
"fee_pool": {
"community_pool": []
},
"outstanding_rewards": [],
"previous_proposer": "",
"validator_accumulated_commissions": [],
"validator_current_rewards": [],
"validator_historical_rewards": [],
"validator_slash_events": [],
"withdraw_addr_enabled": true
},
"auth": {
"accounts": [
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
}
],
"params": {
"max_memo_characters": "256",
"sig_verify_cost_ed25519": "590",
"sig_verify_cost_secp256k1": "1000",
"tx_sig_limit": "7",
"tx_size_cost_per_byte": "10"
}
},
"staking": {
"delegations": null,
"exported": false,
"last_total_power": "0",
"last_validator_powers": null,
"params": {
"bond_denom": "ukava",
"max_entries": 7,
"max_validators": 100,
"unbonding_time": "3600000000000"
},
"redelegations": null,
"unbonding_delegations": null,
"validators": null
},
"pricefeed": {
"params": {
"markets": [
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp:usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "btc",
"market_id": "btc:usd",
"oracles": [],
"quote_asset": "usd"
}
]
},
"posted_prices": [
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "btc:usd",
"oracle_address": "",
"price": "8700.0"
},
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "xrp:usd",
"oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"price": "0.25"
}
]
},
"gov": {
"deposit_params": {
"max_deposit_period": "300000000000",
"min_deposit": [
{
"amount": "1000000",
"denom": "ukava"
}
]
},
"deposits": null,
"proposals": null,
"starting_proposal_id": "1",
"tally_params": {
"quorum": "0.334000000000000000",
"threshold": "0.500000000000000000",
"veto": "0.334000000000000000"
},
"votes": null,
"voting_params": {
"voting_period": "300000000000"
}
}
}
}

View File

@ -0,0 +1,360 @@
{
"genesis_time": "2020-01-22T22:39:50.083273Z",
"chain_id": "testing",
"consensus_params": {
"block": {
"max_bytes": "200000",
"max_gas": "2000000",
"time_iota_ms": "1000"
},
"evidence": {
"max_age": "1000000"
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"app_hash": "",
"app_state": {
"slashing": {
"missed_blocks": {},
"params": {
"downtime_jail_duration": "600000000000",
"max_evidence_age": "3600000000000",
"min_signed_per_window": "0.010000000000000000",
"signed_blocks_window": "1000",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.000100000000000000"
},
"signing_infos": {}
},
"params": null,
"bank": {
"send_enabled": true
},
"crisis": {
"constant_fee": {
"amount": "1333000000",
"denom": "ukava"
}
},
"cdp": {
"cdps": [],
"debt_denom": "ukava",
"deposits": [],
"gov_denom": "ukava",
"params": {
"circuit_breaker": false,
"collateral_params": [
{
"auction_size": "10000000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 0,
"stability_fee": "1.0000000007829977"
},
{
"auction_size": "100000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "10000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.1",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
],
"debt_auction_threshold": "9000000",
"debt_params": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"denom": "usdx",
"reference_asset": "usd"
}
],
"global_debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"surplus_auction_threshold": "9000000"
},
"previous_block_time": "1970-01-01T00:00:00Z",
"starting_cdp_id": "1"
},
"auction": {
"auctions": [],
"next_auction_id": "0",
"params": {
"bid_duration": "100000000000",
"max_auction_duration": "100000000000"
}
},
"genutil": {
"gentxs": [
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgCreateValidator",
"value": {
"description": {
"moniker": "validator",
"identity": "",
"website": "",
"security_contact": "",
"details": ""
},
"commission": {
"rate": "0.100000000000000000",
"max_rate": "0.200000000000000000",
"max_change_rate": "0.010000000000000000"
},
"min_self_delegation": "1",
"delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt",
"value": {
"denom": "ukava",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw=="
}
],
"memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656"
}
}
]
},
"supply": {
"supply": []
},
"validatorvesting": {
"previous_block_time": "1970-01-01T00:00:00Z"
},
"mint": {
"minter": {
"annual_provisions": "0.000000000000000000",
"inflation": "0.020000000000000000"
},
"params": {
"blocks_per_year": "6311520",
"goal_bonded": "0.670000000000000000",
"inflation_max": "0.130000000000000000",
"inflation_min": "0.010000000000000000",
"inflation_rate_change": "0.130000000000000000",
"mint_denom": "ukava"
}
},
"distribution": {
"base_proposer_reward": "0.010000000000000000",
"bonus_proposer_reward": "0.040000000000000000",
"community_tax": "0.000000000000000000",
"delegator_starting_infos": [],
"delegator_withdraw_infos": [],
"fee_pool": {
"community_pool": []
},
"outstanding_rewards": [],
"previous_proposer": "",
"validator_accumulated_commissions": [],
"validator_current_rewards": [],
"validator_historical_rewards": [],
"validator_slash_events": [],
"withdraw_addr_enabled": true
},
"auth": {
"accounts": [
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
}
],
"params": {
"max_memo_characters": "256",
"sig_verify_cost_ed25519": "590",
"sig_verify_cost_secp256k1": "1000",
"tx_sig_limit": "7",
"tx_size_cost_per_byte": "10"
}
},
"staking": {
"delegations": null,
"exported": false,
"last_total_power": "0",
"last_validator_powers": null,
"params": {
"bond_denom": "ukava",
"max_entries": 7,
"max_validators": 100,
"unbonding_time": "3600000000000"
},
"redelegations": null,
"unbonding_delegations": null,
"validators": null
},
"pricefeed": {
"params": {
"markets": [
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp:usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "btc",
"market_id": "btc:usd",
"oracles": [],
"quote_asset": "usd"
}
]
},
"posted_prices": [
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "btc:usd",
"oracle_address": "",
"price": "8700.0"
},
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "xrp:usd",
"oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"price": "0.25"
}
]
},
"gov": {
"deposit_params": {
"max_deposit_period": "300000000000",
"min_deposit": [
{
"amount": "1000000",
"denom": "ukava"
}
]
},
"deposits": null,
"proposals": null,
"starting_proposal_id": "1",
"tally_params": {
"quorum": "0.334000000000000000",
"threshold": "0.500000000000000000",
"veto": "0.334000000000000000"
},
"votes": null,
"voting_params": {
"voting_period": "300000000000"
}
}
}
}

View File

@ -0,0 +1,50 @@
{
"title": "CDP Param Change",
"description": "Add collateral XRP",
"changes": [
{
"subspace": "cdp",
"key": "CollateralParams",
"value": [
{
"auction_size": "50000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "1000000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.05",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 0,
"stability_fee": "1.000000001547126"
},
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
]
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,72 @@
{
"title": "CDP Param Change",
"description": "Add collateral XRP",
"changes": [
{
"subspace": "cdp",
"key": "CollateralParams",
"value": [
{
"auction_size": "50000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "1000000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.05",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 0,
"stability_fee": "1.000000001547126"
},
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
]
},
{
"subspace": "cdp",
"key": "DebtParams",
"value": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"denom": "usdx",
"reference_asset": "usd"
}
]
},
{
"subspace": "cdp",
"key": "GlobalDebtLimit",
"value": [
{
"amount": "3000000000000",
"denom": "usdx"
}
]
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,344 @@
{
"genesis_time": "2020-01-22T22:39:50.083273Z",
"chain_id": "testing",
"consensus_params": {
"block": {
"max_bytes": "200000",
"max_gas": "2000000",
"time_iota_ms": "1000"
},
"evidence": {
"max_age": "1000000"
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"app_hash": "",
"app_state": {
"slashing": {
"missed_blocks": {},
"params": {
"downtime_jail_duration": "600000000000",
"max_evidence_age": "3600000000000",
"min_signed_per_window": "0.010000000000000000",
"signed_blocks_window": "1000",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.000100000000000000"
},
"signing_infos": {}
},
"params": null,
"bank": {
"send_enabled": true
},
"crisis": {
"constant_fee": {
"amount": "1333000000",
"denom": "ukava"
}
},
"cdp": {
"cdps": [],
"debt_denom": "debt",
"deposits": [],
"gov_denom": "ukava",
"params": {
"circuit_breaker": false,
"collateral_params": [
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
],
"debt_auction_threshold": "1000000000",
"debt_params": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"denom": "usdx",
"reference_asset": "usd"
}
],
"global_debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"surplus_auction_threshold": "1000000000"
},
"previous_block_time": "1970-01-01T00:00:00Z",
"starting_cdp_id": "1"
},
"auction": {
"auctions": [],
"next_auction_id": "0",
"params": {
"bid_duration": "600000000000",
"max_auction_duration": "172800000000000"
}
},
"genutil": {
"gentxs": [
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgCreateValidator",
"value": {
"description": {
"moniker": "validator",
"identity": "",
"website": "",
"security_contact": "",
"details": ""
},
"commission": {
"rate": "0.100000000000000000",
"max_rate": "0.200000000000000000",
"max_change_rate": "0.010000000000000000"
},
"min_self_delegation": "1",
"delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt",
"value": {
"denom": "ukava",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw=="
}
],
"memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656"
}
}
]
},
"supply": {
"supply": []
},
"validatorvesting": {
"previous_block_time": "1970-01-01T00:00:00Z"
},
"mint": {
"minter": {
"annual_provisions": "0.000000000000000000",
"inflation": "0.020000000000000000"
},
"params": {
"blocks_per_year": "6311520",
"goal_bonded": "0.670000000000000000",
"inflation_max": "0.130000000000000000",
"inflation_min": "0.010000000000000000",
"inflation_rate_change": "0.130000000000000000",
"mint_denom": "ukava"
}
},
"distribution": {
"base_proposer_reward": "0.010000000000000000",
"bonus_proposer_reward": "0.040000000000000000",
"community_tax": "0.000000000000000000",
"delegator_starting_infos": [],
"delegator_withdraw_infos": [],
"fee_pool": {
"community_pool": []
},
"outstanding_rewards": [],
"previous_proposer": "",
"validator_accumulated_commissions": [],
"validator_current_rewards": [],
"validator_historical_rewards": [],
"validator_slash_events": [],
"withdraw_addr_enabled": true
},
"auth": {
"accounts": [
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "90000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
}
],
"params": {
"max_memo_characters": "256",
"sig_verify_cost_ed25519": "590",
"sig_verify_cost_secp256k1": "1000",
"tx_sig_limit": "7",
"tx_size_cost_per_byte": "10"
}
},
"staking": {
"delegations": null,
"exported": false,
"last_total_power": "0",
"last_validator_powers": null,
"params": {
"bond_denom": "ukava",
"max_entries": 7,
"max_validators": 100,
"unbonding_time": "3600000000000"
},
"redelegations": null,
"unbonding_delegations": null,
"validators": null
},
"pricefeed": {
"params": {
"markets": [
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp:usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "btc",
"market_id": "btc:usd",
"oracles": [],
"quote_asset": "usd"
}
]
},
"posted_prices": [
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "btc:usd",
"oracle_address": "",
"price": "8700.0"
},
{
"expiry": "2050-01-01T00:00:00Z",
"market_id": "xrp:usd",
"oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"price": "0.25"
}
]
},
"gov": {
"deposit_params": {
"max_deposit_period": "300000000000",
"min_deposit": [
{
"amount": "1000000",
"denom": "ukava"
}
]
},
"deposits": null,
"proposals": null,
"starting_proposal_id": "1",
"tally_params": {
"quorum": "0.334000000000000000",
"threshold": "0.500000000000000000",
"veto": "0.334000000000000000"
},
"votes": null,
"voting_params": {
"voting_period": "300000000000"
}
}
}
}

View File

@ -0,0 +1,45 @@
{
"title": "Pricefeed Param Change",
"description": "Add market bnb-usd",
"changes": [
{
"subspace": "pricefeed",
"key": "Markets",
"value": [
{
"active": true,
"base_asset": "btc",
"market_id": "btc-usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp-usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "bnb",
"market_id": "bnb-usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
}
]
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,359 @@
{
"genesis_time": "2020-01-22T22:39:50.083273Z",
"chain_id": "testing",
"consensus_params": {
"block": {
"max_bytes": "200000",
"max_gas": "2000000",
"time_iota_ms": "1000"
},
"evidence": {
"max_age": "1000000"
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"app_hash": "",
"app_state": {
"slashing": {
"missed_blocks": {},
"params": {
"downtime_jail_duration": "600000000000",
"max_evidence_age": "3600000000000",
"min_signed_per_window": "0.010000000000000000",
"signed_blocks_window": "1000",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.000100000000000000"
},
"signing_infos": {}
},
"params": null,
"bank": {
"send_enabled": true
},
"crisis": {
"constant_fee": {
"amount": "1333000000",
"denom": "ukava"
}
},
"cdp": {
"cdps": [],
"debt_denom": "debt",
"deposits": [],
"gov_denom": "ukava",
"params": {
"circuit_breaker": false,
"collateral_params": [
{
"auction_size": "5000000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.05",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 0,
"stability_fee": "1.000000001547126"
},
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
],
"debt_auction_threshold": "1000000000",
"debt_params": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"denom": "usdx",
"reference_asset": "usd"
}
],
"global_debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"surplus_auction_threshold": "1000000000"
},
"previous_block_time": "1970-01-01T00:00:00Z",
"starting_cdp_id": "1"
},
"auction": {
"auctions": [],
"next_auction_id": "0",
"params": {
"bid_duration": "600000000000",
"max_auction_duration": "172800000000000"
}
},
"genutil": {
"gentxs": [
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgCreateValidator",
"value": {
"description": {
"moniker": "validator",
"identity": "",
"website": "",
"security_contact": "",
"details": ""
},
"commission": {
"rate": "0.100000000000000000",
"max_rate": "0.200000000000000000",
"max_change_rate": "0.010000000000000000"
},
"min_self_delegation": "1",
"delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt",
"value": {
"denom": "ukava",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw=="
}
],
"memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656"
}
}
]
},
"supply": {
"supply": []
},
"validatorvesting": {
"previous_block_time": "1970-01-01T00:00:00Z"
},
"mint": {
"minter": {
"annual_provisions": "0.000000000000000000",
"inflation": "0.020000000000000000"
},
"params": {
"blocks_per_year": "6311520",
"goal_bonded": "0.670000000000000000",
"inflation_max": "0.130000000000000000",
"inflation_min": "0.010000000000000000",
"inflation_rate_change": "0.130000000000000000",
"mint_denom": "ukava"
}
},
"distribution": {
"base_proposer_reward": "0.010000000000000000",
"bonus_proposer_reward": "0.040000000000000000",
"community_tax": "0.000000000000000000",
"delegator_starting_infos": [],
"delegator_withdraw_infos": [],
"fee_pool": {
"community_pool": []
},
"outstanding_rewards": [],
"previous_proposer": "",
"validator_accumulated_commissions": [],
"validator_current_rewards": [],
"validator_historical_rewards": [],
"validator_slash_events": [],
"withdraw_addr_enabled": true
},
"auth": {
"accounts": [
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "100000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "100000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000",
"denom": "ukava"
},
{
"amount": "100000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
}
],
"params": {
"max_memo_characters": "256",
"sig_verify_cost_ed25519": "590",
"sig_verify_cost_secp256k1": "1000",
"tx_sig_limit": "7",
"tx_size_cost_per_byte": "10"
}
},
"staking": {
"delegations": null,
"exported": false,
"last_total_power": "0",
"last_validator_powers": null,
"params": {
"bond_denom": "ukava",
"max_entries": 7,
"max_validators": 100,
"unbonding_time": "3600000000000"
},
"redelegations": null,
"unbonding_delegations": null,
"validators": null
},
"pricefeed": {
"params": {
"markets": [
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp:usd",
"oracles": [],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "btc",
"market_id": "btc:usd",
"oracles": [],
"quote_asset": "usd"
}
]
},
"posted_prices": [
{
"expiry": "5000-01-01T00:00:00Z",
"market_id": "xrp:usd",
"oracle_address": "",
"price": "0.25"
},
{
"expiry": "5000-01-01T00:00:00Z",
"market_id": "btc:usd",
"oracle_address": "",
"price": "8700.0"
}
]
},
"gov": {
"deposit_params": {
"max_deposit_period": "300000000000",
"min_deposit": [
{
"amount": "1000000",
"denom": "ukava"
}
]
},
"deposits": null,
"proposals": null,
"starting_proposal_id": "1",
"tally_params": {
"quorum": "0.334000000000000000",
"threshold": "0.500000000000000000",
"veto": "0.334000000000000000"
},
"votes": null,
"voting_params": {
"voting_period": "300000000000"
}
}
}
}

View File

@ -0,0 +1,17 @@
{
"title": "Auction Param Change",
"description": "Update auction bid duration",
"changes": [
{
"subspace": "auction",
"key": "BidDuration",
"value": "912345000000"
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,361 @@
{
"genesis_time": "2020-01-22T22:39:50.083273Z",
"chain_id": "testing",
"consensus_params": {
"block": {
"max_bytes": "200000",
"max_gas": "2000000",
"time_iota_ms": "1000"
},
"evidence": {
"max_age": "1000000"
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"app_hash": "",
"app_state": {
"slashing": {
"missed_blocks": {},
"params": {
"downtime_jail_duration": "600000000000",
"max_evidence_age": "3600000000000",
"min_signed_per_window": "0.010000000000000000",
"signed_blocks_window": "1000",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.000100000000000000"
},
"signing_infos": {}
},
"params": null,
"bank": {
"send_enabled": true
},
"crisis": {
"constant_fee": {
"amount": "1333000000",
"denom": "ukava"
}
},
"cdp": {
"cdps": [],
"debt_denom": "debt",
"deposits": [],
"gov_denom": "ukava",
"params": {
"circuit_breaker": false,
"collateral_params": [
{
"auction_size": "5000000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.05",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 0,
"stability_fee": "1.000000001547126"
},
{
"auction_size": "10000000",
"conversion_factor": "8",
"debt_limit": [
{
"amount": "1000000000000",
"denom": "usdx"
}
],
"denom": "btc",
"liquidation_penalty": "0.05",
"liquidation_ratio": "1.5",
"market_id": "btc:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
],
"debt_auction_threshold": "1000000000",
"debt_params": [
{
"conversion_factor": "6",
"debt_floor": "10000000",
"debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"denom": "usdx",
"reference_asset": "usd"
}
],
"global_debt_limit": [
{
"amount": "2000000000000",
"denom": "usdx"
}
],
"surplus_auction_threshold": "1000000000"
},
"previous_block_time": "1970-01-01T00:00:00Z",
"starting_cdp_id": "1"
},
"auction": {
"auctions": [],
"next_auction_id": "0",
"params": {
"bid_duration": "600000000000",
"max_auction_duration": "172800000000000"
}
},
"genutil": {
"gentxs": [
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgCreateValidator",
"value": {
"description": {
"moniker": "validator",
"identity": "",
"website": "",
"security_contact": "",
"details": ""
},
"commission": {
"rate": "0.100000000000000000",
"max_rate": "0.200000000000000000",
"max_change_rate": "0.010000000000000000"
},
"min_self_delegation": "1",
"delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt",
"value": {
"denom": "ukava",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw=="
}
],
"memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656"
}
}
]
},
"supply": {
"supply": []
},
"validatorvesting": {
"previous_block_time": "1970-01-01T00:00:00Z"
},
"mint": {
"minter": {
"annual_provisions": "0.000000000000000000",
"inflation": "0.020000000000000000"
},
"params": {
"blocks_per_year": "6311520",
"goal_bonded": "0.670000000000000000",
"inflation_max": "0.130000000000000000",
"inflation_min": "0.010000000000000000",
"inflation_rate_change": "0.130000000000000000",
"mint_denom": "ukava"
}
},
"distribution": {
"base_proposer_reward": "0.010000000000000000",
"bonus_proposer_reward": "0.040000000000000000",
"community_tax": "0.000000000000000000",
"delegator_starting_infos": [],
"delegator_withdraw_infos": [],
"fee_pool": {
"community_pool": []
},
"outstanding_rewards": [],
"previous_proposer": "",
"validator_accumulated_commissions": [],
"validator_current_rewards": [],
"validator_historical_rewards": [],
"validator_slash_events": [],
"withdraw_addr_enabled": true
},
"auth": {
"accounts": [
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000000",
"denom": "ukava"
},
{
"amount": "9000000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000000",
"denom": "ukava"
},
{
"amount": "9000000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
},
{
"type": "cosmos-sdk/Account",
"value": {
"account_number": "0",
"address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"coins": [
{
"amount": "10000000000",
"denom": "btc"
},
{
"amount": "9000000000000",
"denom": "ukava"
},
{
"amount": "9000000000000",
"denom": "xrp"
}
],
"public_key": null,
"sequence": "0"
}
}
],
"params": {
"max_memo_characters": "256",
"sig_verify_cost_ed25519": "590",
"sig_verify_cost_secp256k1": "1000",
"tx_sig_limit": "7",
"tx_size_cost_per_byte": "10"
}
},
"staking": {
"delegations": null,
"exported": false,
"last_total_power": "0",
"last_validator_powers": null,
"params": {
"bond_denom": "ukava",
"max_entries": 7,
"max_validators": 100,
"unbonding_time": "3600000000000"
},
"redelegations": null,
"unbonding_delegations": null,
"validators": null
},
"pricefeed": {
"params": {
"markets": [
{
"active": true,
"base_asset": "xrp",
"market_id": "xrp:usd",
"oracles": [
"kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"
],
"quote_asset": "usd"
},
{
"active": true,
"base_asset": "btc",
"market_id": "btc:usd",
"oracles": [],
"quote_asset": "usd"
}
]
},
"posted_prices": [
{
"expiry": "5000-01-01T00:00:00Z",
"market_id": "xrp:usd",
"oracle_address": "",
"price": "0.25"
},
{
"expiry": "5000-01-01T00:00:00Z",
"market_id": "btc:usd",
"oracle_address": "",
"price": "8700.0"
}
]
},
"gov": {
"deposit_params": {
"max_deposit_period": "300000000000",
"min_deposit": [
{
"amount": "1000000",
"denom": "ukava"
}
]
},
"deposits": null,
"proposals": null,
"starting_proposal_id": "1",
"tally_params": {
"quorum": "0.334000000000000000",
"threshold": "0.500000000000000000",
"veto": "0.334000000000000000"
},
"votes": null,
"voting_params": {
"voting_period": "300000000000"
}
}
}
}

View File

@ -0,0 +1,34 @@
{
"title": "CDP Param Change",
"description": "Update usdx debt limit for collateral type XRP",
"changes": [
{
"subspace": "cdp",
"key": "CollateralParams",
"value": [
{
"auction_size": "10000000",
"conversion_factor": "6",
"debt_limit": [
{
"amount": "750000000",
"denom": "usdx"
}
],
"denom": "xrp",
"liquidation_penalty": "0.05",
"liquidation_ratio": "2.0",
"market_id": "xrp:usd",
"prefix": 1,
"stability_fee": "1.0000000007829977"
}
]
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,17 @@
{
"title": "Auction Param Change",
"description": "Update max auction duration",
"changes": [
{
"subspace": "auction",
"key": "MaxAuctionDuration",
"value": "987654321000"
}
],
"deposit": [
{
"denom": "ukava",
"amount": "1000000"
}
]
}

View File

@ -0,0 +1,39 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cdp/MsgCreateCDP",
"value": {
"sender": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
],
"principal": [
{
"denom": "usdx",
"amount": "10000000"
}
]
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "AxYhgA8CL/ZmTZCQDS2dGcccu+dv7zOBT0Wz0+grIufW"
},
"signature": "KMGQekAgj9qSqnu38swM/jTnIkrSskL4m9L3QQlBIbVZdzDQVh9Rkb0UL7TPeXTaswlSTiwFic3hvZZOcjM4SA=="
}
],
"memo": ""
}
}

View File

@ -0,0 +1,34 @@
{
"tx": {
"msg": [
{
"type": "cdp/MsgDeposit",
"value": {
"depositor": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj",
"owner": "kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
]
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "A2SB6wV+X9cVl6URZ28mmfxNQwFJD1u3oo5G332jMa2L"
},
"signature": "mJCjAhEVS/2KCvmGpcnvmeHCBo/UEu3/SbdhoyWkp8sDOLe5FO6016HyjUMJ2jJh80aSBJpPFQyyh2gOFQluXw=="
}
],
"memo": ""
},
"mode": "sync"
}

View File

@ -0,0 +1,30 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "pricefeed/MsgPostPrice",
"value": {
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"market_id": "xrp:usd",
"price": "0.290000000000000000",
"expiry": "2100-03-29T10:40:00Z"
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43"
},
"signature": "1CMkPnD0UYSpIgPTIGUjroM/16DizLgrR+NSNlsjNs1b5cvD2j3VMfYM0nyBa4IC305RkBOUdthTPc6RPZWDGg=="
}
],
"memo": ""
}
}

View File

@ -0,0 +1,31 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cdp/MsgCreateCDP",
"value": {
"sender": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
],
"principal": [
{
"denom": "usdx",
"amount": "10000000"
}
]
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": null,
"memo": ""
}
}

View File

@ -0,0 +1,25 @@
{
"base_req": {
"from": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"memo": "",
"chain_id": "testing",
"account_number": "2",
"sequence": "0",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"owner": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
],
"principal": [
{
"denom": "usdx",
"amount": "10000000"
}
]
}

View File

@ -0,0 +1,20 @@
{
"base_req": {
"from": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj",
"memo": "",
"chain_id": "kava-internal",
"account_number": "12",
"sequence": "0",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"owner": "kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq",
"depositor": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
]
}

View File

@ -0,0 +1,25 @@
{
"base_req": {
"from": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"memo": "",
"chain_id": "testing",
"account_number": "2",
"sequence": "0",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"owner": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03",
"collateral": [
{
"denom": "xrp",
"amount": "110000000"
}
],
"principal": [
{
"denom": "usdx",
"amount": "10000000"
}
]
}

View File

@ -0,0 +1,16 @@
{
"base_req": {
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"memo": "",
"chain_id": "testing",
"account_number": "0",
"sequence": "96",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"market_id": "xrp:usd",
"price": "0.29",
"expiry": "4110000000"
}

View File

@ -0,0 +1,22 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "pricefeed/MsgPostPrice",
"value": {
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"market_id": "xrp:usd",
"price": "0.290000000000000000",
"expiry": "2100-03-29T10:40:00Z"
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": null,
"memo": ""
}
}

View File

@ -0,0 +1,16 @@
{
"base_req": {
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"memo": "",
"chain_id": "testing",
"account_number": "0",
"sequence": "96",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"from": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw",
"market_id": "xrp:usd",
"price": "0.29",
"expiry": "4110000000"
}

View File

@ -0,0 +1,16 @@
{
"base_req": {
"from": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"memo": "arbitrary text",
"chain_id": "testing",
"account_number": "0",
"sequence": "2",
"gas": "500000",
"gas_adjustment": "1.0",
"simulate": false
},
"amount": {
"denom": "usdx",
"amount": "100000000"
}
}

View File

@ -0,0 +1,32 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "auction/MsgPlaceBid",
"value": {
"auction_id": "1",
"bidder": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"amount": {
"denom": "usdx",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "AsBYqm9S7cjIBdG6N/xzAGuEizTt2S2CJuM1FCsgta7u"
},
"signature": "O3zsB8P61SN7/5C+TNfO9hMaFYWV6hjr2aVigN0GrckR2PQGZhpyEZ03P71A9uK7DFLawzvAHDZRqQu833lAnA=="
}
],
"memo": "arbitrary text"
}
}

View File

@ -0,0 +1,24 @@
{
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "auction/MsgPlaceBid",
"value": {
"auction_id": "1",
"bidder": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"amount": {
"denom": "usdx",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": null,
"memo": "arbitrary text"
}
}

View File

@ -0,0 +1,32 @@
{
"tx": {
"msg": [
{
"type": "auction/MsgPlaceBid",
"value": {
"auction_id": "1",
"bidder": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj",
"amount": {
"denom": "usdx",
"amount": "100000000"
}
}
}
],
"fee": {
"amount": [],
"gas": "500000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "AsBYqm9S7cjIBdG6N/xzAGuEizTt2S2CJuM1FCsgta7u"
},
"signature": "O3zsB8P61SN7/5C+TNfO9hMaFYWV6hjr2aVigN0GrckR2PQGZhpyEZ03P71A9uK7DFLawzvAHDZRqQu833lAnA=="
}
],
"memo": "arbitrary text"
},
"mode": "block"
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require (
github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1
github.com/gorilla/mux v1.7.3
github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.4.0

30
go.sum
View File

@ -43,6 +43,7 @@ github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c/go.mod h1:Fxj
github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0=
github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y=
github.com/cosmos/cosmos-sdk v0.37.1 h1:mz5W3Au32VIPPtrY65dheVYeVDSFfS3eSSmuIj+cXsI=
github.com/cosmos/cosmos-sdk v0.37.4 h1:1ioXxkpiS+wOgaUbROeDIyuF7hciU5nti0TSyBmV2Ok=
github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI=
github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc=
@ -58,6 +59,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
@ -69,6 +72,8 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
@ -140,6 +145,11 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.8.5 h1:2ucYeik+NtUTg+IAiNQtoFC5ZGs5mIVidI7Ome0Br3Y=
github.com/klauspost/compress v1.8.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@ -156,6 +166,9 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
@ -199,6 +212,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 h1:o0pGzJnfjk0E+CZg6jxQrADfk9WYO9fMuLOtSP4owtE=
github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75/go.mod h1:/L9q8uCsB8BOWdzLK+6WIwkAlcMfKhFCZY0n8/CLHRY=
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk=
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@ -206,7 +221,12 @@ github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -267,6 +287,11 @@ github.com/tendermint/tm-db v0.2.0/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.5.0 h1:dhq+O9pmNZFF6qAXpasMO1xSm7dL4qEz2ylfZN8BG9w=
github.com/valyala/fasthttp v1.5.0/go.mod h1:eriCz9OhZjKCGfJ185a/IDgNl0bg9IbzfpcslMZXU1c=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8=
@ -297,6 +322,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -310,10 +338,12 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View File

@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "./swagger-testnet-4000.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View File

@ -0,0 +1,68 @@
<!doctype html>
<html lang="en-US">
<title>Swagger UI: OAuth2 Redirect</title>
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
x/auction/abci.go Normal file
View File

@ -0,0 +1,13 @@
package auction
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// EndBlocker runs at the end of every block.
func EndBlocker(ctx sdk.Context, k Keeper) {
err := k.CloseExpiredAuctions(ctx)
if err != nil {
panic(err)
}
}

66
x/auction/abci_test.go Normal file
View File

@ -0,0 +1,66 @@
package auction_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
)
func TestKeeper_EndBlocker(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(2)
buyer := addrs[0]
returnAddrs := addrs[1:]
returnWeights := []sdk.Int{sdk.NewInt(1)}
sellerModName := cdp.LiquidatorMacc
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetAuctionKeeper()
auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40))
require.NoError(t, err)
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30)))
// Run the endblocker, simulating a block time 1ns before auction expiry
preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1)
auction.EndBlocker(ctx.WithBlockTime(preExpiryTime), keeper)
// Check auction has not been closed yet
_, found := keeper.GetAuction(ctx, auctionID)
require.True(t, found)
// Run the endblocker, simulating a block time equal to auction expiry
expiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration)
auction.EndBlocker(ctx.WithBlockTime(expiryTime), keeper)
// Check auction has been closed
_, found = keeper.GetAuction(ctx, auctionID)
require.False(t, found)
}
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}

80
x/auction/alias.go Normal file
View File

@ -0,0 +1,80 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/auction/types/
// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper/
package auction
import (
"github.com/kava-labs/kava/x/auction/keeper"
"github.com/kava-labs/kava/x/auction/types"
)
const (
DefaultCodespace = types.DefaultCodespace
CodeInvalidInitialAuctionID = types.CodeInvalidInitialAuctionID
CodeUnrecognizedAuctionType = types.CodeUnrecognizedAuctionType
CodeAuctionNotFound = types.CodeAuctionNotFound
CodeAuctionHasNotExpired = types.CodeAuctionHasNotExpired
CodeAuctionHasExpired = types.CodeAuctionHasExpired
CodeInvalidBidDenom = types.CodeInvalidBidDenom
CodeInvalidLotDenom = types.CodeInvalidLotDenom
CodeBidTooSmall = types.CodeBidTooSmall
CodeBidTooLarge = types.CodeBidTooLarge
CodeLotTooLarge = types.CodeLotTooLarge
CodeCollateralAuctionIsInReversePhase = types.CodeCollateralAuctionIsInReversePhase
CodeCollateralAuctionIsInForwardPhase = types.CodeCollateralAuctionIsInForwardPhase
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
DefaultBidDuration = types.DefaultBidDuration
QueryGetAuction = types.QueryGetAuction
DefaultNextAuctionID = types.DefaultNextAuctionID
)
var (
// functions aliases
NewSurplusAuction = types.NewSurplusAuction
NewDebtAuction = types.NewDebtAuction
NewCollateralAuction = types.NewCollateralAuction
NewWeightedAddresses = types.NewWeightedAddresses
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
GetAuctionKey = types.GetAuctionKey
GetAuctionByTimeKey = types.GetAuctionByTimeKey
Uint64FromBytes = types.Uint64FromBytes
Uint64ToBytes = types.Uint64ToBytes
NewMsgPlaceBid = types.NewMsgPlaceBid
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
AuctionKeyPrefix = types.AuctionKeyPrefix
AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix
NextAuctionIDKey = types.NextAuctionIDKey
KeyAuctionBidDuration = types.KeyAuctionBidDuration
KeyAuctionDuration = types.KeyAuctionDuration
)
type (
Auction = types.Auction
BaseAuction = types.BaseAuction
SurplusAuction = types.SurplusAuction
DebtAuction = types.DebtAuction
CollateralAuction = types.CollateralAuction
WeightedAddresses = types.WeightedAddresses
SupplyKeeper = types.SupplyKeeper
GenesisAuctions = types.GenesisAuctions
GenesisAuction = types.GenesisAuction
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
Params = types.Params
Keeper = keeper.Keeper
)

View File

@ -0,0 +1,120 @@
package cli
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/auction/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
auctionQueryCmd := &cobra.Command{
Use: "auction",
Short: "Querying commands for the auction module",
}
auctionQueryCmd.AddCommand(client.GetCommands(
QueryGetAuctionCmd(queryRoute, cdc),
QueryGetAuctionsCmd(queryRoute, cdc),
QueryParamsCmd(queryRoute, cdc),
)...)
return auctionQueryCmd
}
// QueryGetAuctionCmd queries one auction in the store
func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "auction [auction-id]",
Short: "get a info about an auction",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("auction-id '%s' not a valid uint", args[0])
}
bz, err := cdc.MarshalJSON(types.QueryAuctionParams{
AuctionID: id,
})
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuction), bz)
if err != nil {
return err
}
// Decode and print results
var auction types.Auction
cdc.MustUnmarshalJSON(res, &auction)
auctionWithPhase := types.NewAuctionWithPhase(auction)
return cliCtx.PrintOutput(auctionWithPhase)
},
}
}
// QueryGetAuctionsCmd queries the auctions in the store
func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "auctions",
Short: "get a list of active auctions",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuctions), nil)
if err != nil {
return err
}
// Decode and print results
var auctions types.Auctions
cdc.MustUnmarshalJSON(res, &auctions)
auctionsWithPhase := []types.AuctionWithPhase{} // using empty slice so json returns [] instead of null when there's no auctions
for _, a := range auctions {
auctionsWithPhase = append(auctionsWithPhase, types.NewAuctionWithPhase(a))
}
return cliCtx.PrintOutput(auctionsWithPhase)
},
}
}
// QueryParamsCmd queries the auction module parameters
func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the auction module parameters",
Long: "Get the current global auction module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, _, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
// Decode and print results
var out types.Params
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

View File

@ -0,0 +1,69 @@
package cli
import (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/auction/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
auctionTxCmd := &cobra.Command{
Use: "auction",
Short: "auction transactions subcommands",
}
auctionTxCmd.AddCommand(client.PostCommands(
GetCmdPlaceBid(cdc),
)...)
return auctionTxCmd
}
// GetCmdPlaceBid cli command for placing bids on auctions
func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "bid [auction-id] [amount]",
Short: "place a bid on an auction",
Long: strings.TrimSpace(
fmt.Sprintf(`Place a bid on any type of auction, updating the latest bid amount to [amount]. Collateral auctions must be bid up to their maxbid before entering reverse phase.
Example:
$ %s tx %s bid 34 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("auction-id '%s' not a valid uint", args[0])
}
amt, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), amt)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,119 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/auction/types"
)
const restAuctionID = "auction-id"
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/auctions", types.ModuleName), queryAuctionsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/auctions/{%s}", types.ModuleName, restAuctionID), queryAuctionHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), getParamsHandlerFn(cliCtx)).Methods("GET")
}
func queryAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[restAuctionID]) == 0 {
err := fmt.Errorf("%s required but not specified", restAuctionID)
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
auctionID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[restAuctionID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.QueryAuctionParams{AuctionID: auctionID})
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAuction), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Decode and return results
cliCtx = cliCtx.WithHeight(height)
var auction types.Auction
err = cliCtx.Codec.UnmarshalJSON(res, &auction)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
auctionWithPhase := types.NewAuctionWithPhase(auction)
rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(auctionWithPhase))
}
}
func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Get all auctions
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAuctions), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
// Decode and return results
cliCtx = cliCtx.WithHeight(height)
var auctions types.Auctions
err = cliCtx.Codec.UnmarshalJSON(res, &auctions)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
auctionsWithPhase := []types.AuctionWithPhase{} // using empty slice so json returns [] instead of null when there's no auctions
for _, a := range auctions {
auctionsWithPhase = append(auctionsWithPhase, types.NewAuctionWithPhase(a))
}
rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(auctionsWithPhase))
}
}
func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Get the params
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetParams), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Decode and return results
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,58 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/auction/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/auctions/{%s}/bids", types.ModuleName, restAuctionID), bidHandlerFn(cliCtx)).Methods("POST")
}
type placeBidReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Amount sdk.Coin `json:"amount"`
}
func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get auction ID from url
auctionID, ok := rest.ParseUint64OrReturnBadRequest(w, mux.Vars(r)[restAuctionID])
if !ok {
return
}
// Get info from the http request body
var req placeBidReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
bidderAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Create and return a StdTx
msg := types.NewMsgPlaceBid(auctionID, bidderAddr, req.Amount)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

60
x/auction/genesis.go Normal file
View File

@ -0,0 +1,60 @@
package auction
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
keeper.SetNextAuctionID(ctx, gs.NextAuctionID)
keeper.SetParams(ctx, gs.Params)
totalAuctionCoins := sdk.NewCoins()
for _, a := range gs.Auctions {
keeper.SetAuction(ctx, a)
// find the total coins that should be present in the module account
totalAuctionCoins.Add(a.GetModuleAccountCoins())
}
// check if the module account exists
moduleAcc := supplyKeeper.GetModuleAccount(ctx, ModuleName)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", ModuleName))
}
// check module coins match auction coins
// Note: Other sdk modules do not check this, instead just using the existing module account coins, or if zero, setting them.
if !moduleAcc.GetCoins().IsEqual(totalAuctionCoins) {
panic(fmt.Sprintf("total auction coins (%s) do not equal (%s) module account (%s) ", moduleAcc.GetCoins(), ModuleName, totalAuctionCoins))
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
nextAuctionID, err := keeper.GetNextAuctionID(ctx)
if err != nil {
panic(err)
}
params := keeper.GetParams(ctx)
genAuctions := GenesisAuctions{} // return empty list instead of nil if no auctions
keeper.IterateAuctions(ctx, func(a Auction) bool {
ga, ok := a.(types.GenesisAuction)
if !ok {
panic("could not convert stored auction to GenesisAuction type")
}
genAuctions = append(genAuctions, ga)
return false
})
return NewGenesisState(nextAuctionID, params, genAuctions)
}

109
x/auction/genesis_test.go Normal file
View File

@ -0,0 +1,109 @@
package auction_test
import (
"sort"
"testing"
"time"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction"
)
var testTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
var testAuction = auction.NewCollateralAuction(
"seller",
c("lotdenom", 10),
testTime,
c("biddenom", 1000),
auction.WeightedAddresses{},
c("debt", 1000),
).WithID(3).(auction.GenesisAuction)
func TestInitGenesis(t *testing.T) {
t.Run("valid", func(t *testing.T) {
// setup keepers
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
// create genesis
gs := auction.NewGenesisState(
10,
auction.DefaultParams(),
auction.GenesisAuctions{testAuction},
)
// run init
require.NotPanics(t, func() {
auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs)
})
// check state is as expected
actualID, err := keeper.GetNextAuctionID(ctx)
require.NoError(t, err)
require.Equal(t, gs.NextAuctionID, actualID)
require.Equal(t, gs.Params, keeper.GetParams(ctx))
// TODO is there a nicer way of comparing state?
sort.Slice(gs.Auctions, func(i, j int) bool {
return gs.Auctions[i].GetID() > gs.Auctions[j].GetID()
})
i := 0
keeper.IterateAuctions(ctx, func(a auction.Auction) bool {
require.Equal(t, gs.Auctions[i], a)
i++
return false
})
})
t.Run("invalid", func(t *testing.T) {
// setup keepers
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
// create invalid genesis
gs := auction.NewGenesisState(
0, // next id < testAuction ID
auction.DefaultParams(),
auction.GenesisAuctions{testAuction},
)
// check init fails
require.Panics(t, func() {
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
})
})
}
func TestExportGenesis(t *testing.T) {
t.Run("default", func(t *testing.T) {
// setup state
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates()
// export
gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper())
// check state matches
require.Equal(t, auction.DefaultGenesisState(), gs)
})
t.Run("one auction", func(t *testing.T) {
// setup state
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates()
tApp.GetAuctionKeeper().SetAuction(ctx, testAuction)
// export
gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper())
// check state matches
expectedGenesisState := auction.DefaultGenesisState()
expectedGenesisState.Auctions = append(expectedGenesisState.Auctions, testAuction)
require.Equal(t, expectedGenesisState, gs)
})
}

43
x/auction/handler.go Normal file
View File

@ -0,0 +1,43 @@
package auction
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
)
// NewHandler returns a function to handle all "auction" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case MsgPlaceBid:
return handleMsgPlaceBid(ctx, keeper, msg)
default:
return sdk.ErrUnknownRequest(fmt.Sprintf("Unrecognized auction msg type: %T", msg)).Result()
}
}
}
func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result {
err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Amount)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Bidder.String()),
),
)
return sdk.Result{
Events: ctx.EventManager().Events(),
}
}

View File

@ -0,0 +1,526 @@
package keeper
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction/types"
)
// StartSurplusAuction starts a new surplus (forward) auction.
func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) {
auction := types.NewSurplusAuction(
seller,
lot,
bidDenom,
types.DistantFuture)
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
return 0, err
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
),
)
return auctionID, nil
}
// StartDebtAuction starts a new debt (reverse) auction.
func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) {
auction := types.NewDebtAuction(
buyer,
bid,
initialLot,
types.DistantFuture,
debt)
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
macc := k.supplyKeeper.GetModuleAccount(ctx, buyer)
if !macc.HasPermission(supply.Minter) {
return 0, types.ErrInvalidModulePermissions(k.codespace, supply.Minter)
}
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt))
if err != nil {
return 0, err
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
),
)
return auctionID, nil
}
// StartCollateralAuction starts a new collateral (2-phase) auction.
func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) {
weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights)
if err != nil {
return 0, err
}
auction := types.NewCollateralAuction(
seller,
lot,
types.DistantFuture,
maxBid,
weightedAddresses,
debt)
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
return 0, err
}
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(debt))
if err != nil {
return 0, err
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
),
)
return auctionID, nil
}
// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return types.ErrAuctionNotFound(k.codespace, auctionID)
}
// validation common to all auctions
if ctx.BlockTime().After(auction.GetEndTime()) {
return types.ErrAuctionHasExpired(k.codespace, auctionID)
}
// move coins and return updated auction
var err sdk.Error
var updatedAuction types.Auction
switch a := auction.(type) {
case types.SurplusAuction:
if updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount); err != nil {
return err
}
case types.DebtAuction:
if updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount); err != nil {
return err
}
case types.CollateralAuction:
if !a.IsReversePhase() {
updatedAuction, err = k.PlaceForwardBidCollateral(ctx, a, bidder, newAmount)
} else {
updatedAuction, err = k.PlaceReverseBidCollateral(ctx, a, bidder, newAmount)
}
if err != nil {
return err
}
default:
return types.ErrUnrecognizedAuctionType(k.codespace)
}
k.SetAuction(ctx, updatedAuction)
return nil
}
// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom)
}
if !a.Bid.IsLT(bid) {
return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid)
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid, or the amount being zero (sending zero coins produces meaningless send events).
if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Increase in bid is burned
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid)))
if err != nil {
return a, err
}
err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid)))
if err != nil {
return a, err
}
// Update Auction
a.Bidder = bidder
a.Bid = bid
if !a.HasReceivedBids {
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
}
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
a.HasReceivedBids = true
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
),
)
return a, nil
}
// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom)
}
if a.IsReversePhase() {
return a, types.ErrCollateralAuctionIsInReversePhase(k.codespace, a.ID)
}
if !a.Bid.IsLT(bid) {
return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid)
}
if a.MaxBid.IsLT(bid) {
return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid)
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events).
if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Increase in bid sent to auction initiator
bidIncrement := bid.Sub(a.Bid)
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bidIncrement))
if err != nil {
return a, err
}
// Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement (or whatever is left if < bidIncrement).
if a.CorrespondingDebt.IsPositive() {
debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, a.CorrespondingDebt.Amount)
debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn)
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn))
if err != nil {
return a, err
}
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above
}
// Update Auction
a.Bidder = bidder
a.Bid = bid
if !a.HasReceivedBids {
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
}
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
a.HasReceivedBids = true
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
),
)
return a, nil
}
// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom)
}
if !a.IsReversePhase() {
return a, types.ErrCollateralAuctionIsInForwardPhase(k.codespace, a.ID)
}
if !lot.IsLT(a.Lot) {
return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot)
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid
if !bidder.Equals(a.Bidder) {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Decrease in lot is sent to weighted addresses (normally the CDP depositors)
// TODO paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - check this can't be gamed.
lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights)
if err != nil {
return a, err
}
for i, payout := range lotPayouts {
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout))
if err != nil {
return a, err
}
}
// Update Auction
a.Bidder = bidder
a.Lot = lot
if !a.HasReceivedBids {
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
}
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
a.HasReceivedBids = true
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
),
)
return a, nil
}
// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom)
}
if !lot.IsLT(a.Lot) {
return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot)
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid
if !bidder.Equals(a.Bidder) {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to min of Bid and amount of debt.
if a.Bidder.Equals(supply.NewModuleAddress(a.Initiator)) {
debtAmountToReturn := sdk.MinInt(a.Bid.Amount, a.CorrespondingDebt.Amount)
debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn)
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn))
if err != nil {
return a, err
}
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above
}
// Update Auction
a.Bidder = bidder
a.Lot = lot
if !a.HasReceivedBids {
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
}
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
a.HasReceivedBids = true
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
),
)
return a, nil
}
// CloseAuction closes an auction and distributes funds to the highest bidder.
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return types.ErrAuctionNotFound(k.codespace, auctionID)
}
if ctx.BlockTime().Before(auction.GetEndTime()) {
return types.ErrAuctionHasNotExpired(k.codespace, ctx.BlockTime(), auction.GetEndTime())
}
// payout to the last bidder
switch auc := auction.(type) {
case types.SurplusAuction:
if err := k.PayoutSurplusAuction(ctx, auc); err != nil {
return err
}
case types.DebtAuction:
if err := k.PayoutDebtAuction(ctx, auc); err != nil {
return err
}
case types.CollateralAuction:
if err := k.PayoutCollateralAuction(ctx, auc); err != nil {
return err
}
default:
return types.ErrUnrecognizedAuctionType(k.codespace)
}
k.DeleteAuction(ctx, auctionID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionClose,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
),
)
return nil
}
// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins.
func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error {
err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
if err != nil {
return err
}
}
return nil
}
// PayoutSurplusAuction pays out the proceeds for a surplus auction.
func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
return nil
}
// PayoutCollateralAuction pays out the proceeds for a collateral auction.
func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
if err != nil {
return err
}
}
return nil
}
// CloseExpiredAuctions finds all auctions that are past (or at) their ending times and closes them, paying out to the highest bidder.
func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error {
var expiredAuctions []uint64
k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool {
expiredAuctions = append(expiredAuctions, id)
return false
})
// Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration
for _, id := range expiredAuctions {
if err := k.CloseAuction(ctx, id); err != nil {
return err
}
}
return nil
}
// earliestTime returns the earliest of two times.
func earliestTime(t1, t2 time.Time) time.Time {
if t1.Before(t2) {
return t1
}
return t2 // also returned if times are equal
}
// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights.
func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) {
for _, bucket := range buckets {
if bucket.IsNegative() {
return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight")
}
}
amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
result := make([]sdk.Coin, len(amounts))
for i, a := range amounts {
result[i] = sdk.NewCoin(coin.Denom, a)
}
return result, nil
}

View File

@ -0,0 +1,409 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction/types"
"github.com/kava-labs/kava/x/cdp"
)
func TestSurplusAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0]
sellerModName := cdp.LiquidatorMacc
sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Create an auction (lot: 20 token1, initialBid: 0 token2)
auctionID, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
// PlaceBid (bid: 10 token, lot: same as starting)
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10)))
// Check buyer's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have not increased (because proceeds are burned)
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
// increment bid same bidder
err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20))
require.NoError(t, err)
// Close auction at just at auction expiry time
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 80)))
}
func TestDebtAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
seller := addrs[0]
buyerModName := cdp.LiquidatorMacc
buyerAddr := supply.NewModuleAddress(buyerModName)
tApp := app.NewTestApp()
buyerAcc := supply.NewEmptyModuleAccount(buyerModName, supply.Minter) // reverse auctions mint payout
require.NoError(t, buyerAcc.SetCoins(cs(c("debt", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
buyerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999), c("debt", 20))
require.NoError(t, err)
// Check buyer's coins have not decreased (except for debt), as lot is minted at the end
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("debt", 80)))
// Place a bid
require.NoError(t, keeper.PlaceBid(ctx, auctionID, seller, c("token2", 10)))
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100)))
// Check buyer's coins have increased
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20), c("debt", 100)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check seller's coins increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110)))
}
func TestDebtAuctionDebtRemaining(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
seller := addrs[0]
buyerModName := cdp.LiquidatorMacc
buyerAddr := supply.NewModuleAddress(buyerModName)
tApp := app.NewTestApp()
buyerAcc := supply.NewEmptyModuleAccount(buyerModName, supply.Minter) // reverse auctions mint payout
require.NoError(t, buyerAcc.SetCoins(cs(c("debt", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
buyerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 10), c("token2", 99999), c("debt", 20))
require.NoError(t, err)
// Check buyer's coins have not decreased (except for debt), as lot is minted at the end
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("debt", 80)))
// Place a bid
require.NoError(t, keeper.PlaceBid(ctx, auctionID, seller, c("token2", 10)))
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 90), c("token2", 100)))
// Check buyer's coins have increased
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 90)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check seller's coins increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 90), c("token2", 110)))
// check that debt has increased due to corresponding debt being greater than bid
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 100)))
}
func TestCollateralAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(4)
buyer := addrs[0]
returnAddrs := addrs[1:]
returnWeights := is(30, 20, 10)
sellerModName := cdp.LiquidatorMacc
sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40))
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100), c("debt", 60)))
// Place a forward bid
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10)))
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 70)))
// Check return addresses have not received coins
for _, ra := range returnAddrs {
tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100)))
}
// Place a reverse bid
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 50))) // first bid up to max bid to switch phases
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token1", 15)))
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150), c("debt", 100)))
// Check return addresses have received coins
tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[2], cs(c("token1", 101), c("token2", 100)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50)))
}
func TestCollateralAuctionDebtRemaining(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(4)
buyer := addrs[0]
returnAddrs := addrs[1:]
returnWeights := is(30, 20, 10)
sellerModName := cdp.LiquidatorMacc
sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40))
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100), c("debt", 60)))
// Place a forward bid
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10)))
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 70)))
// Check return addresses have not received coins
for _, ra := range returnAddrs {
tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100)))
}
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// check that buyers coins have increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90)))
// Check return addresses have not received coins
for _, ra := range returnAddrs {
tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100)))
}
// check that token2 has increased by 10, debt by 40, for a net debt increase of 30 debt
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 100)))
}
func TestStartSurplusAuction(t *testing.T) {
someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
type args struct {
seller string
lot sdk.Coin
bidDenom string
}
testCases := []struct {
name string
blockTime time.Time
args args
expectPass bool
}{
{
"normal",
someTime,
args{cdp.LiquidatorMacc, c("stable", 10), "gov"},
true,
},
{
"no module account",
someTime,
args{"nonExistentModule", c("stable", 10), "gov"},
false,
},
{
"not enough coins",
someTime,
args{cdp.LiquidatorMacc, c("stable", 101), "gov"},
false,
},
{
"incorrect denom",
someTime,
args{cdp.LiquidatorMacc, c("notacoin", 10), "gov"},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// setup
initialLiquidatorCoins := cs(c("stable", 100))
tApp := app.NewTestApp()
liqAcc := supply.NewEmptyModuleAccount(cdp.LiquidatorMacc, supply.Burner)
require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}),
)
ctx := tApp.NewContext(false, abci.Header{}).WithBlockTime(tc.blockTime)
keeper := tApp.GetAuctionKeeper()
// run function under test
id, err := keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom)
// check
sk := tApp.GetSupplyKeeper()
liquidatorCoins := sk.GetModuleAccount(ctx, cdp.LiquidatorMacc).GetCoins()
actualAuc, found := keeper.GetAuction(ctx, id)
if tc.expectPass {
require.NoError(t, err)
// check coins moved
require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins)
// check auction in store and is correct
require.True(t, found)
expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{
ID: id,
Initiator: tc.args.seller,
Lot: tc.args.lot,
Bidder: nil,
Bid: c(tc.args.bidDenom, 0),
HasReceivedBids: false,
EndTime: types.DistantFuture,
MaxEndTime: types.DistantFuture,
}})
require.Equal(t, expectedAuction, actualAuc)
} else {
require.Error(t, err)
// check coins not moved
require.Equal(t, initialLiquidatorCoins, liquidatorCoins)
// check auction not in store
require.False(t, found)
}
})
}
}
func TestCloseAuction(t *testing.T) {
// Set up
_, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0]
sellerModName := cdp.LiquidatorMacc
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Create an auction (lot: 20 token1, initialBid: 0 token2)
id, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom
require.NoError(t, err)
// Attempt to close the auction before EndTime
require.Error(t, keeper.CloseAuction(ctx, id))
// Attempt to close auction that does not exist
require.Error(t, keeper.CloseAuction(ctx, 999))
}
func TestCloseExpiredAuctions(t *testing.T) {
// Set up
_, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0]
sellerModName := "liquidator"
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction 1
_, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom
require.NoError(t, err)
// Start auction 2
_, err = keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom
require.NoError(t, err)
// Fast forward the block time
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxAuctionDuration).Add(1))
// Close expired auctions
err = keeper.CloseExpiredAuctions(ctx)
require.NoError(t, err)
}

View File

@ -0,0 +1,348 @@
package keeper_test
import (
"strings"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction/types"
)
type AuctionType int
const (
Invalid AuctionType = 0
Surplus AuctionType = 1
Debt AuctionType = 2
CollateralPhase1 AuctionType = 3
CollateralPhase2 AuctionType = 4
)
func TestAuctionBidding(t *testing.T) {
someTime := time.Date(0001, time.January, 1, 0, 0, 0, 0, time.UTC)
_, addrs := app.GeneratePrivKeyAddressPairs(5)
buyer := addrs[0]
secondBuyer := addrs[1]
modName := "liquidator"
collateralAddrs := addrs[2:]
collateralWeights := is(30, 20, 10)
type auctionArgs struct {
auctionType AuctionType
seller string
lot sdk.Coin
bid sdk.Coin
debt sdk.Coin
addresses []sdk.AccAddress
weights []sdk.Int
}
type bidArgs struct {
bidder sdk.AccAddress
amount sdk.Coin
secondBidder sdk.AccAddress
}
tests := []struct {
name string
auctionArgs auctionArgs
bidArgs bidArgs
expectedError sdk.CodeType
expectedEndTime time.Time
expectedBidder sdk.AccAddress
expectedBid sdk.Coin
expectpass bool
}{
{
"basic: auction doesn't exist",
auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token2", 10), nil},
types.CodeAuctionNotFound,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"surplus: normal",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token2", 10), nil},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
true,
},
{
"surplus: second bidder",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token2", 10), secondBuyer},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 11),
true,
},
{
"surplus: invalid bid denom",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("badtoken", 10), nil},
types.CodeInvalidBidDenom,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"surplus: invalid bid (equal)",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token2", 0), nil},
types.CodeBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"debt: normal",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
bidArgs{buyer, c("token1", 10), nil},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 100),
true,
},
{
"debt: second bidder",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
bidArgs{buyer, c("token1", 10), secondBuyer},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 100),
true,
},
{
"debt: invalid lot denom",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot
bidArgs{buyer, c("badtoken", 10), nil},
types.CodeInvalidLotDenom,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token1", 20),
false,
},
{
"debt: invalid lot size (larger)",
auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token1", 21), nil},
types.CodeLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token1", 20),
false,
},
{
"collateral [forward]: normal",
auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token2", 10), nil},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
true,
},
{
"collateral [forward]: second bidder",
auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token2", 10), secondBuyer},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 11),
true,
},
{
"collateral [forward]: invalid bid denom",
auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("badtoken", 10), nil},
types.CodeInvalidBidDenom,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"collateral [forward]: invalid bid size (smaller)",
auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token2", 0), nil}, // lot, bid
types.CodeBidTooSmall,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"collateral [forward]: invalid bid size (greater than max)",
auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token2", 101), nil}, // lot, bid
types.CodeBidTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
{
"collateral [reverse]: normal",
auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token1", 15), nil},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
true,
},
{
"collateral [reverse]: second bidder",
auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token1", 15), secondBuyer},
sdk.CodeType(0),
someTime.Add(types.DefaultBidDuration),
secondBuyer,
c("token2", 50),
true,
},
{
"collateral [reverse]: invalid lot denom",
auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("badtoken", 15), nil},
types.CodeInvalidLotDenom,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
},
{
"collateral [reverse]: invalid lot size (equal)",
auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token1", 20), nil},
types.CodeLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
},
{
"collateral [reverse]: invalid lot size (greater)",
auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid
bidArgs{buyer, c("token1", 21), nil},
types.CodeLotTooLarge,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 50),
false,
},
{
"basic: closed auction",
auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}},
bidArgs{buyer, c("token2", 10), nil},
types.CodeAuctionHasExpired,
someTime.Add(types.DefaultBidDuration),
buyer,
c("token2", 10),
false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup test
tApp := app.NewTestApp()
// Set up seller account
sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000))))
// Initialize genesis accounts
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
auth.NewBaseAccount(secondBuyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
auth.NewBaseAccount(collateralAddrs[0], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
auth.NewBaseAccount(collateralAddrs[1], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
auth.NewBaseAccount(collateralAddrs[2], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start Auction
var id uint64
var err sdk.Error
switch tc.auctionArgs.auctionType {
case Surplus:
id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom)
case Debt:
id, _ = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt)
case CollateralPhase1, CollateralPhase2:
id, _ = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson
// Move CollateralAuction to debt phase by placing max bid
if tc.auctionArgs.auctionType == CollateralPhase2 {
err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.auctionArgs.bid)
require.NoError(t, err)
}
default:
t.Fail()
}
// Close the auction early to test late bidding (if applicable)
if strings.Contains(tc.name, "closed") {
ctx = ctx.WithBlockTime(types.DistantFuture.Add(1))
}
// Place bid on auction
err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount)
// Place second bid from new bidder
if tc.bidArgs.secondBidder != nil {
// Set bid increase/decrease based on auction type, phase
var secondBid sdk.Coin
switch tc.auctionArgs.auctionType {
case Surplus, CollateralPhase1:
secondBid = tc.bidArgs.amount.Add(c(tc.bidArgs.amount.Denom, 1))
case Debt, CollateralPhase2:
secondBid = tc.bidArgs.amount.Sub(c(tc.bidArgs.amount.Denom, 1))
default:
t.Fail()
}
// Place the second bid
err2 := keeper.PlaceBid(ctx, id, tc.bidArgs.secondBidder, secondBid)
require.NoError(t, err2)
}
// Check success/failure
if tc.expectpass {
require.Nil(t, err)
// Get auction from store
auction, found := keeper.GetAuction(ctx, id)
require.True(t, found)
// Check auction values
require.Equal(t, modName, auction.GetInitiator())
require.Equal(t, tc.expectedBidder, auction.GetBidder())
require.Equal(t, tc.expectedBid, auction.GetBid())
require.Equal(t, tc.expectedEndTime, auction.GetEndTime())
} else {
// Check expected error code type
require.Equal(t, tc.expectedError, err.Result().Code)
}
})
}
}

View File

@ -0,0 +1,24 @@
package keeper_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/kava-labs/kava/app"
)
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}
func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}

171
x/auction/keeper/keeper.go Normal file
View File

@ -0,0 +1,171 @@
package keeper
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/tendermint/tendermint/libs/log"
"github.com/kava-labs/kava/x/auction/types"
)
type Keeper struct {
supplyKeeper types.SupplyKeeper
storeKey sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
codespace sdk.CodespaceType
}
// NewKeeper returns a new auction keeper.
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper {
if addr := supplyKeeper.GetModuleAddress(types.ModuleName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
return Keeper{
supplyKeeper: supplyKeeper,
storeKey: storeKey,
cdc: cdc,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// SetNextAuctionID stores an ID to be used for the next created auction
func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.NextAuctionIDKey, types.Uint64ToBytes(id))
}
// GetNextAuctionID reads the next available global ID from store
func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.NextAuctionIDKey)
if bz == nil {
return 0, types.ErrInvalidInitialAuctionID(k.codespace)
}
return types.Uint64FromBytes(bz), nil
}
// IncrementNextAuctionID increments the next auction ID in the store by 1.
func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error {
id, err := k.GetNextAuctionID(ctx)
if err != nil {
return err
}
k.SetNextAuctionID(ctx, id+1)
return nil
}
// StoreNewAuction stores an auction, adding a new ID
func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, sdk.Error) {
newAuctionID, err := k.GetNextAuctionID(ctx)
if err != nil {
return 0, err
}
auction = auction.WithID(newAuctionID)
k.SetAuction(ctx, auction)
err = k.IncrementNextAuctionID(ctx)
if err != nil {
return 0, err
}
return newAuctionID, nil
}
// SetAuction puts the auction into the store, and updates any indexes.
func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) {
// remove the auction from the byTime index if it is already in there
existingAuction, found := k.GetAuction(ctx, auction.GetID())
if found {
k.removeFromByTimeIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID())
}
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction)
store.Set(types.GetAuctionKey(auction.GetID()), bz)
k.InsertIntoByTimeIndex(ctx, auction.GetEndTime(), auction.GetID())
}
// GetAuction gets an auction from the store.
func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bool) {
var auction types.Auction
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
bz := store.Get(types.GetAuctionKey(auctionID))
if bz == nil {
return auction, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auction)
return auction, true
}
// DeleteAuction removes an auction from the store, and any indexes.
func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) {
auction, found := k.GetAuction(ctx, auctionID)
if found {
k.removeFromByTimeIndex(ctx, auction.GetEndTime(), auctionID)
}
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
store.Delete(types.GetAuctionKey(auctionID))
}
// InsertIntoByTimeIndex adds an auction ID and end time into the byTime index.
func (k Keeper) InsertIntoByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { // TODO make private, and find way to make tests work
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID))
}
// removeFromByTimeIndex removes an auction ID and end time from the byTime index.
func (k Keeper) removeFromByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
store.Delete(types.GetAuctionByTimeKey(endTime, auctionID))
}
// IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime.
// For each auction cb will be callled. If cb returns true the iterator will close and stop.
func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID uint64) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
iterator := store.Iterator(
nil, // start at the very start of the prefix store
sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime
)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
auctionID := types.Uint64FromBytes(iterator.Value())
if cb(auctionID) {
break
}
}
}
// IterateAuctions provides an iterator over all stored auctions.
// For each auction, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var auction types.Auction
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction)
if cb(auction) {
break
}
}
}

View File

@ -0,0 +1,135 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction/types"
)
func SetGetDeleteAuction(t *testing.T) {
// setup keeper, create auction
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal
var id uint64 = 5
auction := types.NewSurplusAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id)
// write and read from store
keeper.SetAuction(ctx, auction)
readAuction, found := keeper.GetAuction(ctx, id)
// check before and after match
require.True(t, found)
require.Equal(t, auction, readAuction)
// check auction is in the index
keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID uint64) bool {
require.Equal(t, auction.GetID(), readID)
return false
})
// delete auction
keeper.DeleteAuction(ctx, id)
// check auction does not exist
_, found = keeper.GetAuction(ctx, id)
require.False(t, found)
// check auction not in index
keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID uint64) bool {
require.Fail(t, "index should be empty", " found auction ID '%s", readID)
return false
})
}
func TestIncrementNextAuctionID(t *testing.T) {
// setup keeper
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
// store id
var id uint64 = 123456
keeper.SetNextAuctionID(ctx, id)
require.NoError(t, keeper.IncrementNextAuctionID(ctx))
// check id was incremented
readID, err := keeper.GetNextAuctionID(ctx)
require.NoError(t, err)
require.Equal(t, id+1, readID)
}
func TestIterateAuctions(t *testing.T) {
// setup
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
auctions := []types.Auction{
types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0),
types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("debt", 12345678)).WithID(1),
types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}, c("debt", 12345678)).WithID(2),
}
for _, a := range auctions {
keeper.SetAuction(ctx, a)
}
// run
var readAuctions []types.Auction
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
readAuctions = append(readAuctions, a)
return false
})
// check
require.Equal(t, auctions, readAuctions)
}
func TestIterateAuctionsByTime(t *testing.T) {
// setup keeper
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
// setup byTime index
byTimeIndex := []struct {
endTime time.Time
auctionID uint64
}{
{time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 9999}, // distant past
{time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 1}, // just before cutoff
{time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 2}, //
{time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 3}, // equal to cutoff
{time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 4}, //
{time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 5}, // just after cutoff
{time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 6}, //
{time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future
}
for _, v := range byTimeIndex {
keeper.InsertIntoByTimeIndex(ctx, v.endTime, v.auctionID)
}
// read out values from index up to a cutoff time and check they are as expected
cutoffTime := time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC)
var expectedIndex []uint64
for _, v := range byTimeIndex {
if v.endTime.Before(cutoffTime) || v.endTime.Equal(cutoffTime) { // endTime ≤ cutoffTime
expectedIndex = append(expectedIndex, v.auctionID)
}
}
var readIndex []uint64
keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id uint64) bool {
readIndex = append(readIndex, id)
return false
})
require.Equal(t, expectedIndex, readIndex)
}

69
x/auction/keeper/math.go Normal file
View File

@ -0,0 +1,69 @@
package keeper
import (
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
// It uses the largest remainder method:
// https://en.wikipedia.org/wiki/Largest_remainder_method
// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
// TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them
if amount.IsNegative() {
panic("negative amount")
}
for _, bucket := range buckets {
if bucket.IsNegative() {
panic("negative bucket")
}
}
totalWeights := totalInts(buckets...)
// split amount by weights, recording whole number part and remainder
quotients := make([]quoRem, len(buckets))
for i := range buckets {
q := amount.Mul(buckets[i]).Quo(totalWeights)
r := amount.Mul(buckets[i]).Mod(totalWeights)
quotients[i] = quoRem{index: i, quo: q, rem: r}
}
// apportion left over to buckets with the highest remainder (to minimize error)
sort.Slice(quotients, func(i, j int) bool {
return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order
})
allocated := sdk.ZeroInt()
for _, qr := range quotients {
allocated = allocated.Add(qr.quo)
}
leftToAllocate := amount.Sub(allocated)
results := make([]sdk.Int, len(quotients))
for _, qr := range quotients {
results[qr.index] = qr.quo
if !leftToAllocate.IsZero() {
results[qr.index] = results[qr.index].Add(sdk.OneInt())
leftToAllocate = leftToAllocate.Sub(sdk.OneInt())
}
}
return results
}
type quoRem struct {
index int
quo sdk.Int
rem sdk.Int
}
// totalInts adds together sdk.Ints
func totalInts(is ...sdk.Int) sdk.Int {
total := sdk.ZeroInt()
for _, i := range is {
total = total.Add(i)
}
return total
}

View File

@ -0,0 +1,36 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestSplitIntIntoWeightedBuckets(t *testing.T) {
testCases := []struct {
name string
amount sdk.Int
buckets []sdk.Int
want []sdk.Int
}{
{"2split1,1", i(2), is(1, 1), is(1, 1)},
{"100split1,9", i(100), is(1, 9), is(10, 90)},
{"7split1,2", i(7), is(1, 2), is(2, 5)},
{"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets)
require.Equal(t, tc.want, got)
})
}
}
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}

View File

@ -0,0 +1,15 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
)
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
k.paramSubspace.GetParamSet(ctx, &params)
return
}

View File

@ -0,0 +1,79 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/auction/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case types.QueryGetAuction:
return queryAuction(ctx, req, keeper)
case types.QueryGetAuctions:
return queryAuctions(ctx, req, keeper)
case types.QueryGetParams:
return queryGetParams(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unknown auction query endpoint")
}
}
}
func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Decode request
var requestParams types.QueryAuctionParams
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
// Lookup auction
auction, found := keeper.GetAuction(ctx, requestParams.AuctionID)
if !found {
return nil, types.ErrAuctionNotFound(types.DefaultCodespace, requestParams.AuctionID)
}
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, auction)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}
func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Get all auctions
auctionsList := types.Auctions{}
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
auctionsList = append(auctionsList, a)
return false
})
// Encode Results
bz, err := codec.MarshalJSONIndent(keeper.cdc, auctionsList)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}
// query params in the auction store
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Get params
params := keeper.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, params)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}

View File

@ -0,0 +1,134 @@
package keeper_test
import (
"math/rand"
"strings"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction/keeper"
"github.com/kava-labs/kava/x/auction/types"
"github.com/kava-labs/kava/x/cdp"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
custom = "custom"
TestAuctionCount = 10
)
type QuerierTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
auctions types.Auctions
ctx sdk.Context
querier sdk.Querier
}
func (suite *QuerierTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0]
modName := cdp.LiquidatorMacc
// Set up seller account
sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner)
sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000)))
// Initialize genesis accounts
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0),
sellerAcc,
}),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetAuctionKeeper()
// Populate with auctions
randSrc := rand.New(rand.NewSource(int64(1234)))
for j := 0; j < TestAuctionCount; j++ {
lotAmount := simulation.RandIntBetween(randSrc, 10, 100)
id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2")
suite.NoError(err)
auc, found := suite.keeper.GetAuction(suite.ctx, id)
suite.True(found)
suite.auctions = append(suite.auctions, auc)
}
suite.querier = keeper.NewQuerier(suite.keeper)
}
func (suite *QuerierTestSuite) TestQueryAuction() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuction}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.QueryAuctionParams{AuctionID: types.DefaultNextAuctionID}), // get the first auction
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryGetAuction}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes into type Auction
var auction types.Auction
suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auction))
// Check the returned auction
suite.Equal(suite.auctions[0].GetID(), auction.GetID())
suite.Equal(suite.auctions[0].GetInitiator(), auction.GetInitiator())
suite.Equal(suite.auctions[0].GetLot(), auction.GetLot())
suite.Equal(suite.auctions[0].GetBid(), auction.GetBid())
suite.Equal(suite.auctions[0].GetEndTime(), auction.GetEndTime())
}
func (suite *QuerierTestSuite) TestQueryAuctions() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuctions}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryGetAuctions}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes into type Auctions
var auctions types.Auctions
suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auctions))
// Check that each Auction has correct values
if len(auctions) == 0 && len(suite.auctions) != 0 {
suite.FailNow("no auctions returned") // skip the panic from indexing empty slice below
}
for i := 0; i < TestAuctionCount; i++ {
suite.Equal(suite.auctions[i].GetID(), auctions[i].GetID())
suite.Equal(suite.auctions[i].GetInitiator(), auctions[i].GetInitiator())
suite.Equal(suite.auctions[i].GetLot(), auctions[i].GetLot())
suite.Equal(suite.auctions[i].GetBid(), auctions[i].GetBid())
suite.Equal(suite.auctions[i].GetEndTime(), auctions[i].GetEndTime())
}
}
func TestQuerierTestSuite(t *testing.T) {
suite.Run(t, new(QuerierTestSuite))
}

130
x/auction/module.go Normal file
View File

@ -0,0 +1,130 @@
package auction
import (
"encoding/json"
"fmt"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/auction/client/cli"
"github.com/kava-labs/kava/x/auction/client/rest"
"github.com/kava-labs/kava/x/auction/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic implements the sdk.AppModuleBasic interface.
type AppModuleBasic struct{}
// Name returns the module name.
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec registers the module codec.
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis returns the default genesis state.
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the auction module.
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return fmt.Errorf("failed to unmarshal %s genesis state: %w", ModuleName, err)
}
return gs.Validate()
}
// RegisterRESTRoutes registers the REST routes for the auction module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the auction module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns the root query command for the auction module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
keeper Keeper
supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
supplyKeeper: supplyKeeper,
}
}
// RegisterInvariants performs a no-op.
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis performs genesis initialization for the auction module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
EndBlocker(ctx, am.keeper)
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,9 @@
# Concepts
Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system.
* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a surplus auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, surplus auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX.
* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, debt auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA.
* **Surplus Reverse Auction:** Are two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example, collateral auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM.
Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time.

View File

@ -0,0 +1,75 @@
# State
## Parameters and genesis state
`Paramaters` define the rules according to which auctions are run. There is only one active parameter set at any given time. Updates to the parameter set can be made via on-chain parameter update proposals.
```go
// Params governance parameters for auction module
type Params struct {
MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction
MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry.
}
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the auction module to resume.
```go
// GenesisState - auction state that must be provided at genesis
type GenesisState struct {
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` // auctionID that will be used for the next created auction
Params Params `json:"auction_params" yaml:"auction_params"` // auction params
Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` // auctions currently in the store
}
```
## Base types
```go
// Auction is an interface to several types of auction.
type Auction interface {
GetID() uint64
WithID(uint64) Auction
GetEndTime() time.Time
}
// BaseAuction is a common type shared by all Auctions.
type BaseAuction struct {
ID uint64
Initiator string // Module name that starts the auction. Pays out Lot.
Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder.
Bidder sdk.AccAddress // Latest bidder. Receiver of Lot.
Bid sdk.Coin // Coins paid into the auction the bidder.
EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime.
MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after.
}
// SurplusAuction is a forward auction that burns what it receives from bids.
// It is normally used to sell off excess pegged asset acquired by the CDP system.
type SurplusAuction struct {
BaseAuction
}
// DebtAuction is a reverse auction that mints what it pays out.
// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral.
type DebtAuction struct {
BaseAuction
}
// WeightedAddresses is a type for storing some addresses and associated weights.
type WeightedAddresses struct {
Addresses []sdk.AccAddress
Weights []sdk.Int
}
// CollateralAuction is a two phase auction.
// Initially, in forward auction phase, bids can be placed up to a max bid.
// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down.
// Unsold Lot is sent to LotReturns, being divided among the addresses by weight.
// Collateral auctions are normally used to sell off collateral seized from CDPs.
type CollateralAuction struct {
BaseAuction
MaxBid sdk.Coin
LotReturns WeightedAddresses
}
```

View File

@ -0,0 +1,32 @@
# Messages
## Bidding
Users can bid on auctions using the `MsgPlaceBid` message type. All auction types can be bid on using the same message type.
```go
// MsgPlaceBid is the message type used to place a bid on any type of auction.
type MsgPlaceBid struct {
AuctionID uint64
Bidder sdk.AccAddress
Amount sdk.Coin
}
```
**State Modifications:**
* Update bidder if different than previous bidder
* For Surplus auctions:
* Update Bid to msg.Amount
* Return bid coins to previous bidder
* Burn coins equal to the increment in the bid (CurrentBid - PreviousBid)
* For Debt auctions:
* Update Lot amount to msg.Amount
* Return bid coins to previous bidder
* For Collateral auctions:
* Return bid coins to previous bidder
* If in forward phase:
* Update Bid amount to msg.Amount
* If in reverse phase:
* Update Lot amount to msg.Amount
* Extend auction by `BidDuration`, up to `MaxEndTime`

View File

@ -0,0 +1,32 @@
# Events
The `x/auction` module emits the following events:
## Triggered By Other Modules
| Type | Attribute Key | Attribute Value |
|---------------|---------------|---------------------|
| auction_start | auction_id | {auction ID} |
| auction_start | auction_type | {auction type} |
| auction_start | lot_denom | {auction lot denom} |
| auction_start | bid_denom | {auction bid denom} |
## Handlers
### MsgPlaceBid
| Type | Attribute Key | Attribute Value |
|-------------|---------------|--------------------|
| auction_bid | auction_id | {auction ID} |
| auction_bid | bidder | {latest bidder} |
| auction_bid | bid_amount | {coin amount} |
| auction_bid | lot_amount | {coin amount} |
| auction_bid | end_time | {auction end time} |
| message | module | auction |
| message | sender | {sender address} |
## EndBlock
| Type | Attribute Key | Attribute Value |
|---------------|---------------|-----------------|
| auction_close | auction_id | {auction ID} |

View File

@ -0,0 +1,8 @@
# Parameters
The auction module contains the following parameters:
| Key | Type | Example |
| ------------------ | ---------------------- | -----------|
| MaxAuctionDuration | string (time.Duration) | "48h0m0s" |
| BidDuration | string (time.Duration) | "3h0m0s" |

View File

@ -0,0 +1,18 @@
# End Block
At the end of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows:
```go
var expiredAuctions []uint64
k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool {
expiredAuctions = append(expiredAuctions, id)
return false
})
for _, id := range expiredAuctions {
err := k.CloseAuction(ctx, id)
if err != nil {
panic(err)
}
}
```

13
x/auction/spec/README.md Normal file
View File

@ -0,0 +1,13 @@
# `auction`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
6. **[EndBlock](06_end_block.md)**
## Abstract
`x/auction` is an implementation of a Cosmos SDK Module that handles the creation, bidding, and payout of 3 distinct auction types. All auction types implement the `Auction` interface. Each auction type is used at different points during the normal functioning of the CDP system.

266
x/auction/types/auctions.go Normal file
View File

@ -0,0 +1,266 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/supply"
)
// DistantFuture is a very large time value to use as initial the ending time for auctions.
// It is not set to the max time supported. This can cause problems with time comparisons, see https://stackoverflow.com/a/32620397.
// Also amino panics when encoding times ≥ the start of year 10000.
var DistantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)
// Auction is an interface for handling common actions on auctions.
type Auction interface {
GetID() uint64
WithID(uint64) Auction
GetInitiator() string
GetLot() sdk.Coin
GetBidder() sdk.AccAddress
GetBid() sdk.Coin
GetEndTime() time.Time
GetType() string
GetPhase() string
}
// Auctions is a slice of auctions.
type Auctions []Auction
// BaseAuction is a common type shared by all Auctions.
type BaseAuction struct {
ID uint64 `json:"id" yaml:"id"`
Initiator string `json:"initiator" yaml:"initiator"` // Module name that starts the auction. Pays out Lot.
Lot sdk.Coin `json:"lot" yaml:"lot"` // Coins that will paid out by Initiator to the winning bidder.
Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"` // Latest bidder. Receiver of Lot.
Bid sdk.Coin `json:"bid" yaml:"bid"` // Coins paid into the auction the bidder.
HasReceivedBids bool `json:"has_received_bids" yaml:"has_received_bids"` // Whether the auction has received any bids or not.
EndTime time.Time `json:"end_time" yaml:"end_time"` // Current auction closing time. Triggers at the end of the block with time ≥ EndTime.
MaxEndTime time.Time `json:"max_end_time" yaml:"max_end_time"` // Maximum closing time. Auctions can close before this but never after.
}
// GetID is a getter for auction ID.
func (a BaseAuction) GetID() uint64 { return a.ID }
// GetInitiator is a getter for auction Initiator.
func (a BaseAuction) GetInitiator() string { return a.Initiator }
// GetLot is a getter for auction Lot.
func (a BaseAuction) GetLot() sdk.Coin { return a.Lot }
// GetBidder is a getter for auction Bidder.
func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder }
// GetBid is a getter for auction Bid.
func (a BaseAuction) GetBid() sdk.Coin { return a.Bid }
// GetEndTime is a getter for auction end time.
func (a BaseAuction) GetEndTime() time.Time { return a.EndTime }
// GetType returns theauction type. Used to identify auctions in event attributes.
func (a BaseAuction) GetType() string { return "base" }
// Validate verifies that the auction end time is before max end time
func (a BaseAuction) Validate() error {
if a.EndTime.After(a.MaxEndTime) {
return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime)
}
return nil
}
func (a BaseAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
Lot: %s
Bidder: %s
Bid: %s
End Time: %s
Max End Time: %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(),
)
}
// SurplusAuction is a forward auction that burns what it receives from bids.
// It is normally used to sell off excess pegged asset acquired by the CDP system.
type SurplusAuction struct {
BaseAuction `json:"base_auction" yaml:"base_auction"`
}
// WithID returns an auction with the ID set.
func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a }
// GetType returns the auction type. Used to identify auctions in event attributes.
func (a SurplusAuction) GetType() string { return "surplus" }
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
// It is used in genesis initialize the module account correctly.
func (a SurplusAuction) GetModuleAccountCoins() sdk.Coins {
// a.Bid is paid out on bids, so is never stored in the module account
return sdk.NewCoins(a.Lot)
}
// GetPhase returns the direction of a surplus auction, which never changes.
func (a SurplusAuction) GetPhase() string { return "forward" }
// NewSurplusAuction returns a new surplus auction.
func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction {
auction := SurplusAuction{BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(bidDenom, 0),
HasReceivedBids: false, // new auctions don't have any bids
EndTime: endTime,
MaxEndTime: endTime,
}}
return auction
}
// DebtAuction is a reverse auction that mints what it pays out.
// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral.
type DebtAuction struct {
BaseAuction `json:"base_auction" yaml:"base_auction"`
CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"`
}
// WithID returns an auction with the ID set.
func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a }
// GetType returns the auction type. Used to identify auctions in event attributes.
func (a DebtAuction) GetType() string { return "debt" }
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
// It is used in genesis initialize the module account correctly.
func (a DebtAuction) GetModuleAccountCoins() sdk.Coins {
// a.Lot is minted at auction close, so is never stored in the module account
// a.Bid is paid out on bids, so is never stored in the module account
return sdk.NewCoins(a.CorrespondingDebt)
}
// GetPhase returns the direction of a debt auction, which never changes.
func (a DebtAuction) GetPhase() string { return "reverse" }
// NewDebtAuction returns a new debt auction.
func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, endTime time.Time, debt sdk.Coin) DebtAuction {
// Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator)
// Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount.
// This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed.
auction := DebtAuction{
BaseAuction: BaseAuction{
// no ID
Initiator: buyerModAccName,
Lot: initialLot,
Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer.
Bid: bid, // amount that the buyer is buying - doesn't change over course of auction
HasReceivedBids: false, // new auctions don't have any bids
EndTime: endTime,
MaxEndTime: endTime},
CorrespondingDebt: debt,
}
return auction
}
// CollateralAuction is a two phase auction.
// Initially, in forward auction phase, bids can be placed up to a max bid.
// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down.
// Unsold Lot is sent to LotReturns, being divided among the addresses by weight.
// Collateral auctions are normally used to sell off collateral seized from CDPs.
type CollateralAuction struct {
BaseAuction `json:"base_auction" yaml:"base_auction"`
CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"`
MaxBid sdk.Coin `json:"max_bid" yaml:"max_bid"`
LotReturns WeightedAddresses `json:"lot_returns" yaml:"lot_returns"`
}
// WithID returns an auction with the ID set.
func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a }
// GetType returns the auction type. Used to identify auctions in event attributes.
func (a CollateralAuction) GetType() string { return "collateral" }
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
// It is used in genesis initialize the module account correctly.
func (a CollateralAuction) GetModuleAccountCoins() sdk.Coins {
// a.Bid is paid out on bids, so is never stored in the module account
return sdk.NewCoins(a.Lot).Add(sdk.NewCoins(a.CorrespondingDebt))
}
// IsReversePhase returns whether the auction has switched over to reverse phase or not.
// CollateralAuctions initially start in forward phase.
func (a CollateralAuction) IsReversePhase() bool {
return a.Bid.IsEqual(a.MaxBid)
}
// GetPhase returns the direction of a collateral auction.
func (a CollateralAuction) GetPhase() string {
if a.IsReversePhase() {
return "reverse"
}
return "forward"
}
func (a CollateralAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
Lot: %s
Bidder: %s
Bid: %s
End Time: %s
Max End Time: %s
Max Bid %s
LotReturns %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(), a.MaxBid, a.LotReturns,
)
}
// NewCollateralAuction returns a new collateral auction.
func NewCollateralAuction(seller string, lot sdk.Coin, endTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction {
auction := CollateralAuction{
BaseAuction: BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
HasReceivedBids: false, // new auctions don't have any bids
EndTime: endTime,
MaxEndTime: endTime},
CorrespondingDebt: debt,
MaxBid: maxBid,
LotReturns: lotReturns,
}
return auction
}
// WeightedAddresses is a type for storing some addresses and associated weights.
type WeightedAddresses struct {
Addresses []sdk.AccAddress `json:"addresses" yaml:"addresses"`
Weights []sdk.Int `json:"weights" yaml:"weights"`
}
// NewWeightedAddresses returns a new list addresses with weights.
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
if len(addrs) != len(weights) {
return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights")
}
for _, w := range weights {
if w.IsNegative() {
return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount")
}
}
return WeightedAddresses{
Addresses: addrs,
Weights: weights,
}, nil
}

View File

@ -0,0 +1,195 @@
package types
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
const (
TestInitiatorModuleName = "liquidator"
TestLotDenom = "usdx"
TestLotAmount = 100
TestBidDenom = "kava"
TestBidAmount = 20
TestDebtDenom = "debt"
TestDebtAmount1 = 20
TestDebtAmount2 = 15
TestExtraEndTime = 10000
TestAuctionID = 9999123
TestAccAddress1 = "kava1qcfdf69js922qrdr4yaww3ax7gjml6pd39p8lj"
TestAccAddress2 = "kava1pdfav2cjhry9k79nu6r8kgknnjtq6a7rcr0qlr"
)
func TestNewWeightedAddresses(t *testing.T) {
tests := []struct {
name string
addresses []sdk.AccAddress
weights []sdk.Int
expectpass bool
}{
{
"normal",
[]sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
sdk.NewInt(8),
},
true,
},
{
"mismatched",
[]sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
},
false,
},
{
"negativeWeight",
[]sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
sdk.NewInt(-8),
},
false,
},
}
// Run NewWeightedAdresses tests
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Attempt to instantiate new WeightedAddresses
weightedAddresses, err := NewWeightedAddresses(tc.addresses, tc.weights)
if tc.expectpass {
// Confirm there is no error
require.Nil(t, err)
// Check addresses, weights
require.Equal(t, tc.addresses, weightedAddresses.Addresses)
require.Equal(t, tc.weights, weightedAddresses.Weights)
} else {
// Confirm that there is an error
require.NotNil(t, err)
switch tc.name {
case "mismatched":
require.Contains(t, err.Error(), "number of addresses doesn't match number of weights")
case "negativeWeight":
require.Contains(t, err.Error(), "weights contain a negative amount")
default:
// Unexpected error state
t.Fail()
}
}
})
}
}
func TestBaseAuctionGetters(t *testing.T) {
endTime := time.Now().Add(TestExtraEndTime)
// Create a new BaseAuction (via SurplusAuction)
auction := NewSurplusAuction(
TestInitiatorModuleName,
c(TestLotDenom, TestLotAmount),
TestBidDenom, endTime,
)
auctionID := auction.GetID()
auctionBid := auction.GetBid()
auctionLot := auction.GetLot()
auctionEndTime := auction.GetEndTime()
auctionString := auction.String()
require.Equal(t, auction.ID, auctionID)
require.Equal(t, auction.Bid, auctionBid)
require.Equal(t, auction.Lot, auctionLot)
require.Equal(t, auction.EndTime, auctionEndTime)
require.NotNil(t, auctionString)
}
func TestNewSurplusAuction(t *testing.T) {
endTime := time.Now().Add(TestExtraEndTime)
// Create a new SurplusAuction
surplusAuction := NewSurplusAuction(
TestInitiatorModuleName,
c(TestLotDenom, TestLotAmount),
TestBidDenom, endTime,
)
require.Equal(t, surplusAuction.Initiator, TestInitiatorModuleName)
require.Equal(t, surplusAuction.Lot, c(TestLotDenom, TestLotAmount))
require.Equal(t, surplusAuction.Bid, c(TestBidDenom, 0))
require.Equal(t, surplusAuction.EndTime, endTime)
require.Equal(t, surplusAuction.MaxEndTime, endTime)
}
func TestNewDebtAuction(t *testing.T) {
endTime := time.Now().Add(TestExtraEndTime)
// Create a new DebtAuction
debtAuction := NewDebtAuction(
TestInitiatorModuleName,
c(TestBidDenom, TestBidAmount),
c(TestLotDenom, TestLotAmount),
endTime,
c(TestDebtDenom, TestDebtAmount1),
)
require.Equal(t, debtAuction.Initiator, TestInitiatorModuleName)
require.Equal(t, debtAuction.Lot, c(TestLotDenom, TestLotAmount))
require.Equal(t, debtAuction.Bid, c(TestBidDenom, TestBidAmount))
require.Equal(t, debtAuction.EndTime, endTime)
require.Equal(t, debtAuction.MaxEndTime, endTime)
require.Equal(t, debtAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount1))
}
func TestNewCollateralAuction(t *testing.T) {
// Set up WeightedAddresses
addresses := []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
}
weights := []sdk.Int{
sdk.NewInt(6),
sdk.NewInt(8),
}
weightedAddresses, _ := NewWeightedAddresses(addresses, weights)
endTime := time.Now().Add(TestExtraEndTime)
collateralAuction := NewCollateralAuction(
TestInitiatorModuleName,
c(TestLotDenom, TestLotAmount),
endTime,
c(TestBidDenom, TestBidAmount),
weightedAddresses,
c(TestDebtDenom, TestDebtAmount2),
)
require.Equal(t, collateralAuction.BaseAuction.Initiator, TestInitiatorModuleName)
require.Equal(t, collateralAuction.BaseAuction.Lot, c(TestLotDenom, TestLotAmount))
require.Equal(t, collateralAuction.BaseAuction.Bid, c(TestBidDenom, 0))
require.Equal(t, collateralAuction.BaseAuction.EndTime, endTime)
require.Equal(t, collateralAuction.BaseAuction.MaxEndTime, endTime)
require.Equal(t, collateralAuction.MaxBid, c(TestBidDenom, TestBidAmount))
require.Equal(t, collateralAuction.LotReturns, weightedAddresses)
require.Equal(t, collateralAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount2))
}

23
x/auction/types/codec.go Normal file
View File

@ -0,0 +1,23 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
)
// ModuleCdc module level codec
var ModuleCdc = codec.New()
func init() {
RegisterCodec(ModuleCdc)
}
// RegisterCodec registers concrete types on the codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil)
cdc.RegisterInterface((*GenesisAuction)(nil), nil)
cdc.RegisterInterface((*Auction)(nil), nil)
cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil)
cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil)
cdc.RegisterConcrete(CollateralAuction{}, "auction/CollateralAuction", nil)
}

92
x/auction/types/errors.go Normal file
View File

@ -0,0 +1,92 @@
// DONTCOVER
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Error codes specific to auction module
const (
DefaultCodespace sdk.CodespaceType = ModuleName
CodeInvalidInitialAuctionID sdk.CodeType = 1
CodeInvalidModulePermissions sdk.CodeType = 2
CodeUnrecognizedAuctionType sdk.CodeType = 3
CodeAuctionNotFound sdk.CodeType = 4
CodeAuctionHasNotExpired sdk.CodeType = 5
CodeAuctionHasExpired sdk.CodeType = 6
CodeInvalidBidDenom sdk.CodeType = 7
CodeInvalidLotDenom sdk.CodeType = 8
CodeBidTooSmall sdk.CodeType = 9
CodeBidTooLarge sdk.CodeType = 10
CodeLotTooLarge sdk.CodeType = 11
CodeCollateralAuctionIsInReversePhase sdk.CodeType = 12
CodeCollateralAuctionIsInForwardPhase sdk.CodeType = 13
)
// ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set
func ErrInvalidInitialAuctionID(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidInitialAuctionID, fmt.Sprintf("initial auction ID hasn't been set"))
}
// ErrInvalidModulePermissions error for when module doesn't have valid permissions
func ErrInvalidModulePermissions(codespace sdk.CodespaceType, permission string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidModulePermissions, fmt.Sprintf("module does not have required permission '%s'", permission))
}
// ErrUnrecognizedAuctionType error for unrecognized auction type
func ErrUnrecognizedAuctionType(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeUnrecognizedAuctionType, fmt.Sprintf("unrecognized auction type"))
}
// ErrAuctionNotFound error for when an auction is not found
func ErrAuctionNotFound(codespace sdk.CodespaceType, id uint64) sdk.Error {
return sdk.NewError(codespace, CodeAuctionNotFound, fmt.Sprintf("auction %d was not found", id))
}
// ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time
func ErrAuctionHasNotExpired(codespace sdk.CodespaceType, blockTime time.Time, endTime time.Time) sdk.Error {
return sdk.NewError(codespace, CodeAuctionHasNotExpired, fmt.Sprintf("auction can't be closed as curent block time (%v) has not passed auction end time (%v)", blockTime, endTime))
}
// ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding
func ErrAuctionHasExpired(codespace sdk.CodespaceType, id uint64) sdk.Error {
return sdk.NewError(codespace, CodeAuctionHasExpired, fmt.Sprintf("auction %d has closed", id))
}
// ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom
func ErrInvalidBidDenom(codespace sdk.CodespaceType, bidDenom string, auctionBidDenom string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidBidDenom, fmt.Sprintf("bid denom %s doesn't match auction bid denom %s", bidDenom, auctionBidDenom))
}
// ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom
func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLotDenom string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidLotDenom, fmt.Sprintf("lot denom %s doesn't match auction lot denom %s", lotDenom, auctionLotDenom))
}
// ErrBidTooSmall error for when bid is not greater than auction's last bid
func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, lastBid sdk.Coin) sdk.Error {
return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's last bid %s", bid.String(), lastBid.String()))
}
// ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid
func ErrBidTooLarge(codespace sdk.CodespaceType, bid sdk.Coin, maxBid sdk.Coin) sdk.Error {
return sdk.NewError(codespace, CodeBidTooLarge, fmt.Sprintf("bid %s is greater than auction's max bid %s", bid.String(), maxBid.String()))
}
// ErrLotTooLarge error for when lot is not smaller than auction's last lot
func ErrLotTooLarge(codespace sdk.CodespaceType, lot sdk.Coin, lastLot sdk.Coin) sdk.Error {
return sdk.NewError(codespace, CodeLotTooLarge, fmt.Sprintf("lot %s is not less than auction's last lot %s", lot.String(), lastLot.String()))
}
// ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase
func ErrCollateralAuctionIsInReversePhase(codespace sdk.CodespaceType, id uint64) sdk.Error {
return sdk.NewError(codespace, CodeCollateralAuctionIsInReversePhase, fmt.Sprintf("invalid bid - auction %d is in reverse phase", id))
}
// ErrCollateralAuctionIsInForwardPhase error for when attempting to place a reverse bid on a collateral auction in forward phase
func ErrCollateralAuctionIsInForwardPhase(codespace sdk.CodespaceType, id uint64) sdk.Error {
return sdk.NewError(codespace, CodeCollateralAuctionIsInForwardPhase, fmt.Sprintf("invalid bid - auction %d is in forward phase", id))
}

18
x/auction/types/events.go Normal file
View File

@ -0,0 +1,18 @@
package types
// Events for auction module
const (
EventTypeAuctionStart = "auction_start"
EventTypeAuctionBid = "auction_bid"
EventTypeAuctionClose = "auction_close"
AttributeValueCategory = ModuleName
AttributeKeyAuctionID = "auction_id"
AttributeKeyAuctionType = "auction_type"
AttributeKeyBidder = "bidder"
AttributeKeyBidDenom = "bid_denom"
AttributeKeyLotDenom = "lot_denom"
AttributeKeyBidAmount = "bid_amount"
AttributeKeyLotAmount = "lot_amount"
AttributeKeyEndTime = "end_time"
)

View File

@ -0,0 +1,19 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// SupplyKeeper defines the expected supply Keeper
type SupplyKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI
SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
}

View File

@ -0,0 +1,83 @@
package types
import (
"bytes"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// DefaultNextAuctionID is the starting poiint for auction IDs.
const DefaultNextAuctionID uint64 = 1
// GenesisAuction is an interface that extends the auction interface to add functionality needed for initializing auctions from genesis.
type GenesisAuction interface {
Auction
GetModuleAccountCoins() sdk.Coins
Validate() error
}
// GenesisAuctions is a slice of genesis auctions.
type GenesisAuctions []GenesisAuction
// GenesisState is auction state that must be provided at chain genesis.
type GenesisState struct {
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"`
Params Params `json:"params" yaml:"params"`
Auctions GenesisAuctions `json:"auctions" yaml:"auctions"`
}
// NewGenesisState returns a new genesis state object for auctions module.
func NewGenesisState(nextID uint64, ap Params, ga GenesisAuctions) GenesisState {
return GenesisState{
NextAuctionID: nextID,
Params: ap,
Auctions: ga,
}
}
// DefaultGenesisState returns the default genesis state for auction module.
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultNextAuctionID,
DefaultParams(),
GenesisAuctions{},
)
}
// Equal checks whether two GenesisState structs are equivalent.
func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty.
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}
// 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[uint64]bool{}
for _, a := range gs.Auctions {
if err := a.Validate(); err != nil {
return fmt.Errorf("found invalid auction: %w", err)
}
if ids[a.GetID()] {
return fmt.Errorf("found duplicate auction ID (%d)", a.GetID())
}
ids[a.GetID()] = true
if a.GetID() >= gs.NextAuctionID {
return fmt.Errorf("found auction ID >= the nextAuctionID (%d >= %d)", a.GetID(), gs.NextAuctionID)
}
}
return nil
}

View File

@ -0,0 +1,46 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
var testCoin = sdk.NewInt64Coin("test", 20)
func TestGenesisState_Validate(t *testing.T) {
testCases := []struct {
name string
nextID uint64
auctions GenesisAuctions
expectPass bool
}{
{"default", DefaultGenesisState().NextAuctionID, DefaultGenesisState().Auctions, true},
{"invalid next ID", 54, GenesisAuctions{SurplusAuction{BaseAuction{ID: 105}}}, false},
{
"repeated ID",
1000,
GenesisAuctions{
SurplusAuction{BaseAuction{ID: 105}},
DebtAuction{BaseAuction{ID: 105}, testCoin},
},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gs := NewGenesisState(tc.nextID, DefaultParams(), tc.auctions)
err := gs.Validate()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}

55
x/auction/types/keys.go Normal file
View File

@ -0,0 +1,55 @@
package types
import (
"encoding/binary"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "auction"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
// QuerierRoute route used for abci queries
QuerierRoute = ModuleName
)
// Key prefixes
var (
AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions
AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index
NextAuctionIDKey = []byte{0x02} // key for the next auction id
)
// GetAuctionKey returns the bytes of an auction key
func GetAuctionKey(auctionID uint64) []byte {
return Uint64ToBytes(auctionID)
}
// GetAuctionByTimeKey returns the key for iterating auctions by time
func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte {
return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...)
}
// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys.
func Uint64ToBytes(id uint64) []byte {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, uint64(id))
return bz
}
// Uint64FromBytes converts some fixed length bytes back into a uint64.
func Uint64FromBytes(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}

53
x/auction/types/msg.go Normal file
View File

@ -0,0 +1,53 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgPlaceBid{}
// MsgPlaceBid is the message type used to place a bid on any type of auction.
type MsgPlaceBid struct {
AuctionID uint64 `json:"auction_id" yaml:"auction_id"`
Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"`
Amount sdk.Coin `json:"amount" yaml:"amount"` // The new bid or lot to be set on the auction.
}
// NewMsgPlaceBid returns a new MsgPlaceBid.
func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, amt sdk.Coin) MsgPlaceBid {
return MsgPlaceBid{
AuctionID: auctionID,
Bidder: bidder,
Amount: amt,
}
}
// Route return the message type used for routing the message.
func (msg MsgPlaceBid) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgPlaceBid) Type() string { return "place_bid" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgPlaceBid) ValidateBasic() sdk.Error {
if msg.Bidder.Empty() {
return sdk.ErrInvalidAddress("invalid (empty) bidder address")
}
if !msg.Amount.IsValid() {
return sdk.ErrInvalidCoins(fmt.Sprintf("invalid bid amount: %s", msg.Amount))
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgPlaceBid) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgPlaceBid) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Bidder}
}

View File

@ -0,0 +1,42 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
addr := sdk.AccAddress([]byte("someName"))
tests := []struct {
name string
msg MsgPlaceBid
expectPass bool
}{
{"normal",
NewMsgPlaceBid(0, addr, c("token", 10)),
true},
{"emptyAddr",
NewMsgPlaceBid(0, sdk.AccAddress{}, c("token", 10)),
false},
{"negativeAmount",
NewMsgPlaceBid(0, addr, sdk.Coin{Denom: "token", Amount: sdk.NewInt(-10)}),
false},
{"zeroAmount",
NewMsgPlaceBid(0, addr, c("token", 0)),
true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.expectPass {
require.NoError(t, tc.msg.ValidateBasic())
} else {
require.Error(t, tc.msg.ValidateBasic())
}
})
}
}
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }

90
x/auction/types/params.go Normal file
View File

@ -0,0 +1,90 @@
package types
import (
"bytes"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Defaults for auction params
const (
// DefaultMaxAuctionDuration max length of auction
DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour
// DefaultBidDuration how long an auction gets extended when someone bids
DefaultBidDuration time.Duration = 1 * time.Hour
)
// Parameter keys
var (
// ParamStoreKeyParams Param store key for auction params
KeyAuctionBidDuration = []byte("BidDuration")
KeyAuctionDuration = []byte("MaxAuctionDuration")
)
var _ subspace.ParamSet = &Params{}
// Params is the governance parameters for the auction module.
type Params struct {
MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction
BidDuration time.Duration `json:"bid_duration" yaml:"bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry.
}
// NewParams returns a new Params object.
func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params {
return Params{
MaxAuctionDuration: maxAuctionDuration,
BidDuration: bidDuration,
}
}
// DefaultParams returns the default parameters for auctions.
func DefaultParams() Params {
return NewParams(
DefaultMaxAuctionDuration,
DefaultBidDuration,
)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs.
func (p *Params) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
{Key: KeyAuctionBidDuration, Value: &p.BidDuration},
{Key: KeyAuctionDuration, Value: &p.MaxAuctionDuration},
}
}
// Equal returns a boolean determining if two Params types are identical.
func (p Params) Equal(p2 Params) bool {
bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p)
bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p2)
return bytes.Equal(bz1, bz2)
}
// String implements stringer interface
func (p Params) String() string {
return fmt.Sprintf(`Auction Params:
Max Auction Duration: %s
Bid Duration: %s`, p.MaxAuctionDuration, p.BidDuration)
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if p.BidDuration < 0 {
return sdk.ErrInternal("bid duration cannot be negative")
}
if p.MaxAuctionDuration < 0 {
return sdk.ErrInternal("max auction duration cannot be negative")
}
if p.BidDuration > p.MaxAuctionDuration {
return sdk.ErrInternal("bid duration param cannot be larger than max auction duration")
}
return nil
}

View File

@ -0,0 +1,38 @@
package types
import (
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestParams_Validate(t *testing.T) {
type fields struct {
}
testCases := []struct {
name string
MaxAuctionDuration time.Duration
BidDuration time.Duration
expectErr bool
}{
{"normal", 24 * time.Hour, 1 * time.Hour, false},
{"negativeBid", 24 * time.Hour, -1 * time.Hour, true},
{"negativeAuction", -24 * time.Hour, 1 * time.Hour, true},
{"bid>auction", 1 * time.Hour, 24 * time.Hour, true},
{"zeros", 0, 0, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := Params{
MaxAuctionDuration: tc.MaxAuctionDuration,
BidDuration: tc.BidDuration,
}
err := p.Validate()
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@ -0,0 +1,46 @@
package types
const (
// QueryGetAuction is the query path for querying one auction
QueryGetAuction = "auction"
// QueryGetAuctions is the query path for querying all auctions
QueryGetAuctions = "auctions"
// QueryGetParams is the query path for querying the global auction params
QueryGetParams = "params"
)
// QueryAuctionParams params for query /auction/auction
type QueryAuctionParams struct {
AuctionID uint64
}
// QueryAllAuctionParams is the params for an auctions query
type QueryAllAuctionParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
}
// NewQueryAllAuctionParams creates a new QueryAllAuctionParams
func NewQueryAllAuctionParams(page int, limit int) QueryAllAuctionParams {
return QueryAllAuctionParams{
Page: page,
Limit: limit,
}
}
// AuctionWithPhase augmented type for collateral auctions which includes auction phase for querying
type AuctionWithPhase struct {
Auction Auction `json:"auction" yaml:"auction"`
Type string `json:"type" yaml:"type"`
Phase string `json:"phase" yaml:"phase"`
}
// NewAuctionWithPhase returns new AuctionWithPhase
func NewAuctionWithPhase(a Auction) AuctionWithPhase {
return AuctionWithPhase{
Auction: a,
Type: a.GetType(),
Phase: a.GetPhase(),
}
}

47
x/cdp/abci.go Normal file
View File

@ -0,0 +1,47 @@
package cdp
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// BeginBlocker compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
}
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
for _, cp := range params.CollateralParams {
for _, dp := range params.DebtParams {
k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed)
}
err := k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio)
if err != nil {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
EventTypeBeginBlockerFatal,
sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)),
sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)),
),
)
}
}
err := k.RunSurplusAndDebtAuctions(ctx)
if err != nil {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
EventTypeBeginBlockerFatal,
sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)),
sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)),
),
)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
return
}

181
x/cdp/abci_test.go Normal file
View File

@ -0,0 +1,181 @@
package cdp_test
import (
"math/rand"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
type ModuleTestSuite struct {
suite.Suite
keeper cdp.Keeper
addrs []sdk.AccAddress
app app.TestApp
cdps cdp.CDPs
ctx sdk.Context
liquidations liquidationTracker
}
type liquidationTracker struct {
xrp []uint64
btc []uint64
debt int64
}
func (suite *ModuleTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
coins := []sdk.Coins{}
tracker := liquidationTracker{}
for j := 0; j < 100; j++ {
coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000)))
}
_, addrs := app.GeneratePrivKeyAddressPairs(100)
authGS := app.NewAuthGenState(
addrs, coins)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetCDPKeeper()
suite.cdps = cdp.CDPs{}
suite.addrs = addrs
suite.liquidations = tracker
}
func (suite *ModuleTestSuite) createCdps() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
cdps := make(cdp.CDPs, 100)
_, addrs := app.GeneratePrivKeyAddressPairs(100)
coins := []sdk.Coins{}
tracker := liquidationTracker{}
for j := 0; j < 100; j++ {
coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000)))
}
authGS := app.NewAuthGenState(
addrs, coins)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetCDPKeeper()
for j := 0; j < 100; j++ {
collateral := "xrp"
amount := 10000000000
debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000)
if j%2 == 0 {
collateral = "btc"
amount = 100000000
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000)
if debt >= 4000000000 {
tracker.btc = append(tracker.btc, uint64(j+1))
tracker.debt += int64(debt)
}
} else {
if debt >= 1000000000 {
tracker.xrp = append(tracker.xrp, uint64(j+1))
tracker.debt += int64(debt)
}
}
suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))))
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
}
suite.cdps = cdps
suite.addrs = addrs
suite.liquidations = tracker
}
func (suite *ModuleTestSuite) setPrice(price sdk.Dec, market string) {
pfKeeper := suite.app.GetPriceFeedKeeper()
pfKeeper.SetPrice(suite.ctx, sdk.AccAddress{}, market, price, suite.ctx.BlockTime().Add(time.Hour*3))
err := pfKeeper.SetCurrentPrices(suite.ctx, market)
suite.NoError(err)
pp, err := pfKeeper.GetCurrentPrice(suite.ctx, market)
suite.NoError(err)
suite.Equal(price, pp.Price)
}
func (suite *ModuleTestSuite) TestBeginBlock() {
suite.createCdps()
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
originalXrpCollateral := acc.GetCoins().AmountOf("xrp")
suite.setPrice(d("0.2"), "xrp:usd")
cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
finalXrpCollateral := acc.GetCoins().AmountOf("xrp")
seizedXrpCollateral := originalXrpCollateral.Sub(finalXrpCollateral)
xrpLiquidations := int(seizedXrpCollateral.Quo(i(10000000000)).Int64())
suite.Equal(len(suite.liquidations.xrp), xrpLiquidations)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
originalBtcCollateral := acc.GetCoins().AmountOf("btc")
suite.setPrice(d("6000"), "btc:usd")
cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
finalBtcCollateral := acc.GetCoins().AmountOf("btc")
seizedBtcCollateral := originalBtcCollateral.Sub(finalBtcCollateral)
btcLiquidations := int(seizedBtcCollateral.Quo(i(100000000)).Int64())
suite.Equal(len(suite.liquidations.btc), btcLiquidations)
acc = sk.GetModuleAccount(suite.ctx, auction.ModuleName)
suite.Equal(suite.liquidations.debt, acc.GetCoins().AmountOf("debt").Int64())
}
func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], cs(c("xrp", 10000000000)), cs(c("usdx", 1000000000)))
suite.NoError(err)
suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
previousBlockTime, _ := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.Equal(i(1000000000), suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx"))
sk := suite.app.GetSupplyKeeper()
cdpMacc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
suite.Equal(i(1000000000), cdpMacc.GetCoins().AmountOf("debt"))
for i := 0; i < 100; i++ {
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 6))
cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper)
}
cdpMacc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
suite.Equal(i(1000000900), (cdpMacc.GetCoins().AmountOf("debt")))
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
timeElapsed := sdk.NewInt(suite.ctx.BlockTime().Unix() - previousBlockTime.Unix())
fees := suite.keeper.CalculateFees(suite.ctx, cdp.Principal, timeElapsed, "xrp")
suite.Equal(i(928), fees.AmountOf("usdx"))
err = suite.keeper.SeizeCollateral(suite.ctx, cdp)
suite.NoError(err)
}
func TestModuleTestSuite(t *testing.T) {
suite.Run(t, new(ModuleTestSuite))
}

169
x/cdp/alias.go Normal file
View File

@ -0,0 +1,169 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/cdp/types/
// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper/
package cdp
import (
"github.com/kava-labs/kava/x/cdp/keeper"
"github.com/kava-labs/kava/x/cdp/types"
)
const (
DefaultCodespace = types.DefaultCodespace
CodeCdpAlreadyExists = types.CodeCdpAlreadyExists
CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid
CodeCollateralNotSupported = types.CodeCollateralNotSupported
CodeDebtNotSupported = types.CodeDebtNotSupported
CodeExceedsDebtLimit = types.CodeExceedsDebtLimit
CodeInvalidCollateralRatio = types.CodeInvalidCollateralRatio
CodeCdpNotFound = types.CodeCdpNotFound
CodeDepositNotFound = types.CodeDepositNotFound
CodeInvalidDepositDenom = types.CodeInvalidDepositDenom
CodeInvalidPaymentDenom = types.CodeInvalidPaymentDenom
CodeDepositNotAvailable = types.CodeDepositNotAvailable
CodeInvalidCollateralDenom = types.CodeInvalidCollateralDenom
CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount
CodeCdpNotAvailable = types.CodeCdpNotAvailable
CodeBelowDebtFloor = types.CodeBelowDebtFloor
CodePaymentExceedsDebt = types.CodePaymentExceedsDebt
CodeLoadingAugmentedCDP = types.CodeLoadingAugmentedCDP
EventTypeCreateCdp = types.EventTypeCreateCdp
EventTypeCdpDeposit = types.EventTypeCdpDeposit
EventTypeCdpDraw = types.EventTypeCdpDraw
EventTypeCdpRepay = types.EventTypeCdpRepay
EventTypeCdpClose = types.EventTypeCdpClose
EventTypeCdpWithdrawal = types.EventTypeCdpWithdrawal
EventTypeCdpLiquidation = types.EventTypeCdpLiquidation
EventTypeBeginBlockerFatal = types.EventTypeBeginBlockerFatal
AttributeKeyCdpID = types.AttributeKeyCdpID
AttributeKeyDepositor = types.AttributeKeyDepositor
AttributeValueCategory = types.AttributeValueCategory
AttributeKeyError = types.AttributeKeyError
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
LiquidatorMacc = types.LiquidatorMacc
QueryGetCdp = types.QueryGetCdp
QueryGetCdps = types.QueryGetCdps
QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization
QueryGetParams = types.QueryGetParams
RestOwner = types.RestOwner
RestCollateralDenom = types.RestCollateralDenom
RestRatio = types.RestRatio
)
var (
// functions aliases
NewCDP = types.NewCDP
RegisterCodec = types.RegisterCodec
NewDeposit = types.NewDeposit
ErrCdpAlreadyExists = types.ErrCdpAlreadyExists
ErrInvalidCollateralLength = types.ErrInvalidCollateralLength
ErrCollateralNotSupported = types.ErrCollateralNotSupported
ErrDebtNotSupported = types.ErrDebtNotSupported
ErrExceedsDebtLimit = types.ErrExceedsDebtLimit
ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio
ErrCdpNotFound = types.ErrCdpNotFound
ErrDepositNotFound = types.ErrDepositNotFound
ErrInvalidDepositDenom = types.ErrInvalidDepositDenom
ErrInvalidPaymentDenom = types.ErrInvalidPaymentDenom
ErrDepositNotAvailable = types.ErrDepositNotAvailable
ErrInvalidCollateralDenom = types.ErrInvalidCollateralDenom
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt
ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP
DefaultGenesisState = types.DefaultGenesisState
GetCdpIDBytes = types.GetCdpIDBytes
GetCdpIDFromBytes = types.GetCdpIDFromBytes
CdpKey = types.CdpKey
SplitCdpKey = types.SplitCdpKey
DenomIterKey = types.DenomIterKey
SplitDenomIterKey = types.SplitDenomIterKey
DepositKey = types.DepositKey
SplitDepositKey = types.SplitDepositKey
DepositIterKey = types.DepositIterKey
SplitDepositIterKey = types.SplitDepositIterKey
CollateralRatioBytes = types.CollateralRatioBytes
CollateralRatioKey = types.CollateralRatioKey
SplitCollateralRatioKey = types.SplitCollateralRatioKey
CollateralRatioIterKey = types.CollateralRatioIterKey
SplitCollateralRatioIterKey = types.SplitCollateralRatioIterKey
NewMsgCreateCDP = types.NewMsgCreateCDP
NewMsgDeposit = types.NewMsgDeposit
NewMsgWithdraw = types.NewMsgWithdraw
NewMsgDrawDebt = types.NewMsgDrawDebt
NewMsgRepayDebt = types.NewMsgRepayDebt
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewQueryCdpsParams = types.NewQueryCdpsParams
NewQueryCdpParams = types.NewQueryCdpParams
NewQueryCdpsByRatioParams = types.NewQueryCdpsByRatioParams
ValidSortableDec = types.ValidSortableDec
SortableDecBytes = types.SortableDecBytes
ParseDecBytes = types.ParseDecBytes
RelativePow = types.RelativePow
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
CdpIDKeyPrefix = types.CdpIDKeyPrefix
CdpKeyPrefix = types.CdpKeyPrefix
CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix
CdpIDKey = types.CdpIDKey
DebtDenomKey = types.DebtDenomKey
GovDenomKey = types.GovDenomKey
DepositKeyPrefix = types.DepositKeyPrefix
PrincipalKeyPrefix = types.PrincipalKeyPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
KeyCollateralParams = types.KeyCollateralParams
KeyDebtParams = types.KeyDebtParams
KeyCircuitBreaker = types.KeyCircuitBreaker
KeyDebtThreshold = types.KeyDebtThreshold
KeySurplusThreshold = types.KeySurplusThreshold
DefaultGlobalDebt = types.DefaultGlobalDebt
DefaultCircuitBreaker = types.DefaultCircuitBreaker
DefaultCollateralParams = types.DefaultCollateralParams
DefaultDebtParams = types.DefaultDebtParams
DefaultCdpStartingID = types.DefaultCdpStartingID
DefaultDebtDenom = types.DefaultDebtDenom
DefaultGovDenom = types.DefaultGovDenom
DefaultSurplusThreshold = types.DefaultSurplusThreshold
DefaultDebtThreshold = types.DefaultDebtThreshold
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
MaxSortableDec = types.MaxSortableDec
)
type (
CDP = types.CDP
CDPs = types.CDPs
AugmentedCDP = types.AugmentedCDP
AugmentedCDPs = types.AugmentedCDPs
Deposit = types.Deposit
Deposits = types.Deposits
SupplyKeeper = types.SupplyKeeper
PricefeedKeeper = types.PricefeedKeeper
GenesisState = types.GenesisState
MsgCreateCDP = types.MsgCreateCDP
MsgDeposit = types.MsgDeposit
MsgWithdraw = types.MsgWithdraw
MsgDrawDebt = types.MsgDrawDebt
MsgRepayDebt = types.MsgRepayDebt
Params = types.Params
CollateralParam = types.CollateralParam
CollateralParams = types.CollateralParams
DebtParam = types.DebtParam
DebtParams = types.DebtParams
QueryCdpsParams = types.QueryCdpsParams
QueryCdpParams = types.QueryCdpParams
QueryCdpsByRatioParams = types.QueryCdpsByRatioParams
Keeper = keeper.Keeper
)

227
x/cdp/client/cli/query.go Normal file
View File

@ -0,0 +1,227 @@
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/kava-labs/kava/x/cdp/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
cdpQueryCmd := &cobra.Command{
Use: "cdp",
Short: "Querying commands for the cdp module",
}
cdpQueryCmd.AddCommand(client.GetCommands(
QueryCdpCmd(queryRoute, cdc),
QueryCdpsByDenomCmd(queryRoute, cdc),
QueryCdpsByDenomAndRatioCmd(queryRoute, cdc),
QueryCdpDepositsCmd(queryRoute, cdc),
QueryParamsCmd(queryRoute, cdc),
)...)
return cdpQueryCmd
}
// QueryCdpCmd returns the command handler for querying a particular cdp
func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "cdp [owner-addr] [collateral-name]",
Short: "get info about a cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Get a CDP by the owner address and the collateral name.
Example:
$ %s query %s cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
bz, err := cdc.MarshalJSON(types.QueryCdpParams{
CollateralDenom: args[1],
Owner: ownerAddress,
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdp)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var cdp types.AugmentedCDP
cdc.MustUnmarshalJSON(res, &cdp)
return cliCtx.PrintOutput(cdp)
},
}
}
// QueryCdpsByDenomCmd returns the command handler for querying cdps for a collateral type
func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "cdps [collateral-name]",
Short: "query CDPs by collateral",
Long: strings.TrimSpace(
fmt.Sprintf(`List all CDPs collateralized with the specified asset.
Example:
$ %s query %s cdps uatom
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralDenom: args[0]})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var cdps types.AugmentedCDPs
cdc.MustUnmarshalJSON(res, &cdps)
return cliCtx.PrintOutput(cdps)
},
}
}
// QueryCdpsByDenomAndRatioCmd returns the command handler for querying cdps
// that are under the specified collateral ratio
func QueryCdpsByDenomAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "cdps-by-ratio [collateral-name] [collateralization-ratio]",
Short: "get cdps under a collateralization ratio",
Long: strings.TrimSpace(
fmt.Sprintf(`List all CDPs under a specified collateralization ratio.
Collateralization ratio is: collateral * price / debt.
Example:
$ %s query %s cdps-by-ratio uatom 1.5
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ratio, errSdk := sdk.NewDecFromStr(args[1])
if errSdk != nil {
return fmt.Errorf(errSdk.Error())
}
bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{
CollateralDenom: args[0],
Ratio: ratio,
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpsByCollateralization)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var cdps types.AugmentedCDPs
cdc.MustUnmarshalJSON(res, &cdps)
return cliCtx.PrintOutput(cdps)
},
}
}
// QueryCdpDepositsCmd returns the command handler for querying the deposits of a particular cdp
func QueryCdpDepositsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposits [owner-addr] [collateral-name]",
Short: "get deposits for a cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Get the deposits of a CDP.
Example:
$ %s query %s deposits kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
bz, err := cdc.MarshalJSON(types.QueryCdpParams{
CollateralDenom: args[1],
Owner: ownerAddress,
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpDeposits)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var deposits types.Deposits
cdc.MustUnmarshalJSON(res, &deposits)
return cliCtx.PrintOutput(deposits)
},
}
}
// QueryParamsCmd returns the command handler for cdp parameter querying
func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the cdp module parameters",
Long: "Get the current global cdp module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, _, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
// Decode and print results
var out types.Params
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

198
x/cdp/client/cli/tx.go Normal file
View File

@ -0,0 +1,198 @@
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/cdp/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
cdpTxCmd := &cobra.Command{
Use: "cdp",
Short: "cdp transactions subcommands",
}
cdpTxCmd.AddCommand(client.PostCommands(
GetCmdCreateCdp(cdc),
GetCmdDeposit(cdc),
GetCmdWithdraw(cdc),
GetCmdDraw(cdc),
GetCmdRepay(cdc),
)...)
return cdpTxCmd
}
// GetCmdCreateCdp returns the command handler for creating a cdp
func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "create [collateral] [debt]",
Short: "create a new cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Create a new cdp, depositing some collateral and drawing some debt.
Example:
$ %s tx %s create 10000000uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[0])
if err != nil {
return err
}
debt, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
msg := types.NewMsgCreateCDP(cliCtx.GetFromAddress(), collateral, debt)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdDeposit cli command for depositing to a cdp.
func GetCmdDeposit(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposit [owner-addr] [collateral]",
Short: "deposit collateral to an existing cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Add collateral to an existing cdp.
Example:
$ %s tx %s deposit kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgDeposit(owner, cliCtx.GetFromAddress(), collateral)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdWithdraw cli command for withdrawing from a cdp.
func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "withdraw [owner-addr] [collateral]",
Short: "withdraw collateral from an existing cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Remove collateral from an existing cdp.
Example:
$ %s tx %s withdraw kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgWithdraw(owner, cliCtx.GetFromAddress(), collateral)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdDraw cli command for depositing to a cdp.
func GetCmdDraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "draw [collateral-name] [debt]",
Short: "draw debt off an existing cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Create debt in an existing cdp and send the newly minted asset to your account.
Example:
$ %s tx %s draw uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
debt, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
msg := types.NewMsgDrawDebt(cliCtx.GetFromAddress(), args[0], debt)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdRepay cli command for depositing to a cdp.
func GetCmdRepay(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "repay [collateral-name] [debt]",
Short: "repay debt to an existing cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Cancel out debt in an existing cdp.
Example:
$ %s tx %s repay uatom 1000usdx --from myKeyName
`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
payment, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
msg := types.NewMsgRepayDebt(cliCtx.GetFromAddress(), args[0], payment)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

183
x/cdp/client/rest/query.go Normal file
View File

@ -0,0 +1,183 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/cdp/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdp/parameters", getParamsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/denom/{%s}", types.RestCollateralDenom), queryCdpsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/ratio/{%s}/{%s}", types.RestCollateralDenom, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/cdp/deposits/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpDepositsHandlerFn(cliCtx)).Methods("GET")
}
func queryCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
ownerBech32 := vars[types.RestOwner]
collateralDenom := vars[types.RestCollateralDenom]
owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
params := types.NewQueryCdpParams(owner, collateralDenom)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdp), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
collateralDenom := vars[types.RestCollateralDenom]
params := types.NewQueryCdpsParams(collateralDenom)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpsByRatioHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
collateralDenom := vars[types.RestCollateralDenom]
ratioStr := vars[types.RestRatio]
ratioDec, sdkError := sdk.NewDecFromStr(ratioStr)
if sdkError != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, sdkError.Error())
return
}
params := types.NewQueryCdpsByRatioParams(collateralDenom, ratioDec)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdpsByCollateralization), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpDepositsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
ownerBech32 := vars[types.RestOwner]
collateralDenom := vars[types.RestCollateralDenom]
owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
params := types.NewQueryCdpDeposits(owner, collateralDenom)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdpDeposits), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Get the params
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetParams), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Return the params
rest.PostProcessResponse(w, cliCtx, res)
}
}

55
x/cdp/client/rest/rest.go Normal file
View File

@ -0,0 +1,55 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}
// PostCdpReq defines the properties of cdp request's body.
type PostCdpReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
}
// PostDepositReq defines the properties of cdp request's body.
type PostDepositReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// PostWithdrawalReq defines the properties of cdp request's body.
type PostWithdrawalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// PostDrawReq defines the properties of cdp request's body.
type PostDrawReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
}
// PostRepayReq defines the properties of cdp request's body.
type PostRepayReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
Payment sdk.Coins `json:"payment" yaml:"payment"`
}

Some files were not shown because too many files have changed in this diff Show More