Squash merge swap-acceptance branch (#956)

* add failing acceptance test for a user depositing into a pool

* implement GetAccount test helper

* implement swap.MsgDeposit for creating and adding liquidity to a pool

* update aliases, add event types, and fix typo/compiler errors in handler
test

* use only aliases names in handler test (don't use swap types -- ensures
we have run aliasgen), add assertion for even type message

* implement account and module account balance checks in handler test

* fill out handler assertions for testing keeper state and events

* update signed json representation and register swap/MsgDeposit for
proper encoding

* fill out boilerplate to get handler test to compile

* alias gen for pool

* add handling of message type; fill in deposit keeper method for
succesful compile; noop but test assertions now run up to module acc not
nil check

* add module account permissions for swap module -- fixes module account
creation; pass account keeper and supply keeper into swap keeper to
allow the ability to work with user and module accounts

* implement create pool logic for msg deposit; allows creation of a of new
pool, checking params to see if it is allowed.  Initi shares are set,
and senders number of shares are stored

* Swap migrations scaffolding (#925)

* swap module scaffolding

* global swap fee

* can't think of a reason for begin blocker; removing for abci.go for now;

* test pair types; refactor pair name logic; simplify pairs validation and
fix stack overflow error

* check comparison

* use test package

* init swap module genesis

* add basic marshall tests

* remove reward apy from pairs

* fix integration helpers

* use max swap fee constant; fix validation of swap fee; add tests to
cover param validation and param set setup

* use noerror over nil

* start genesis tests

* test param set validation mirrors param validation

* add genesis tests

* remove print statement

* add subtests for genesis test cases; add extra querier test for unknown
route; add keeper params testing

* add spec

* update swagger

* find replace hard -> swap in comments

* remove unused method

* rename pairs to allowed pools; pool is more commonly used, and
allowedPool makes it more clear what swap parameter is for.  In
addition, we won't conflict with Pool data structure for storing a
created pool in the store.

* remove generated link

* missed spec rename

* validate token order for allowed pools

* fix swagger

* json should be snakecase; change allowedPools to allowed_pools

* add legacy types

* add swap genesis to v0_15 migration

* add legacy types

* add swap genesis to v0_15 migration

* migration revisions

Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>

* keeper todos

* update keeper tests

* type todos

* update types tests

* tx deposit cli cmd

* tx deposit rest

* Swap module simulation scaffolding (#924)

* sims scaffolding

* add noop operation

* genesis revisions

* add param changes

* mvoe persistance methods to main keeper file, consolidate tests

* make helper methods private. they are tested via deposit method, and
unit testing them would make test suite brittle and refactoring
difficult

* use more clear coin variables

* code 1 is reserved, use code 2 and sequence all errors

* remove todo

* Implement deadline for swap module module message. This is implemented in
handler with a interface to easily apply to it to all messages, and
separate msg validation concerns from the keeper

* move allowed pools to params -- let pool and pool_test focus on pool domain logic, not
parameter & governance concerns

* update alias

* add unitless implementatin of constant product liquidity pool to
isolate and enapsulate liquidity logic.  Swap methods interfaces are
added, but implementation not yet added

* nits and todos

* add ErrInvalidPool

* add tests for edge cases around pool depletion; add explicit panic for
edge case that results in a pool reserve being zero; handle pool
reinitialization if it is empty

* touch up comments and flush out the rest of assertions

* add data structures for keeper state storage separate from pool domain
objects, and improve structure for easier querying

* rename pool name to pool key for events

* add support for a denominated pool that uses sdk.Coins and sdk.Coin
arguments, keeping tracking of the units in the base pool.  This gives
nice separation between pool logic, and coin/denom logic

* refactor keeper to use new records for storage, and implement pool
deposit using the denominated pool

* address previous PR comment - reminder for migration if changing
account permissions

* msg deposit should validate that denoms are not equal

* add godoc comments

* golint and some poolName -> poolID cleanup

* implement adding liquidity to an existing pool

* hardcode pools in sims

* touch up comment

* withdraw keeper logic

* withdraw type updates

* add withdraw msg tx handler

* initial withdraw test

* fix panic

* use new denominated pool with existing shares

* fix: check args on deposit cmd

* add slippage limit check for depositing to an existing pool

* send coins just before event emission

* check liquidity returned is greater than zero for both coins; ensure
returned number of shares are greater than zero

* add deadline to msgwithdraw

* register msgwithdraw

* scaffold msgwithdraw types test

* register the correct msg

* modify swap functions to also return the amount paid for the pool swap
fee.  This will be used to calculate slippage and for event tracking

* add slippage types

* add expected withdrawal coins

* calculate slippage against expected coins

* update withdraw keeper tests

* spelling, improve comments on add liquidity math

* typo

* typo

* grammer

* typo / grammer

* remove pool_id from withdraw msg

* add slippage to tx cmd

* TestWithdraw_Partial

* nit

* add withdraw no pool, no deposit record tests

* drop event check on partial withdraw test

* fix broken link

* fix broken link

* resolve merge conflicts

* ensure swap fee can not be equal to 1; add full implementation of swap
pool methods;  these implementation ensure that the pool invariant is
always greater or equal to the previous invariant

* refactor duplicated code into private swap methods

* add runtime assertion to always ensure invariant is greater or equal
to the previous invariant sub fee on swaps

* improve comments for base pool swap functions

* add swap exact input and output methods to denominated pool that wrap
the base pool interface for swapping

* comment touch ups

* more comment touchups

* fix msg deposit struct tag (#943)

* use better name for swap calculation private methods

* nits: golint

* fix misspelling in method name

* Add HARD token governance committee for Hard module (#941)

* add hard gov token committee

* revisions: update migration

* revisions: update test/data file

* initial revisions

* add TokenCommittee JSONMarshal test

* fix SetPermissions method

* remove BaseCommittee Type field

* add incentive params to allowed params

* Add SWP token governance committee for Swap module (#946)

* add swp token commitee to migration

* update test, add gen export utility method

* final revisions: add TODO

* remove slippage from withdraw to use min values for coins; add
additional validation test cases

* update alias for swap module

* add withdraw tests to handler for increased coverage; note: first pass,
improvements still yet to be made here

* refact withdraw keeper to use min amounts; panic for cases that do not
happen in normal situations

* lint fixes

* use total shares to track if pool should be deleted; add more in depth
withdraw comment

* add exact args for withdraw cmd

* extract record update methods

* update depositor share record if it exists -- do not overwrite an
existing record; ensures no loss of shares if the same address deposits
more than once

* Swap queries: deposit, pool, pools (#949)

* query deposits types

* implement deposit querier keeper methods

* query deposits CLI

* query deposits REST

* query types for pool/pools

* pool/pools querier keeper methods

* pool/pools CLI

* pool/pools REST

* basic pool/pools query tests

* basic deposit querier test

* iterate share records via owner bytes

* nit: add example for querying deposits by owner only

Co-authored-by: karzak <kjydavis3@gmail.com>

* feat: add REST tx handler for swap LP withdrawals

Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>
Co-authored-by: Denali Marsh <denali@kava.io>
Co-authored-by: denalimarsh <denalimarsh@gmail.com>
Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
Ruaridh 2021-07-13 12:38:15 +01:00 committed by GitHub
parent 3fc2a63556
commit 65052ce31a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 5662 additions and 445 deletions

View File

@ -90,6 +90,8 @@ var (
) )
// module account permissions // module account permissions
// if these are changed, then the permissions
// must also be migrated during a chain upgrade
mAccPerms = map[string][]string{ mAccPerms = map[string][]string{
auth.FeeCollectorName: nil, auth.FeeCollectorName: nil,
distr.ModuleName: nil, distr.ModuleName: nil,
@ -105,6 +107,7 @@ var (
kavadist.ModuleName: {supply.Minter}, kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner}, issuance.ModuleAccountName: {supply.Minter, supply.Burner},
hard.ModuleAccountName: {supply.Minter}, hard.ModuleAccountName: {supply.Minter},
swap.ModuleAccountName: nil,
} }
// module accounts that are allowed to receive tokens // module accounts that are allowed to receive tokens
@ -391,6 +394,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
app.cdc, app.cdc,
keys[swap.StoreKey], keys[swap.StoreKey],
swapSubspace, swapSubspace,
app.accountKeeper,
app.supplyKeeper,
) )
app.incentiveKeeper = incentive.NewKeeper( app.incentiveKeeper = incentive.NewKeeper(
app.cdc, app.cdc,
@ -492,6 +497,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
committee.NewAppModule(app.committeeKeeper, app.accountKeeper), committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper), issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper), hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper),
swap.NewAppModule(app.swapKeeper),
) )
app.sm.RegisterStoreDecoders() app.sm.RegisterStoreDecoders()

View File

@ -39,6 +39,7 @@ import (
"github.com/kava-labs/kava/x/incentive" "github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
"github.com/kava-labs/kava/x/swap"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting" validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
) )
@ -185,6 +186,7 @@ func TestAppImportExport(t *testing.T) {
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}}, {app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}}, {app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
{app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}}, {app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}},
{app.keys[swap.StoreKey], newApp.keys[swap.StoreKey], [][]byte{}},
} }
for _, skp := range storeKeysPrefixes { for _, skp := range storeKeysPrefixes {

View File

@ -2,28 +2,34 @@
parent: parent:
order: false order: false
---> --->
# Community Tools # Community Tools
The Kava community has developed some amazing tools and services. If you've built something great on, for, or in support of the Kava ecosystem let us know and we'll add it to this list. The Kava community has developed some amazing tools and services. If you've built something great on, for, or in support of the Kava ecosystem let us know and we'll add it to this list.
## User Interfaces ## User Interfaces
- [Kava Web App](https://app.kava.io/) - [Kava Web App](https://app.kava.io/)
- Cosmostation Mobile App: [App Store](https://apps.apple.com/us/app/cosmostation/id1459830339), [Play Store](https://play.google.com/store/apps/details?id=wannabit.io.cosmostaion&hl=en) - Cosmostation Mobile App: [App Store](https://apps.apple.com/us/app/cosmostation/id1459830339), [Play Store](https://play.google.com/store/apps/details?id=wannabit.io.cosmostaion&hl=en)
- Frontier Mobile App: [App Store](https://apps.apple.com/us/app/frontier-defi-wallet/id1482380988), [Play Store](https://play.google.com/store/apps/details?id=com.frontierwallet&hl=en) - Frontier Mobile App: [App Store](https://apps.apple.com/us/app/frontier-defi-wallet/id1482380988), [Play Store](https://play.google.com/store/apps/details?id=com.frontierwallet&hl=en)
## Explorers ## Explorers
- [Mintscan](https://kava.mintscan.io/) by Cosmostation - [Mintscan](https://kava.mintscan.io/) by Cosmostation
- [Big Dipper](https://kava.bigdipper.live/) by Forbole - [Big Dipper](https://kava.bigdipper.live/) by Forbole
- [PING.pub](https://kava.ping.pub/#/parameter) - [PING.pub](https://kava.ping.pub/#/parameter)
- [KAVAScan](https://kavascan.com) - [KAVAScan](https://kavascan.com)
## For Validators ## For Validators
- [QuickSync](https://kava.quicksync.io/) chain snapshots - [QuickSync](https://kava.quicksync.io/) chain snapshots
## Staking ## Staking
- [Staking rewards calculator](https://www.stakingrewards.com/earn/kava/calculate) - [Staking rewards calculator](https://www.stakingrewards.com/earn/kava/calculate)
### Staking Providers ### Staking Providers
Listed in alphabetical order. Listed in alphabetical order.
- [ATEAM](https://nodeateam.com/) - [ATEAM](https://nodeateam.com/)
@ -46,5 +52,3 @@ Listed in alphabetical order.
- [StakeWith.Us](https://www.stakewith.us/) - [StakeWith.Us](https://www.stakewith.us/)
- [Staked](https://staked.us/) - [Staked](https://staked.us/)
- [stake.fish](https://stake.fish/en/) - [stake.fish](https://stake.fish/en/)

View File

@ -14,6 +14,7 @@ import (
v0_15committee "github.com/kava-labs/kava/x/committee/types" v0_15committee "github.com/kava-labs/kava/x/committee/types"
v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14"
v0_15incentive "github.com/kava-labs/kava/x/incentive/types" v0_15incentive "github.com/kava-labs/kava/x/incentive/types"
v0_15swap "github.com/kava-labs/kava/x/swap/types"
) )
var ( var (
@ -74,6 +75,8 @@ func MigrateAppState(v0_14AppState genutil.AppMap) {
// Marshal v15 committee genesis state // Marshal v15 committee genesis state
v0_14AppState[v0_15committee.ModuleName] = v0_15Codec.MustMarshalJSON(Committee(committeeGS)) v0_14AppState[v0_15committee.ModuleName] = v0_15Codec.MustMarshalJSON(Committee(committeeGS))
} }
v0_14AppState[v0_15swap.ModuleName] = v0_15Codec.MustMarshalJSON(Swap())
} }
func makeV014Codec() *codec.Codec { func makeV014Codec() *codec.Codec {
@ -92,7 +95,8 @@ func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisS
proposals := []v0_15committee.Proposal{} proposals := []v0_15committee.Proposal{}
for _, com := range genesisState.Committees { for _, com := range genesisState.Committees {
if com.ID == 1 { switch com.ID {
case 1:
// Initialize member committee without permissions // Initialize member committee without permissions
stabilityCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members, stabilityCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members,
[]v0_15committee.Permission{}, com.VoteThreshold, com.ProposalDuration, []v0_15committee.Permission{}, com.VoteThreshold, com.ProposalDuration,
@ -186,10 +190,9 @@ func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisS
newStabilityCommitteePermissions = append(newStabilityCommitteePermissions, v0_15committee.TextPermission{}) newStabilityCommitteePermissions = append(newStabilityCommitteePermissions, v0_15committee.TextPermission{})
// Set stability committee permissions // Set stability committee permissions
baseStabilityCom := stabilityCom.SetPermissions(newStabilityCommitteePermissions) newStabilityCom := stabilityCom.SetPermissions(newStabilityCommitteePermissions)
newStabilityCom := v0_15committee.MemberCommittee{BaseCommittee: baseStabilityCom}
committees = append(committees, newStabilityCom) committees = append(committees, newStabilityCom)
} else { case 2:
safetyCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members, safetyCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members,
[]v0_15committee.Permission{v0_15committee.SoftwareUpgradePermission{}}, []v0_15committee.Permission{v0_15committee.SoftwareUpgradePermission{}},
com.VoteThreshold, com.ProposalDuration, v0_15committee.FirstPastThePost) com.VoteThreshold, com.ProposalDuration, v0_15committee.FirstPastThePost)
@ -197,6 +200,73 @@ func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisS
} }
} }
stabilityComMembers, err := loadStabilityComMembers()
if err != nil {
panic(err)
}
// ---------------------------- Initialize hard governance committee ----------------------------
hardGovDuration := time.Duration(time.Hour * 24 * 7)
hardGovThreshold := sdk.MustNewDecFromStr("0.5")
hardGovQuorum := sdk.MustNewDecFromStr("0.33")
hardGovCom := v0_15committee.NewTokenCommittee(3, "Hard Governance Committee", stabilityComMembers,
[]v0_15committee.Permission{}, hardGovThreshold, hardGovDuration, v0_15committee.Deadline,
hardGovQuorum, "hard")
// Add hard money market committee permissions
var newHardCommitteePermissions []v0_15committee.Permission
var newHardSubParamPermissions v0_15committee.SubParamChangePermission
// Allowed params permissions
hardComAllowedParams := v0_15committee.AllowedParams{
v0_15committee.AllowedParam{Subspace: "hard", Key: "MoneyMarkets"},
v0_15committee.AllowedParam{Subspace: "hard", Key: "MinimumBorrowUSDValue"},
v0_15committee.AllowedParam{Subspace: "incentive", Key: "HardSupplyRewardPeriods"},
v0_15committee.AllowedParam{Subspace: "incentive", Key: "HardBorrowRewardPeriods"},
v0_15committee.AllowedParam{Subspace: "incentive", Key: "HardDelegatorRewardPeriods"},
}
newHardSubParamPermissions.AllowedParams = hardComAllowedParams
// Money market permissions
var newMoneyMarketParams v0_15committee.AllowedMoneyMarkets
hardMMDenoms := []string{"bnb", "busd", "btcb", "xrpb", "usdx", "ukava", "hard"}
for _, mmDenom := range hardMMDenoms {
newMoneyMarketParam := v0_15committee.NewAllowedMoneyMarket(mmDenom, true, true, false, true, true, true)
newMoneyMarketParams = append(newMoneyMarketParams, newMoneyMarketParam)
}
newHardSubParamPermissions.AllowedMoneyMarkets = newMoneyMarketParams
newHardCommitteePermissions = append(newHardCommitteePermissions, newHardSubParamPermissions)
// Set hard governance committee permissions
permissionedHardGovCom := hardGovCom.SetPermissions(newHardCommitteePermissions)
committees = append(committees, permissionedHardGovCom)
// ---------------------------- Initialize swp governance committee ----------------------------
swpGovDuration := time.Duration(time.Hour * 24 * 7)
swpGovThreshold := sdk.MustNewDecFromStr("0.5")
swpGovQuorum := sdk.MustNewDecFromStr("0.33")
swpGovCom := v0_15committee.NewTokenCommittee(4, "Swp Governance Committee", stabilityComMembers,
[]v0_15committee.Permission{}, swpGovThreshold, swpGovDuration, v0_15committee.Deadline,
swpGovQuorum, "swp")
// Add swap money market committee permissions
var newSwapCommitteePermissions []v0_15committee.Permission
var newSwapSubParamPermissions v0_15committee.SubParamChangePermission
// TODO: add additional incentive params that manage LP rewards
swpAllowedParams := v0_15committee.AllowedParams{
v0_15committee.AllowedParam{Subspace: "swap", Key: "AllowedPools"},
v0_15committee.AllowedParam{Subspace: "swap", Key: "SwapFee"},
v0_15committee.AllowedParam{Subspace: "incentive", Key: "HardDelegatorRewardPeriods"},
}
newSwapSubParamPermissions.AllowedParams = swpAllowedParams
newSwpCommitteePermissions := append(newSwapCommitteePermissions, newSwapSubParamPermissions)
permissionedSwapGovCom := swpGovCom.SetPermissions(newSwpCommitteePermissions)
committees = append(committees, permissionedSwapGovCom)
for _, v := range genesisState.Votes { for _, v := range genesisState.Votes {
newVote := v0_15committee.NewVote(v.ProposalID, v.Voter, v0_15committee.Yes) newVote := v0_15committee.NewVote(v.ProposalID, v.Voter, v0_15committee.Yes)
votes = append(votes, v0_15committee.Vote(newVote)) votes = append(votes, v0_15committee.Vote(newVote))
@ -211,6 +281,32 @@ func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisS
genesisState.NextProposalID, committees, proposals, votes) genesisState.NextProposalID, committees, proposals, votes)
} }
func loadStabilityComMembers() ([]sdk.AccAddress, error) {
strAddrs := []string{
"kava1gru35up50ql2wxhegr880qy6ynl63ujlv8gum2",
"kava1sc3mh3pkas5e7xd269am4xm5mp6zweyzmhjagj",
"kava1c9ye54e3pzwm3e0zpdlel6pnavrj9qqv6e8r4h",
"kava1m7p6sjqrz6mylz776ct48wj6lpnpcd0z82209d",
"kava1a9pmkzk570egv3sflu3uwdf3gejl7qfy9hghzl",
}
var addrs []sdk.AccAddress
for _, strAddr := range strAddrs {
addr, err := sdk.AccAddressFromBech32(strAddr)
if err != nil {
return addrs, err
}
addrs = append(addrs, addr)
}
return addrs, nil
}
// Swap introduces new v0.15 swap genesis state
func Swap() v0_15swap.GenesisState {
return v0_15swap.NewGenesisState(v0_15swap.DefaultParams())
}
// Incentive migrates from a v0.14 incentive genesis state to a v0.15 incentive genesis state // Incentive migrates from a v0.14 incentive genesis state to a v0.15 incentive genesis state
func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisState { func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisState {
// Migrate params // Migrate params

View File

@ -43,7 +43,7 @@ func TestCommittee(t *testing.T) {
err = newGenState.Validate() err = newGenState.Validate()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(oldGenState.Committees), len(newGenState.Committees)) require.Equal(t, len(oldGenState.Committees)+2, len(newGenState.Committees)) // New gen state has 2 additional committees
for i := 0; i < len(oldGenState.Committees); i++ { for i := 0; i < len(oldGenState.Committees); i++ {
require.Equal(t, len(oldGenState.Committees[i].Permissions), len(newGenState.Committees[i].GetPermissions())) require.Equal(t, len(oldGenState.Committees[i].Permissions), len(newGenState.Committees[i].GetPermissions()))
} }
@ -57,6 +57,12 @@ func TestCommittee(t *testing.T) {
require.Equal(t, len(oldSPCP.AllowedMoneyMarkets), len(newSPCP.AllowedMoneyMarkets)) require.Equal(t, len(oldSPCP.AllowedMoneyMarkets), len(newSPCP.AllowedMoneyMarkets))
} }
// exportGenesisJSON is a utility testing method
func exportGenesisJSON(genState v0_15committee.GenesisState) {
v15Cdc := app.MakeCodec()
ioutil.WriteFile(filepath.Join("testdata", "kava-8-committee-state.json"), v15Cdc.MustMarshalJSON(genState), 0644)
}
func TestIncentive(t *testing.T) { func TestIncentive(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-7-incentive-state.json")) bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-7-incentive-state.json"))
require.NoError(t, err) require.NoError(t, err)

View File

@ -106,7 +106,6 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
VoteThreshold: suite.testGenesis.Committees[0].GetVoteThreshold(), VoteThreshold: suite.testGenesis.Committees[0].GetVoteThreshold(),
ProposalDuration: suite.testGenesis.Committees[0].GetProposalDuration(), ProposalDuration: suite.testGenesis.Committees[0].GetProposalDuration(),
TallyOption: types.FirstPastThePost, TallyOption: types.FirstPastThePost,
Type: types.MemberCommitteeType,
}, },
}, },
), ),

View File

@ -36,6 +36,7 @@ func RegisterCodec(cdc *codec.Codec) {
// Committees // Committees
cdc.RegisterInterface((*Committee)(nil), nil) cdc.RegisterInterface((*Committee)(nil), nil)
cdc.RegisterConcrete(BaseCommittee{}, "kava/BaseCommittee", nil)
cdc.RegisterConcrete(MemberCommittee{}, "kava/MemberCommittee", nil) cdc.RegisterConcrete(MemberCommittee{}, "kava/MemberCommittee", nil)
cdc.RegisterConcrete(TokenCommittee{}, "kava/TokenCommittee", nil) cdc.RegisterConcrete(TokenCommittee{}, "kava/TokenCommittee", nil)

View File

@ -24,6 +24,7 @@ const (
) )
const ( const (
BaseCommitteeType = "kava/BaseCommittee"
MemberCommitteeType = "kava/MemberCommittee" // Committee is composed of member addresses that vote to enact proposals within their permissions MemberCommitteeType = "kava/MemberCommittee" // Committee is composed of member addresses that vote to enact proposals within their permissions
TokenCommitteeType = "kava/TokenCommittee" // Committee is composed of token holders with voting power determined by total token balance TokenCommitteeType = "kava/TokenCommittee" // Committee is composed of token holders with voting power determined by total token balance
BondDenom = "ukava" BondDenom = "ukava"
@ -33,6 +34,7 @@ func init() {
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go). // CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
// But since these proposals contain Committees, these types also need registering: // But since these proposals contain Committees, these types also need registering:
govtypes.ModuleCdc.RegisterInterface((*Committee)(nil), nil) govtypes.ModuleCdc.RegisterInterface((*Committee)(nil), nil)
govtypes.RegisterProposalTypeCodec(BaseCommittee{}, "kava/BaseCommittee")
govtypes.RegisterProposalTypeCodec(MemberCommittee{}, "kava/MemberCommittee") govtypes.RegisterProposalTypeCodec(MemberCommittee{}, "kava/MemberCommittee")
govtypes.RegisterProposalTypeCodec(TokenCommittee{}, "kava/TokenCommittee") govtypes.RegisterProposalTypeCodec(TokenCommittee{}, "kava/TokenCommittee")
} }
@ -130,7 +132,7 @@ type Committee interface {
HasMember(addr sdk.AccAddress) bool HasMember(addr sdk.AccAddress) bool
GetPermissions() []Permission GetPermissions() []Permission
SetPermissions([]Permission) BaseCommittee SetPermissions([]Permission) Committee
HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool
GetProposalDuration() time.Duration GetProposalDuration() time.Duration
@ -148,7 +150,6 @@ type Committees []Committee
// BaseCommittee is a common type shared by all Committees // BaseCommittee is a common type shared by all Committees
type BaseCommittee struct { type BaseCommittee struct {
Type string `json:"type" yaml:"type"`
ID uint64 `json:"id" yaml:"id"` ID uint64 `json:"id" yaml:"id"`
Description string `json:"description" yaml:"description"` Description string `json:"description" yaml:"description"`
Members []sdk.AccAddress `json:"members" yaml:"members"` Members []sdk.AccAddress `json:"members" yaml:"members"`
@ -159,7 +160,7 @@ type BaseCommittee struct {
} }
// GetType is a getter for committee type // GetType is a getter for committee type
func (c BaseCommittee) GetType() string { return c.Type } func (c BaseCommittee) GetType() string { return BaseCommitteeType }
// GetID is a getter for committee ID // GetID is a getter for committee ID
func (c BaseCommittee) GetID() uint64 { return c.ID } func (c BaseCommittee) GetID() uint64 { return c.ID }
@ -275,14 +276,13 @@ func (c BaseCommittee) Validate() error {
// String implements fmt.Stringer // String implements fmt.Stringer
func (c BaseCommittee) String() string { func (c BaseCommittee) String() string {
return fmt.Sprintf(`Committee %d: return fmt.Sprintf(`Committee %d:
Type: %s
Description: %s Description: %s
Members: %s Members: %s
Permissions: %s Permissions: %s
VoteThreshold: %s VoteThreshold: %s
ProposalDuration: %s ProposalDuration: %s
TallyOption: %s`, TallyOption: %s`,
c.ID, c.Type, c.Description, c.GetMembers(), c.Permissions, c.ID, c.Description, c.GetMembers(), c.Permissions,
c.VoteThreshold.String(), c.ProposalDuration.String(), c.VoteThreshold.String(), c.ProposalDuration.String(),
c.TallyOption.String(), c.TallyOption.String(),
) )
@ -298,7 +298,6 @@ func NewMemberCommittee(id uint64, description string, members []sdk.AccAddress,
threshold sdk.Dec, duration time.Duration, tallyOption TallyOption) MemberCommittee { threshold sdk.Dec, duration time.Duration, tallyOption TallyOption) MemberCommittee {
return MemberCommittee{ return MemberCommittee{
BaseCommittee: BaseCommittee{ BaseCommittee: BaseCommittee{
Type: MemberCommitteeType,
ID: id, ID: id,
Description: description, Description: description,
Members: members, Members: members,
@ -310,6 +309,15 @@ func NewMemberCommittee(id uint64, description string, members []sdk.AccAddress,
} }
} }
// GetType is a getter for committee type
func (c MemberCommittee) GetType() string { return MemberCommitteeType }
// SetPermissions is a setter for committee permissions
func (c MemberCommittee) SetPermissions(permissions []Permission) Committee {
c.Permissions = permissions
return c
}
// Validate validates the committee's fields // Validate validates the committee's fields
func (c MemberCommittee) Validate() error { func (c MemberCommittee) Validate() error {
return c.BaseCommittee.Validate() return c.BaseCommittee.Validate()
@ -327,7 +335,6 @@ func NewTokenCommittee(id uint64, description string, members []sdk.AccAddress,
threshold sdk.Dec, duration time.Duration, tallyOption TallyOption, quorum sdk.Dec, tallyDenom string) TokenCommittee { threshold sdk.Dec, duration time.Duration, tallyOption TallyOption, quorum sdk.Dec, tallyDenom string) TokenCommittee {
return TokenCommittee{ return TokenCommittee{
BaseCommittee: BaseCommittee{ BaseCommittee: BaseCommittee{
Type: TokenCommitteeType,
ID: id, ID: id,
Description: description, Description: description,
Members: members, Members: members,
@ -341,12 +348,21 @@ func NewTokenCommittee(id uint64, description string, members []sdk.AccAddress,
} }
} }
// GetType is a getter for committee type
func (c TokenCommittee) GetType() string { return TokenCommitteeType }
// GetQuorum returns the quorum of the committee // GetQuorum returns the quorum of the committee
func (c TokenCommittee) GetQuorum() sdk.Dec { return c.Quorum } func (c TokenCommittee) GetQuorum() sdk.Dec { return c.Quorum }
// GetTallyDenom returns the tally denom of the committee // GetTallyDenom returns the tally denom of the committee
func (c TokenCommittee) GetTallyDenom() string { return c.TallyDenom } func (c TokenCommittee) GetTallyDenom() string { return c.TallyDenom }
// SetPermissions is a setter for committee permissions
func (c TokenCommittee) SetPermissions(permissions []Permission) Committee {
c.Permissions = permissions
return c
}
// Validate validates the committee's fields // Validate validates the committee's fields
func (c TokenCommittee) Validate() error { func (c TokenCommittee) Validate() error {
if c.TallyDenom == BondDenom { if c.TallyDenom == BondDenom {

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto"
@ -199,7 +200,6 @@ func TestMemberCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: MemberCommitteeType,
}, },
}, },
expectPass: true, expectPass: true,
@ -245,7 +245,6 @@ func TestTokenCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
}, },
Quorum: d("0.4"), Quorum: d("0.4"),
TallyDenom: "hard", TallyDenom: "hard",
@ -263,7 +262,6 @@ func TestTokenCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
}, },
Quorum: sdk.Dec{Int: nil}, Quorum: sdk.Dec{Int: nil},
TallyDenom: "hard", TallyDenom: "hard",
@ -281,7 +279,6 @@ func TestTokenCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
}, },
Quorum: d("-0.1"), Quorum: d("-0.1"),
TallyDenom: "hard", TallyDenom: "hard",
@ -299,7 +296,6 @@ func TestTokenCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
}, },
Quorum: d("1.001"), Quorum: d("1.001"),
TallyDenom: "hard", TallyDenom: "hard",
@ -317,7 +313,6 @@ func TestTokenCommittee(t *testing.T) {
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
}, },
Quorum: d("0.4"), Quorum: d("0.4"),
TallyDenom: BondDenom, TallyDenom: BondDenom,
@ -340,3 +335,50 @@ func TestTokenCommittee(t *testing.T) {
}) })
} }
} }
// TestTokenCommitteeMarshalJSON tests TokenCommittee JSON marshaling
func TestTokenCommitteeMarshalJSON(t *testing.T) {
addresses := []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
}
testCases := []struct {
name string
committee TokenCommittee
}{
{
name: "normal",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:2],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: Deadline,
},
Quorum: d("0.4"),
TallyDenom: "hard",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.committee.Validate()
require.NoError(t, err)
// Marshal and unmarshal the TokenCommittee
cdc := codec.New()
RegisterCodec(cdc)
bz := cdc.MustMarshalJSON(tc.committee)
var com TokenCommittee
cdc.MustUnmarshalJSON(bz, &com)
require.Equal(t, tc.committee, com)
})
}
}

View File

@ -33,7 +33,6 @@ func TestGenesisState_Validate(t *testing.T) {
Permissions: []Permission{GodPermission{}}, Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"), VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7, ProposalDuration: time.Hour * 24 * 7,
Type: MemberCommitteeType,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
}, },
}, },
@ -45,7 +44,6 @@ func TestGenesisState_Validate(t *testing.T) {
Permissions: nil, Permissions: nil,
VoteThreshold: d("0.8"), VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 21, ProposalDuration: time.Hour * 24 * 21,
Type: MemberCommitteeType,
TallyOption: FirstPastThePost, TallyOption: FirstPastThePost,
}, },
}, },
@ -57,7 +55,6 @@ func TestGenesisState_Validate(t *testing.T) {
Permissions: nil, Permissions: nil,
VoteThreshold: d("0.8"), VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 21, ProposalDuration: time.Hour * 24 * 21,
Type: TokenCommitteeType,
TallyOption: Deadline, TallyOption: Deadline,
}, },
Quorum: sdk.MustNewDecFromStr("0.4"), Quorum: sdk.MustNewDecFromStr("0.4"),

View File

@ -1,3 +1,5 @@
// Code generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) DO NOT EDIT.
package swap package swap
import ( import (
@ -6,23 +8,82 @@ import (
) )
const ( const (
ModuleName = types.ModuleName AttributeKeyDepositor = types.AttributeKeyDepositor
QuerierRoute = types.QuerierRoute AttributeKeyOwner = types.AttributeKeyOwner
RouterKey = types.RouterKey AttributeKeyPoolID = types.AttributeKeyPoolID
StoreKey = types.StoreKey AttributeKeyShares = types.AttributeKeyShares
DefaultParamspace = types.DefaultParamspace AttributeValueCategory = types.AttributeValueCategory
) DefaultParamspace = types.DefaultParamspace
EventTypeSwapDeposit = types.EventTypeSwapDeposit
type ( EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
GenesisState = types.GenesisState ModuleAccountName = types.ModuleAccountName
Keeper = keeper.Keeper ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
StoreKey = types.StoreKey
) )
var ( var (
NewKeeper = keeper.NewKeeper // function aliases
NewQuerier = keeper.NewQuerier NewKeeper = keeper.NewKeeper
ModuleCdc = types.ModuleCdc NewQuerier = keeper.NewQuerier
ParamKeyTable = types.ParamKeyTable DefaultGenesisState = types.DefaultGenesisState
RegisterCodec = types.RegisterCodec DefaultParams = types.DefaultParams
DefaultGenesisState = types.DefaultGenesisState DepositorPoolSharesKey = types.DepositorPoolSharesKey
NewAllowedPool = types.NewAllowedPool
NewAllowedPools = types.NewAllowedPools
NewBasePool = types.NewBasePool
NewBasePoolWithExistingShares = types.NewBasePoolWithExistingShares
NewDenominatedPool = types.NewDenominatedPool
NewDenominatedPoolWithExistingShares = types.NewDenominatedPoolWithExistingShares
NewGenesisState = types.NewGenesisState
NewMsgDeposit = types.NewMsgDeposit
NewMsgWithdraw = types.NewMsgWithdraw
NewParams = types.NewParams
NewPoolRecord = types.NewPoolRecord
NewShareRecord = types.NewShareRecord
ParamKeyTable = types.ParamKeyTable
PoolID = types.PoolID
PoolIDFromCoins = types.PoolIDFromCoins
PoolKey = types.PoolKey
RegisterCodec = types.RegisterCodec
// variable aliases
DefaultAllowedPools = types.DefaultAllowedPools
DefaultSwapFee = types.DefaultSwapFee
DepositorPoolSharesPrefix = types.DepositorPoolSharesPrefix
ErrDeadlineExceeded = types.ErrDeadlineExceeded
ErrDepositNotFound = types.ErrDepositNotFound
ErrInsufficientLiquidity = types.ErrInsufficientLiquidity
ErrInvalidCoin = types.ErrInvalidCoin
ErrInvalidDeadline = types.ErrInvalidDeadline
ErrInvalidPool = types.ErrInvalidPool
ErrInvalidShares = types.ErrInvalidShares
ErrInvalidSlippage = types.ErrInvalidSlippage
ErrNotAllowed = types.ErrNotAllowed
ErrNotImplemented = types.ErrNotImplemented
ErrSlippageExceeded = types.ErrSlippageExceeded
KeyAllowedPools = types.KeyAllowedPools
KeySwapFee = types.KeySwapFee
MaxSwapFee = types.MaxSwapFee
ModuleCdc = types.ModuleCdc
PoolKeyPrefix = types.PoolKeyPrefix
)
type (
Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper
AllowedPool = types.AllowedPool
AllowedPools = types.AllowedPools
BasePool = types.BasePool
DenominatedPool = types.DenominatedPool
GenesisState = types.GenesisState
MsgDeposit = types.MsgDeposit
MsgWithDeadline = types.MsgWithDeadline
MsgWithdraw = types.MsgWithdraw
Params = types.Params
PoolRecord = types.PoolRecord
ShareRecord = types.ShareRecord
SupplyKeeper = types.SupplyKeeper
) )

View File

@ -2,17 +2,26 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
) )
// flags for cli queries
const (
flagOwner = "owner"
flagPool = "pool"
)
// GetQueryCmd returns the cli query commands for the module // GetQueryCmd returns the cli query commands for the module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
swapQueryCmd := &cobra.Command{ swapQueryCmd := &cobra.Command{
@ -25,6 +34,9 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
swapQueryCmd.AddCommand(flags.GetCommands( swapQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc), queryParamsCmd(queryRoute, cdc),
queryDepositsCmd(queryRoute, cdc),
queryPoolCmd(queryRoute, cdc),
queryPoolsCmd(queryRoute, cdc),
)...) )...)
return swapQueryCmd return swapQueryCmd
@ -56,3 +68,134 @@ func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
}, },
} }
} }
func queryDepositsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "deposits",
Short: "get liquidity provider deposits",
Long: strings.TrimSpace(`get liquidity provider deposits:
Example:
$ kvcli q swap deposits --pool bnb/usdx
$ kvcli q swap deposits --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny
$ kvcli q swap deposits --pool bnb/usdx --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny
$ kvcli q swap deposits --page=2 --limit=100
`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
bechOwnerAddr := viper.GetString(flagOwner)
pool := viper.GetString(flagPool)
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
var owner sdk.AccAddress
if len(bechOwnerAddr) != 0 {
ownerAddr, err := sdk.AccAddressFromBech32(bechOwnerAddr)
if err != nil {
return err
}
owner = ownerAddr
}
params := types.NewQueryDepositsParams(page, limit, owner, pool)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
cliCtx := context.NewCLIContext().WithCodec(cdc)
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetDeposits)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var deposits types.DepositsQueryResults
if err := cdc.UnmarshalJSON(res, &deposits); err != nil {
return fmt.Errorf("failed to unmarshal deposit results: %w", err)
}
return cliCtx.PrintOutput(deposits)
},
}
cmd.Flags().Int(flags.FlagPage, 1, "pagination page of deposits to query for")
cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of deposits to query for")
cmd.Flags().String(flagPool, "", "pool name")
cmd.Flags().String(flagOwner, "", "owner, also known as a liquidity provider")
return cmd
}
func queryPoolCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "pool",
Short: "get pool statistics",
Long: strings.TrimSpace(`get statistics about a given liquidity pool:
Example:
$ kvcli q swap pool ukava/usdx`,
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
poolName := args[0]
if len(poolName) == 0 {
return fmt.Errorf("must specify pool ID")
}
// Construct query with params
params := types.NewQueryPoolParams(poolName)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
// Execute query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetPool)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var poolStats types.PoolStatsQueryResult
if err := cdc.UnmarshalJSON(res, &poolStats); err != nil {
return fmt.Errorf("failed to unmarshal pool stats: %w", err)
}
return cliCtx.PrintOutput(poolStats)
},
}
cmd.Flags().String(flagPool, "", "pool ID")
return cmd
}
func queryPoolsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "pools",
Short: "get statistics for all pools",
Long: strings.TrimSpace(`get statistics for all liquidity pools:
Example:
$ kvcli q swap pools`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Execute query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetPools)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var poolStats types.PoolStatsQueryResults
if err := cdc.UnmarshalJSON(res, &poolStats); err != nil {
return fmt.Errorf("failed to unmarshal pools' stats: %w", err)
}
return cliCtx.PrintOutput(poolStats)
},
}
return cmd
}

View File

@ -1,13 +1,20 @@
package cli package cli
import ( import (
"bufio"
"fmt" "fmt"
"strconv"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec" "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/swap/types" "github.com/kava-labs/kava/x/swap/types"
) )
@ -22,7 +29,97 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
RunE: client.ValidateCmd, RunE: client.ValidateCmd,
} }
swapTxCmd.AddCommand(flags.PostCommands()...) swapTxCmd.AddCommand(flags.PostCommands(
getCmdDeposit(cdc),
getCmdWithdraw(cdc),
)...)
return swapTxCmd return swapTxCmd
} }
func getCmdDeposit(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposit [tokenA] [tokenB] [slippage] [deadline]",
Short: "deposit coins to a swap liquidity pool",
Example: fmt.Sprintf(
`%s tx %s deposit 10000000ukava 10000000usdx 0.01 1624224736 --from <key>`, version.ClientName, types.ModuleName,
),
Args: cobra.ExactArgs(4),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
tokenA, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
tokenB, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
slippage, err := sdk.NewDecFromStr(args[2])
if err != nil {
return err
}
deadline, err := strconv.ParseInt(args[3], 10, 64)
if err != nil {
return err
}
msg := types.NewMsgDeposit(cliCtx.GetFromAddress(), tokenA, tokenB, slippage, deadline)
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdWithdraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "withdraw [shares] [minCoinA] [minCoinB] [deadline]",
Short: "withdraw coins from a swap liquidity pool",
Example: fmt.Sprintf(
`%s tx %s withdraw 153000 10000000ukava 20000000usdx 176293740 --from <key>`, version.ClientName, types.ModuleName,
),
Args: cobra.ExactArgs(4),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
numShares, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return err
}
shares := sdk.NewInt(numShares)
minTokenA, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
minTokenB, err := sdk.ParseCoin(args[2])
if err != nil {
return err
}
deadline, err := strconv.ParseInt(args[3], 10, 64)
if err != nil {
return err
}
msg := types.NewMsgWithdraw(cliCtx.GetFromAddress(), shares, minTokenA, minTokenB, deadline)
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -3,10 +3,12 @@ package rest
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context" "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/types/rest"
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
@ -14,6 +16,9 @@ import (
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/deposits", types.ModuleName), queryDepositsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/pool", types.ModuleName), queryPoolHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/pools", types.ModuleName), queryPoolsHandlerFn(cliCtx)).Methods("GET")
} }
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
@ -35,3 +40,110 @@ func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
rest.PostProcessResponse(w, cliCtx, res) rest.PostProcessResponse(w, cliCtx, res)
} }
} }
func queryDepositsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
var owner sdk.AccAddress
var pool string
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
if x := r.URL.Query().Get(RestPool); len(x) != 0 {
pool = strings.TrimSpace(x)
}
if x := r.URL.Query().Get(RestOwner); len(x) != 0 {
ownerStr := strings.ToLower(strings.TrimSpace(x))
shareOwner, err := sdk.AccAddressFromBech32(ownerStr)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from owner %s", ownerStr))
return
}
owner = shareOwner
}
params := types.NewQueryDepositsParams(page, limit, owner, pool)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetDeposits)
res, height, err := cliCtx.QueryWithData(route, bz)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryPoolHandlerFn(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
}
var poolName string
if x := r.URL.Query().Get(RestPool); len(x) != 0 {
poolName = strings.TrimSpace(x)
}
if len(poolName) == 0 {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("must specify pool param"))
return
}
params := types.NewQueryPoolParams(poolName)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetPool)
res, height, err := cliCtx.QueryWithData(route, bz)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryPoolsHandlerFn(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
}
route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryGetPools)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -4,14 +4,39 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
) )
// REST variable names // REST variable names
// nolint // nolint
const () const (
RestPool = "pool"
RestOwner = "owner"
)
// RegisterRoutes registers swap-related REST handlers to a router // RegisterRoutes registers swap-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r) registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r) registerTxRoutes(cliCtx, r)
} }
// PostCreateDepositReq defines the properties of a deposit create request's body
type PostCreateDepositReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
TokenA sdk.Coin `json:"token_a" yaml:"token_a"`
TokenB sdk.Coin `json:"token_b" yaml:"token_b"`
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
Deadline int64 `json:"deadline" yaml:"deadline"`
}
// PostCreateWithdrawReq defines the properties of a withdraw create request's body
type PostCreateWithdrawReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
Shares sdk.Int `json:"shares" yaml:"shares"`
MinTokenA sdk.Coin `json:"token_a" yaml:"token_a"`
MinTokenB sdk.Coin `json:"token_b" yaml:"token_b"`
Deadline int64 `json:"deadline" yaml:"deadline"`
}

View File

@ -1,9 +1,62 @@
package rest package rest
import ( import (
"fmt"
"net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context" "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/swap/types"
) )
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {} func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/deposit", types.ModuleName), postDepositHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/withdraw", types.ModuleName), postWithdrawHandlerFn(cliCtx)).Methods("POST")
}
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode POST request body
var req PostCreateDepositReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgDeposit(req.From, req.TokenA, req.TokenB, req.Slippage, req.Deadline)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode POST request body
var req PostCreateWithdrawReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgWithdraw(req.From, req.Shares, req.MinTokenA, req.MinTokenA, req.Deadline)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

View File

@ -3,15 +3,65 @@ package swap
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/types"
) )
// NewHandler creates an sdk.Handler for swap messages // NewHandler creates an sdk.Handler for swap messages
func NewHandler(k Keeper) sdk.Handler { func NewHandler(k Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager()) ctx = ctx.WithEventManager(sdk.NewEventManager())
if deadlineMsg, ok := msg.(types.MsgWithDeadline); ok {
if deadlineExceeded := deadlineMsg.DeadlineExceeded(ctx.BlockTime()); deadlineExceeded {
return nil, sdkerrors.Wrapf(types.ErrDeadlineExceeded, "block time %d >= deadline %d", ctx.BlockTime().Unix(), deadlineMsg.GetDeadline().Unix())
}
}
switch msg := msg.(type) { switch msg := msg.(type) {
case types.MsgDeposit:
return handleMsgDeposit(ctx, k, msg)
case types.MsgWithdraw:
return handleMsgWithdraw(ctx, k, msg)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
} }
} }
} }
func handleMsgDeposit(ctx sdk.Context, k keeper.Keeper, msg types.MsgDeposit) (*sdk.Result, error) {
if err := k.Deposit(ctx, msg.Depositor, msg.TokenA, msg.TokenB, msg.Slippage); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) (*sdk.Result, error) {
if err := k.Withdraw(ctx, msg.From, msg.Shares, msg.MinTokenA, msg.MinTokenB); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}

360
x/swap/handler_test.go Normal file
View File

@ -0,0 +1,360 @@
package swap_test
import (
"fmt"
"testing"
"time"
"github.com/kava-labs/kava/x/swap"
"github.com/kava-labs/kava/x/swap/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtime "github.com/tendermint/tendermint/types/time"
)
var swapModuleAccountAddress = sdk.AccAddress(crypto.AddressHash([]byte(swap.ModuleAccountName)))
type handlerTestSuite struct {
testutil.Suite
handler sdk.Handler
}
func (suite *handlerTestSuite) SetupTest() {
suite.Suite.SetupTest()
suite.handler = swap.NewHandler(suite.Keeper)
}
func (suite *handlerTestSuite) TestDeposit_CreatePool() {
pool := swap.NewAllowedPool("ukava", "usdx")
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
balance := sdk.NewCoins(
sdk.NewCoin(pool.TokenA, sdk.NewInt(10e6)),
sdk.NewCoin(pool.TokenB, sdk.NewInt(50e6)),
)
depositor := suite.CreateAccount(balance)
deposit := swap.NewMsgDeposit(
depositor.GetAddress(),
sdk.NewCoin(pool.TokenA, depositor.GetCoins().AmountOf(pool.TokenA)),
sdk.NewCoin(pool.TokenB, depositor.GetCoins().AmountOf(pool.TokenB)),
sdk.MustNewDecFromStr("0.01"),
time.Now().Add(10*time.Minute).Unix(),
)
res, err := suite.handler(suite.Ctx, deposit)
suite.Require().NoError(err)
suite.AccountBalanceEqual(depositor, sdk.Coins(nil))
suite.ModuleAccountBalanceEqual(balance)
suite.PoolLiquidityEqual(balance)
suite.PoolShareValueEqual(depositor, pool, balance)
suite.EventsContains(res.Events, sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.GetAddress().String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
bank.EventTypeTransfer,
sdk.NewAttribute(bank.AttributeKeyRecipient, swapModuleAccountAddress.String()),
sdk.NewAttribute(bank.AttributeKeySender, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, balance.String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
swap.EventTypeSwapDeposit,
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID(pool.TokenA, pool.TokenB)),
sdk.NewAttribute(swap.AttributeKeyDepositor, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, balance.String()),
sdk.NewAttribute(swap.AttributeKeyShares, "22360679"),
))
}
func (suite *handlerTestSuite) TestDeposit_DeadlineExceeded() {
pool := swap.NewAllowedPool("ukava", "usdx")
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
balance := sdk.NewCoins(
sdk.NewCoin(pool.TokenA, sdk.NewInt(10e6)),
sdk.NewCoin(pool.TokenB, sdk.NewInt(50e6)),
)
depositor := suite.CreateAccount(balance)
deposit := swap.NewMsgDeposit(
depositor.GetAddress(),
sdk.NewCoin(pool.TokenA, depositor.GetCoins().AmountOf(pool.TokenA)),
sdk.NewCoin(pool.TokenB, depositor.GetCoins().AmountOf(pool.TokenB)),
sdk.MustNewDecFromStr("0.01"),
suite.Ctx.BlockTime().Add(-1*time.Second).Unix(),
)
res, err := suite.handler(suite.Ctx, deposit)
suite.EqualError(err, fmt.Sprintf("deadline exceeded: block time %d >= deadline %d", suite.Ctx.BlockTime().Unix(), deposit.GetDeadline().Unix()))
suite.Nil(res)
}
func (suite *handlerTestSuite) TestDeposit_ExistingPool() {
pool := swap.NewAllowedPool("ukava", "usdx")
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
err := suite.CreatePool(reserves)
suite.Require().NoError(err)
balance := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
)
depositor := suite.NewAccountFromAddr(sdk.AccAddress("new depositor"), balance)
deposit := swap.NewMsgDeposit(
depositor.GetAddress(),
sdk.NewCoin("usdx", depositor.GetCoins().AmountOf("usdx")),
sdk.NewCoin("ukava", depositor.GetCoins().AmountOf("ukava")),
sdk.MustNewDecFromStr("0.01"),
time.Now().Add(10*time.Minute).Unix(),
)
res, err := suite.handler(suite.Ctx, deposit)
suite.Require().NoError(err)
expectedDeposit := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
)
expectedShareValue := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(999999)),
sdk.NewCoin("usdx", sdk.NewInt(4999998)),
)
suite.AccountBalanceEqual(depositor, balance.Sub(expectedDeposit))
suite.ModuleAccountBalanceEqual(reserves.Add(expectedDeposit...))
suite.PoolLiquidityEqual(reserves.Add(expectedDeposit...))
suite.PoolShareValueEqual(depositor, pool, expectedShareValue)
suite.EventsContains(res.Events, sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.GetAddress().String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
bank.EventTypeTransfer,
sdk.NewAttribute(bank.AttributeKeyRecipient, swapModuleAccountAddress.String()),
sdk.NewAttribute(bank.AttributeKeySender, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedDeposit.String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
swap.EventTypeSwapDeposit,
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID(pool.TokenA, pool.TokenB)),
sdk.NewAttribute(swap.AttributeKeyDepositor, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedDeposit.String()),
sdk.NewAttribute(swap.AttributeKeyShares, "2236067"),
))
}
func (suite *handlerTestSuite) TestDeposit_ExistingPool_SlippageFailure() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
err := suite.CreatePool(reserves)
suite.Require().NoError(err)
balance := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(5e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
)
depositor := suite.CreateAccount(balance)
deposit := swap.NewMsgDeposit(
depositor.GetAddress(),
sdk.NewCoin("usdx", depositor.GetCoins().AmountOf("usdx")),
sdk.NewCoin("ukava", depositor.GetCoins().AmountOf("ukava")),
sdk.MustNewDecFromStr("0.01"),
time.Now().Add(10*time.Minute).Unix(),
)
res, err := suite.handler(suite.Ctx, deposit)
suite.EqualError(err, "slippage exceeded: slippage 4.000000000000000000 > limit 0.010000000000000000")
suite.Nil(res)
}
func (suite *handlerTestSuite) TestWithdraw_AllShares() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
depositor := suite.CreateAccount(reserves)
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
suite.Require().NoError(err)
withdraw := swap.NewMsgWithdraw(
depositor.GetAddress(),
sdk.NewInt(22360679),
reserves[0],
reserves[1],
time.Now().Add(10*time.Minute).Unix(),
)
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
res, err := suite.handler(ctx, withdraw)
suite.Require().NoError(err)
suite.AccountBalanceEqual(depositor, reserves)
suite.ModuleAccountBalanceEqual(sdk.Coins(nil))
suite.PoolDeleted("ukava", "usdx")
suite.PoolSharesDeleted(depositor.GetAddress(), "ukava", "usdx")
suite.EventsContains(res.Events, sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.GetAddress().String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
bank.EventTypeTransfer,
sdk.NewAttribute(bank.AttributeKeyRecipient, depositor.GetAddress().String()),
sdk.NewAttribute(bank.AttributeKeySender, swapModuleAccountAddress.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, reserves.String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
swap.EventTypeSwapWithdraw,
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID(pool.TokenA, pool.TokenB)),
sdk.NewAttribute(swap.AttributeKeyOwner, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, reserves.String()),
sdk.NewAttribute(swap.AttributeKeyShares, "22360679"),
))
}
func (suite *handlerTestSuite) TestWithdraw_PartialShares() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
depositor := suite.CreateAccount(reserves)
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
suite.Require().NoError(err)
minTokenA := sdk.NewCoin("ukava", sdk.NewInt(4999999))
minTokenB := sdk.NewCoin("usdx", sdk.NewInt(24999998))
withdraw := swap.NewMsgWithdraw(
depositor.GetAddress(),
sdk.NewInt(11180339),
minTokenA,
minTokenB,
time.Now().Add(10*time.Minute).Unix(),
)
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
res, err := suite.handler(ctx, withdraw)
suite.Require().NoError(err)
expectedCoinsReceived := sdk.NewCoins(minTokenA, minTokenB)
suite.AccountBalanceEqual(depositor, expectedCoinsReceived)
suite.ModuleAccountBalanceEqual(reserves.Sub(expectedCoinsReceived))
suite.PoolLiquidityEqual(reserves.Sub(expectedCoinsReceived))
suite.PoolShareValueEqual(depositor, swap.NewAllowedPool("ukava", "usdx"), reserves.Sub(expectedCoinsReceived))
suite.EventsContains(res.Events, sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, swap.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.GetAddress().String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
bank.EventTypeTransfer,
sdk.NewAttribute(bank.AttributeKeyRecipient, depositor.GetAddress().String()),
sdk.NewAttribute(bank.AttributeKeySender, swapModuleAccountAddress.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedCoinsReceived.String()),
))
suite.EventsContains(res.Events, sdk.NewEvent(
swap.EventTypeSwapWithdraw,
sdk.NewAttribute(swap.AttributeKeyPoolID, swap.PoolID(pool.TokenA, pool.TokenB)),
sdk.NewAttribute(swap.AttributeKeyOwner, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedCoinsReceived.String()),
sdk.NewAttribute(swap.AttributeKeyShares, "11180339"),
))
}
func (suite *handlerTestSuite) TestWithdraw_SlippageFailure() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
depositor := suite.CreateAccount(reserves)
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
suite.Require().NoError(err)
minTokenA := sdk.NewCoin("ukava", sdk.NewInt(5e6))
minTokenB := sdk.NewCoin("usdx", sdk.NewInt(25e6))
withdraw := swap.NewMsgWithdraw(
depositor.GetAddress(),
sdk.NewInt(11180339),
minTokenA,
minTokenB,
time.Now().Add(10*time.Minute).Unix(),
)
res, err := suite.handler(suite.Ctx, withdraw)
suite.EqualError(err, "slippage exceeded: minimum withdraw not met")
suite.Nil(res)
}
func (suite *handlerTestSuite) TestWithdraw_DeadlineExceeded() {
balance := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
from := suite.CreateAccount(balance)
withdraw := swap.NewMsgWithdraw(
from.GetAddress(),
sdk.NewInt(2e6),
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
suite.Ctx.BlockTime().Add(-1*time.Second).Unix(),
)
res, err := suite.handler(suite.Ctx, withdraw)
suite.EqualError(err, fmt.Sprintf("deadline exceeded: block time %d >= deadline %d", suite.Ctx.BlockTime().Unix(), withdraw.GetDeadline().Unix()))
suite.Nil(res)
}
func (suite *handlerTestSuite) TestInvalidMsg() {
res, err := suite.handler(suite.Ctx, sdk.NewTestMsg())
suite.Nil(res)
suite.EqualError(err, "unknown request: unrecognized swap message type: *types.TestMsg")
}
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(handlerTestSuite))
}

145
x/swap/keeper/deposit.go Normal file
View File

@ -0,0 +1,145 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/swap/types"
)
// Deposit creates a new pool or adds liquidity to an existing pool. For a pool to be created, a pool
// for the coin denominations must not exist yet, and it must be allowed by the swap module parameters.
//
// When adding liquidity to an existing pool, the provided coins are considered to be the desired deposit
// amount, and the actual deposited coins may be less than or equal to the provided coins. A deposit
// will never be exceed the coinA and coinB amounts.
//
// The slippage is calculated using both the price and inverse price of the provided coinA and coinB.
// Since adding liquidity is not directional, like a swap would be, using both the price (coinB/coinA),
// and the inverse price (coinA/coinB), protects the depositor from a large deviation in their deposit.
//
// The amount deposited may only change by B' < B or A' < A -- either B depreciates, or A depreciates.
// Therefore, slippage can be written as a function of this depreciation d. Where the new price is
// B*(1-d)/A or A*(1-d)/B, and the inverse of each, and is A/(B*(1-d)) and B/(A*(1-d))
// respectively.
//
// Since 1/(1-d) >= (1-d) for d <= 1, the maximum slippage is always in the appreciating price
// A/(B*(1-d)) and B/(A*(1-d)). In other words, when the price of an asset depreciates, the
// inverse price -- or the price of the other pool asset, appreciates by a larger amount.
// It's this percent change we calculate and compare to the slippage limit provided.
//
// For example, if we have a pool with 100e6 ukava and 400e6 usdx. The ukava price is 4 usdx and the
// usdx price is 0.25 ukava. If a depositor adds liquidity of 4e6 ukava and 14e6 usdx, a kava price of
// 3.50 usdx and a usdx price of 0.29 ukava. This is a -12.5% slippage is the ukava price, and a 14.3%
// slippage in the usdx price.
//
// These slippages can be calculated by S_B = ((A/B')/(A/B) - 1) and S_A ((B/A')/(B/A) - 1), simplifying to
// S_B = (A/A' - 1), and S_B = (B/B' - 1). An error is returned when max(S_A, S_B) > slippageLimit.
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coinA sdk.Coin, coinB sdk.Coin, slippageLimit sdk.Dec) error {
desiredAmount := sdk.NewCoins(coinA, coinB)
poolID := types.PoolIDFromCoins(desiredAmount)
poolRecord, found := k.GetPool(ctx, poolID)
var (
depositAmount sdk.Coins
shares sdk.Int
err error
)
if found {
depositAmount, shares, err = k.addLiquidityToPool(ctx, poolRecord, depositor, desiredAmount)
} else {
depositAmount, shares, err = k.initializePool(ctx, poolID, depositor, desiredAmount)
}
if err != nil {
return err
}
if depositAmount.AmountOf(coinA.Denom).IsZero() || depositAmount.AmountOf(coinB.Denom).IsZero() {
return sdkerrors.Wrap(types.ErrInsufficientLiquidity, "deposit must be increased")
}
if shares.IsZero() {
return sdkerrors.Wrap(types.ErrInsufficientLiquidity, "deposit must be increased")
}
maxPercentPriceChange := sdk.MaxDec(
desiredAmount.AmountOf(coinA.Denom).ToDec().Quo(depositAmount.AmountOf(coinA.Denom).ToDec()),
desiredAmount.AmountOf(coinB.Denom).ToDec().Quo(depositAmount.AmountOf(coinB.Denom).ToDec()),
)
slippage := maxPercentPriceChange.Sub(sdk.OneDec())
if slippage.GT(slippageLimit) {
return sdkerrors.Wrapf(types.ErrSlippageExceeded, "slippage %s > limit %s", slippage, slippageLimit)
}
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleAccountName, depositAmount)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSwapDeposit,
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, depositAmount.String()),
sdk.NewAttribute(types.AttributeKeyShares, shares.String()),
),
)
return nil
}
func (k Keeper) depositAllowed(ctx sdk.Context, poolID string) bool {
params := k.GetParams(ctx)
for _, p := range params.AllowedPools {
if poolID == types.PoolID(p.TokenA, p.TokenB) {
return true
}
}
return false
}
func (k Keeper) initializePool(ctx sdk.Context, poolID string, depositor sdk.AccAddress, reserves sdk.Coins) (sdk.Coins, sdk.Int, error) {
if allowed := k.depositAllowed(ctx, poolID); !allowed {
return sdk.Coins{}, sdk.ZeroInt(), sdkerrors.Wrap(types.ErrNotAllowed, fmt.Sprintf("can not create pool '%s'", poolID))
}
pool, err := types.NewDenominatedPool(reserves)
if err != nil {
return sdk.Coins{}, sdk.ZeroInt(), err
}
poolRecord := types.NewPoolRecord(pool)
shareRecord := types.NewShareRecord(depositor, poolRecord.PoolID, pool.TotalShares())
k.SetPool(ctx, poolRecord)
k.SetDepositorShares(ctx, shareRecord)
return pool.Reserves(), pool.TotalShares(), nil
}
func (k Keeper) addLiquidityToPool(ctx sdk.Context, record types.PoolRecord, depositor sdk.AccAddress, desiredAmount sdk.Coins) (sdk.Coins, sdk.Int, error) {
pool, err := types.NewDenominatedPoolWithExistingShares(record.Reserves(), record.TotalShares)
if err != nil {
return sdk.Coins{}, sdk.ZeroInt(), err
}
depositAmount, shares := pool.AddLiquidity(desiredAmount)
poolRecord := types.NewPoolRecord(pool)
shareRecord, found := k.GetDepositorShares(ctx, depositor, poolRecord.PoolID)
if found {
shareRecord.SharesOwned = shareRecord.SharesOwned.Add(shares)
} else {
shareRecord = types.NewShareRecord(depositor, poolRecord.PoolID, shares)
}
k.SetPool(ctx, poolRecord)
k.SetDepositorShares(ctx, shareRecord)
return depositAmount, shares, nil
}

View File

@ -0,0 +1,343 @@
package keeper_test
import (
"errors"
"fmt"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func (suite *keeperTestSuite) TestDeposit_CreatePool_PoolNotAllowed() {
depositor := suite.CreateAccount(sdk.Coins{})
amountA := sdk.NewCoin("ukava", sdk.NewInt(10e6))
amountB := sdk.NewCoin("usdx", sdk.NewInt(50e6))
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), amountA, amountB, sdk.MustNewDecFromStr("0.01"))
suite.Require().EqualError(err, "not allowed: can not create pool 'ukava/usdx'")
}
func (suite *keeperTestSuite) TestDeposit_InsufficientFunds() {
testCases := []struct {
name string
balanceA sdk.Coin
balanceB sdk.Coin
depositA sdk.Coin
depositB sdk.Coin
}{
{
name: "no balance",
balanceA: sdk.Coin{},
balanceB: sdk.Coin{},
depositA: sdk.NewCoin("ukava", sdk.NewInt(100)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(100)),
},
{
name: "low balance",
balanceA: sdk.NewCoin("ukava", sdk.NewInt(1000000)),
balanceB: sdk.NewCoin("usdx", sdk.NewInt(1000000)),
depositA: sdk.NewCoin("ukava", sdk.NewInt(1000001)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(10000001)),
},
{
name: "large balance difference",
balanceA: sdk.NewCoin("ukava", sdk.NewInt(100e6)),
balanceB: sdk.NewCoin("usdx", sdk.NewInt(500e6)),
depositA: sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
pool := types.NewAllowedPool(tc.depositA.Denom, tc.depositB.Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, types.NewParams(types.NewAllowedPools(pool), types.DefaultSwapFee))
balance := sdk.Coins{tc.balanceA, tc.balanceB}
balance.Sort()
depositor := suite.CreateAccount(balance)
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), tc.depositA, tc.depositB, sdk.MustNewDecFromStr("0"))
// TODO: wrap in module specific error?
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds), fmt.Sprintf("got err %s", err))
suite.SetupTest()
// test deposit to existing pool insuffient funds
err = suite.CreatePool(sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.NewCoin("usdx", sdk.NewInt(50e6))))
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), tc.depositA, tc.depositB, sdk.MustNewDecFromStr("10"))
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds))
})
}
}
func (suite *keeperTestSuite) TestDeposit_InsufficientFunds_Vesting() {
testCases := []struct {
name string
balanceA sdk.Coin
balanceB sdk.Coin
vestingA sdk.Coin
vestingB sdk.Coin
depositA sdk.Coin
depositB sdk.Coin
}{
{
name: "no balance, vesting only",
balanceA: sdk.Coin{},
balanceB: sdk.Coin{},
vestingA: sdk.NewCoin("ukava", sdk.NewInt(100)),
vestingB: sdk.NewCoin("ukava", sdk.NewInt(100)),
depositA: sdk.NewCoin("ukava", sdk.NewInt(100)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(100)),
},
{
name: "vesting matches balance exactly",
balanceA: sdk.NewCoin("ukava", sdk.NewInt(1000000)),
balanceB: sdk.NewCoin("usdx", sdk.NewInt(1000000)),
vestingA: sdk.NewCoin("ukava", sdk.NewInt(1)),
vestingB: sdk.NewCoin("usdx", sdk.NewInt(1)),
depositA: sdk.NewCoin("ukava", sdk.NewInt(1000001)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(10000001)),
},
{
name: "large balance difference, vesting covers difference",
balanceA: sdk.NewCoin("ukava", sdk.NewInt(100e6)),
balanceB: sdk.NewCoin("usdx", sdk.NewInt(500e6)),
vestingA: sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
vestingB: sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
depositA: sdk.NewCoin("ukava", sdk.NewInt(1000e6)),
depositB: sdk.NewCoin("usdx", sdk.NewInt(5000e6)),
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
pool := types.NewAllowedPool(tc.depositA.Denom, tc.depositB.Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, types.NewParams(types.NewAllowedPools(pool), types.DefaultSwapFee))
balance := sdk.Coins{tc.balanceA, tc.balanceB}
balance.Sort()
vesting := sdk.Coins{tc.vestingA, tc.vestingB}
vesting.Sort()
depositor := suite.CreateVestingAccount(balance, vesting)
// test create pool insuffient funds
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), tc.depositA, tc.depositB, sdk.MustNewDecFromStr("0"))
// TODO: wrap in module specific error?
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds))
suite.SetupTest()
// test deposit to existing pool insuffient funds
err = suite.CreatePool(sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.NewCoin("usdx", sdk.NewInt(50e6))))
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), tc.depositA, tc.depositB, sdk.MustNewDecFromStr("4"))
suite.Require().True(errors.Is(err, sdkerrors.ErrInsufficientFunds))
})
}
}
func (suite *keeperTestSuite) TestDeposit_CreatePool() {
pool := types.NewAllowedPool("ukava", "usdx")
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, types.NewParams(types.NewAllowedPools(pool), types.DefaultSwapFee))
amountA := sdk.NewCoin(pool.TokenA, sdk.NewInt(11e6))
amountB := sdk.NewCoin(pool.TokenB, sdk.NewInt(51e6))
balance := sdk.NewCoins(amountA, amountB)
depositor := suite.CreateAccount(balance)
depositA := sdk.NewCoin(pool.TokenA, sdk.NewInt(10e6))
depositB := sdk.NewCoin(pool.TokenB, sdk.NewInt(50e6))
deposit := sdk.NewCoins(depositA, depositB)
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), depositA, depositB, sdk.MustNewDecFromStr("0"))
suite.Require().NoError(err)
suite.AccountBalanceEqual(depositor, sdk.NewCoins(amountA.Sub(depositA), amountB.Sub(depositB)))
suite.ModuleAccountBalanceEqual(sdk.NewCoins(depositA, depositB))
suite.PoolLiquidityEqual(deposit)
suite.PoolShareValueEqual(depositor, pool, deposit)
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
types.EventTypeSwapDeposit,
sdk.NewAttribute(types.AttributeKeyPoolID, pool.Name()),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, deposit.String()),
sdk.NewAttribute(types.AttributeKeyShares, "22360679"),
))
}
func (suite *keeperTestSuite) TestDeposit_PoolExists() {
pool := types.NewAllowedPool("ukava", "usdx")
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
err := suite.CreatePool(reserves)
suite.Require().NoError(err)
balance := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(5e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
)
depositor := suite.NewAccountFromAddr(sdk.AccAddress("new depositor"), balance)
depositA := sdk.NewCoin("usdx", depositor.GetCoins().AmountOf("usdx"))
depositB := sdk.NewCoin("ukava", depositor.GetCoins().AmountOf("ukava"))
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
err = suite.Keeper.Deposit(ctx, depositor.GetAddress(), depositA, depositB, sdk.MustNewDecFromStr("4"))
suite.Require().NoError(err)
expectedDeposit := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
)
expectedShareValue := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(999999)),
sdk.NewCoin("usdx", sdk.NewInt(4999998)),
)
suite.AccountBalanceEqual(depositor, balance.Sub(expectedDeposit))
suite.ModuleAccountBalanceEqual(reserves.Add(expectedDeposit...))
suite.PoolLiquidityEqual(reserves.Add(expectedDeposit...))
suite.PoolShareValueEqual(depositor, pool, expectedShareValue)
suite.EventsContains(ctx.EventManager().Events(), sdk.NewEvent(
types.EventTypeSwapDeposit,
sdk.NewAttribute(types.AttributeKeyPoolID, types.PoolID(pool.TokenA, pool.TokenB)),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, expectedDeposit.String()),
sdk.NewAttribute(types.AttributeKeyShares, "2236067"),
))
}
func (suite *keeperTestSuite) TestDeposit_MultipleDeposit() {
fundsToDeposit := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(5e6)),
sdk.NewCoin("usdx", sdk.NewInt(25e6)),
)
owner := suite.CreateAccount(fundsToDeposit)
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
initialShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, initialShares, owner.GetAddress())
depositA := sdk.NewCoin("usdx", owner.GetCoins().AmountOf("usdx"))
depositB := sdk.NewCoin("ukava", owner.GetCoins().AmountOf("ukava"))
err := suite.Keeper.Deposit(suite.Ctx, owner.GetAddress(), depositA, depositB, sdk.MustNewDecFromStr("4"))
suite.Require().NoError(err)
totalDeposit := reserves.Add(fundsToDeposit...)
totalShares := initialShares.Add(sdk.NewInt(15e6))
suite.AccountBalanceEqual(owner, sdk.Coins(nil))
suite.ModuleAccountBalanceEqual(totalDeposit)
suite.PoolLiquidityEqual(totalDeposit)
suite.PoolDepositorSharesEqual(owner.GetAddress(), poolID, totalShares)
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
types.EventTypeSwapDeposit,
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
sdk.NewAttribute(types.AttributeKeyDepositor, owner.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, fundsToDeposit.String()),
sdk.NewAttribute(types.AttributeKeyShares, "15000000"),
))
}
func (suite *keeperTestSuite) TestDeposit_Slippage() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
testCases := []struct {
depositA sdk.Coin
depositB sdk.Coin
slippage sdk.Dec
shouldFail bool
}{
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.7"), true},
{sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.8"), true},
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("3"), true},
{sdk.NewCoin("ukava", sdk.NewInt(5e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("4"), false},
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0"), false},
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4e6)), sdk.MustNewDecFromStr("0.25"), false},
{sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(4e6)), sdk.MustNewDecFromStr("0.2"), true},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("depositA=%s depositB=%s slippage=%s", tc.depositA, tc.depositB, tc.slippage), func() {
suite.SetupTest()
err := suite.CreatePool(reserves)
suite.Require().NoError(err)
balance := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(100e6)),
sdk.NewCoin("usdx", sdk.NewInt(100e6)),
)
depositor := suite.CreateAccount(balance)
ctx := suite.App.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
err = suite.Keeper.Deposit(ctx, depositor.GetAddress(), tc.depositA, tc.depositB, tc.slippage)
if tc.shouldFail {
suite.Require().Error(err)
suite.Contains(err.Error(), "slippage exceeded")
} else {
suite.NoError(err)
}
})
}
}
func (suite *keeperTestSuite) TestDeposit_InsufficientLiquidity() {
testCases := []struct {
poolA sdk.Coin
poolB sdk.Coin
poolShares sdk.Int
depositA sdk.Coin
depositB sdk.Coin
}{
// test deposit amount truncating to zero
{sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.NewCoin("usdx", sdk.NewInt(50e6)), sdk.NewInt(40e6), sdk.NewCoin("ukava", sdk.NewInt(1)), sdk.NewCoin("usdx", sdk.NewInt(1))},
// test share value rounding to zero
{sdk.NewCoin("ukava", sdk.NewInt(10e6)), sdk.NewCoin("usdx", sdk.NewInt(10e6)), sdk.NewInt(100), sdk.NewCoin("ukava", sdk.NewInt(1000)), sdk.NewCoin("usdx", sdk.NewInt(1000))},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("depositA=%s depositB=%s", tc.depositA, tc.depositB), func() {
suite.SetupTest()
record := types.PoolRecord{
PoolID: "ukava/usdx",
ReservesA: tc.poolA,
ReservesB: tc.poolB,
TotalShares: tc.poolShares,
}
suite.Keeper.SetPool(suite.Ctx, record)
balance := sdk.Coins{tc.depositA, tc.depositB}
balance.Sort()
depositor := suite.CreateAccount(balance)
err := suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), tc.depositA, tc.depositB, sdk.MustNewDecFromStr("10"))
suite.EqualError(err, "insufficient liquidity: deposit must be increased")
})
}
}

View File

@ -9,9 +9,14 @@ import (
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
) )
func i(in int64) sdk.Int { return sdk.NewInt(in) } //nolint
func i(in int64) sdk.Int { return sdk.NewInt(in) }
//nolint
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
//nolint
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func NewAuthGenStateFromAccs(accounts ...authexported.GenesisAccount) app.GenesisState { func NewAuthGenStateFromAccs(accounts ...authexported.GenesisAccount) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)

View File

@ -1,11 +1,12 @@
package keeper package keeper
import ( import (
"github.com/kava-labs/kava/x/swap/types"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace" "github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/swap/types"
) )
// Keeper keeper for the swap module // Keeper keeper for the swap module
@ -13,10 +14,18 @@ type Keeper struct {
key sdk.StoreKey key sdk.StoreKey
cdc *codec.Codec cdc *codec.Codec
paramSubspace subspace.Subspace paramSubspace subspace.Subspace
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
} }
// NewKeeper creates a new keeper // NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace) Keeper { func NewKeeper(
cdc *codec.Codec,
key sdk.StoreKey,
paramstore subspace.Subspace,
accountKeeper types.AccountKeeper,
supplyKeeper types.SupplyKeeper,
) Keeper {
if !paramstore.HasKeyTable() { if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
} }
@ -25,5 +34,141 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace)
key: key, key: key,
cdc: cdc, cdc: cdc,
paramSubspace: paramstore, paramSubspace: paramstore,
accountKeeper: accountKeeper,
supplyKeeper: supplyKeeper,
} }
} }
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.Params
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetPool retrieves a pool record from the store
func (k Keeper) GetPool(ctx sdk.Context, poolID string) (types.PoolRecord, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
bz := store.Get(types.PoolKey(poolID))
if bz == nil {
return types.PoolRecord{}, false
}
var record types.PoolRecord
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &record)
return record, true
}
// SetPool saves a pool record to the store
func (k Keeper) SetPool(ctx sdk.Context, record types.PoolRecord) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(record)
store.Set(types.PoolKey(record.PoolID), bz)
}
// DeletePool deletes a pool record from the store
func (k Keeper) DeletePool(ctx sdk.Context, poolID string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
store.Delete(types.PoolKey(poolID))
}
// IteratePools iterates over all pool objects in the store and performs a callback function
func (k Keeper) IteratePools(ctx sdk.Context, cb func(record types.PoolRecord) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PoolKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var record types.PoolRecord
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &record)
if cb(record) {
break
}
}
}
// GetAllPools returns all pool records from the store
func (k Keeper) GetAllPools(ctx sdk.Context) (records types.PoolRecords) {
k.IteratePools(ctx, func(record types.PoolRecord) bool {
records = append(records, record)
return false
})
return
}
// GetDepositorShares gets a share record from the store
func (k Keeper) GetDepositorShares(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (types.ShareRecord, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix)
bz := store.Get(types.DepositorPoolSharesKey(depositor, poolID))
if bz == nil {
return types.ShareRecord{}, false
}
var record types.ShareRecord
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &record)
return record, true
}
// SetDepositorShares saves a share record to the store
func (k Keeper) SetDepositorShares(ctx sdk.Context, record types.ShareRecord) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(record)
store.Set(types.DepositorPoolSharesKey(record.Depositor, record.PoolID), bz)
}
// DeleteDepositorShares deletes a share record from the store
func (k Keeper) DeleteDepositorShares(ctx sdk.Context, depositor sdk.AccAddress, poolID string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix)
store.Delete(types.DepositorPoolSharesKey(depositor, poolID))
}
// IterateDepositorShares iterates over all pool objects in the store and performs a callback function
func (k Keeper) IterateDepositorShares(ctx sdk.Context, cb func(record types.ShareRecord) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var record types.ShareRecord
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &record)
if cb(record) {
break
}
}
}
// GetAllDepositorShares returns all depositor share records from the store
func (k Keeper) GetAllDepositorShares(ctx sdk.Context) (records types.ShareRecords) {
k.IterateDepositorShares(ctx, func(record types.ShareRecord) bool {
records = append(records, record)
return false
})
return
}
// IterateDepositorSharesByOwner iterates over share records for a specific address and performs a callback function
func (k Keeper) IterateDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddress, cb func(record types.ShareRecord) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix)
iterator := sdk.KVStorePrefixIterator(store, owner.Bytes())
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var record types.ShareRecord
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &record)
if cb(record) {
break
}
}
}
// GetAllDepositorShares returns all depositor share records from the store for a specific address
func (k Keeper) GetAllDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddress) (records types.ShareRecords) {
k.IterateDepositorSharesByOwner(ctx, owner, func(record types.ShareRecord) bool {
records = append(records, record)
return false
})
return
}

View File

@ -0,0 +1,110 @@
package keeper_test
import (
"testing"
"github.com/kava-labs/kava/x/swap/testutil"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
)
type keeperTestSuite struct {
testutil.Suite
}
func (suite *keeperTestSuite) SetupTest() {
suite.Suite.SetupTest()
suite.Keeper.SetParams(suite.Ctx, types.DefaultParams())
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(keeperTestSuite))
}
func (suite *keeperTestSuite) setupPool(reserves sdk.Coins, totalShares sdk.Int, depositor sdk.AccAddress) string {
poolID := types.PoolIDFromCoins(reserves)
suite.AddCoinsToModule(reserves)
poolRecord := types.PoolRecord{
PoolID: poolID,
ReservesA: reserves[0],
ReservesB: reserves[1],
TotalShares: totalShares,
}
suite.Keeper.SetPool(suite.Ctx, poolRecord)
shareRecord := types.ShareRecord{
Depositor: depositor,
PoolID: poolID,
SharesOwned: totalShares,
}
suite.Keeper.SetDepositorShares(suite.Ctx, shareRecord)
return poolID
}
func (suite keeperTestSuite) TestParams_Persistance() {
keeper := suite.Keeper
params := types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("ukava", "usdx"),
},
SwapFee: sdk.MustNewDecFromStr("0.03"),
}
keeper.SetParams(suite.Ctx, params)
suite.Equal(keeper.GetParams(suite.Ctx), params)
oldParams := params
params = types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("hard", "ukava"),
},
SwapFee: sdk.MustNewDecFromStr("0.01"),
}
keeper.SetParams(suite.Ctx, params)
suite.NotEqual(keeper.GetParams(suite.Ctx), oldParams)
suite.Equal(keeper.GetParams(suite.Ctx), params)
}
func (suite *keeperTestSuite) TestPool_Persistance() {
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
pool, err := types.NewDenominatedPool(reserves)
suite.Nil(err)
record := types.NewPoolRecord(pool)
suite.Keeper.SetPool(suite.Ctx, record)
savedRecord, ok := suite.Keeper.GetPool(suite.Ctx, record.PoolID)
suite.True(ok)
suite.Equal(record, savedRecord)
suite.Keeper.DeletePool(suite.Ctx, record.PoolID)
deletedPool, ok := suite.Keeper.GetPool(suite.Ctx, record.PoolID)
suite.False(ok)
suite.Equal(deletedPool, types.PoolRecord{})
}
func (suite *keeperTestSuite) TestShare_Persistance() {
poolID := "ukava/usdx"
depositor := sdk.AccAddress("testAddress1")
shares := sdk.NewInt(3126432331)
record := types.NewShareRecord(depositor, poolID, shares)
suite.Keeper.SetDepositorShares(suite.Ctx, record)
savedRecord, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor, poolID)
suite.True(ok)
suite.Equal(record, savedRecord)
suite.Keeper.DeleteDepositorShares(suite.Ctx, depositor, poolID)
deletedShares, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor, poolID)
suite.False(ok)
suite.Equal(deletedShares, types.ShareRecord{})
}

View File

@ -1,19 +0,0 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/swap/types"
)
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.Params
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}

View File

@ -1,39 +0,0 @@
package keeper_test
import (
"testing"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func TestParams_SetterAndGetter(t *testing.T) {
tApp := app.NewTestApp()
keeper := tApp.GetSwapKeeper()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
params := types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("ukava", "usdx"),
},
SwapFee: sdk.MustNewDecFromStr("0.03"),
}
keeper.SetParams(ctx, params)
assert.Equal(t, keeper.GetParams(ctx), params)
oldParams := params
params = types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("hard", "ukava"),
},
SwapFee: sdk.MustNewDecFromStr("0.01"),
}
keeper.SetParams(ctx, params)
assert.NotEqual(t, keeper.GetParams(ctx), oldParams)
assert.Equal(t, keeper.GetParams(ctx), params)
}

View File

@ -1,6 +1,9 @@
package keeper package keeper
import ( import (
"strings"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -16,6 +19,12 @@ func NewQuerier(k Keeper) sdk.Querier {
switch path[0] { switch path[0] {
case types.QueryGetParams: case types.QueryGetParams:
return queryGetParams(ctx, req, k) return queryGetParams(ctx, req, k)
case types.QueryGetDeposits:
return queryGetDeposits(ctx, req, k)
case types.QueryGetPool:
return queryGetPool(ctx, req, k)
case types.QueryGetPools:
return queryGetPools(ctx, req, k)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
} }
@ -34,3 +43,135 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by
} }
return bz, nil return bz, nil
} }
func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryDepositsParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
var records types.ShareRecords
if len(params.Owner) > 0 {
records = k.GetAllDepositorSharesByOwner(ctx, params.Owner)
} else {
unfilteredRecords := k.GetAllDepositorShares(ctx)
records = filterShareRecords(ctx, unfilteredRecords, params)
}
// Augment each deposit result with the actual share value of depositor's shares
var queryResults types.DepositsQueryResults
for _, record := range records {
pool, err := k.loadDenominatedPool(ctx, record.PoolID)
if err != nil {
return nil, err
}
shareValue := pool.ShareValue(record.SharesOwned)
queryResult := types.NewDepositsQueryResult(record, shareValue)
queryResults = append(queryResults, queryResult)
}
var bz []byte
bz, err = codec.MarshalJSONIndent(types.ModuleCdc, queryResults)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetPool(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryPoolParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
hasPoolParam := len(params.Pool) > 0
if !hasPoolParam {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "must specify pool param")
}
pool, err := k.loadDenominatedPool(ctx, params.Pool)
if err != nil {
return nil, err
}
totalCoins := pool.ShareValue(pool.TotalShares())
poolStats := types.NewPoolStatsQueryResult(params.Pool, totalCoins, pool.TotalShares())
var bz []byte
bz, err = codec.MarshalJSONIndent(types.ModuleCdc, poolStats)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetPools(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
pools := k.GetAllPools(ctx)
var queryResults types.PoolStatsQueryResults
for _, pool := range pools {
denomPool, err := k.loadDenominatedPool(ctx, pool.PoolID)
if err != nil {
return nil, err
}
totalCoins := denomPool.ShareValue(denomPool.TotalShares())
queryResult := types.NewPoolStatsQueryResult(pool.PoolID, totalCoins, denomPool.TotalShares())
queryResults = append(queryResults, queryResult)
}
// Encode results
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, queryResults)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// filterShareRecords retrieves share records filtered by a given set of params.
// If no filters are provided, all share records will be returned in paginated form.
func filterShareRecords(ctx sdk.Context, records types.ShareRecords, params types.QueryDepositsParams) types.ShareRecords {
filteredRecords := make(types.ShareRecords, 0, len(records))
for _, s := range records {
matchOwner, matchPool := true, true
// match owner address (if supplied)
if len(params.Owner) > 0 {
matchOwner = s.Depositor.Equals(params.Owner)
}
// match pool ID (if supplied)
if len(params.Pool) > 0 {
matchPool = strings.Compare(s.PoolID, params.Pool) == 0
}
if matchOwner && matchPool {
filteredRecords = append(filteredRecords, s)
}
}
start, end := client.Paginate(len(filteredRecords), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
filteredRecords = types.ShareRecords{}
} else {
filteredRecords = filteredRecords[start:end]
}
return filteredRecords
}
func (k Keeper) loadDenominatedPool(ctx sdk.Context, poolID string) (*types.DenominatedPool, error) {
poolRecord, found := k.GetPool(ctx, poolID)
if !found {
return &types.DenominatedPool{}, types.ErrInvalidPool
}
denominatedPool, err := types.NewDenominatedPoolWithExistingShares(poolRecord.Reserves(), poolRecord.TotalShares)
if err != nil {
return &types.DenominatedPool{}, types.ErrInvalidPool
}
return denominatedPool, nil
}

View File

@ -1,51 +1,53 @@
package keeper_test package keeper_test
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap/keeper" "github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/testutil"
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
) )
type QuerierTestSuite struct { type querierTestSuite struct {
suite.Suite testutil.Suite
keeper keeper.Keeper querier sdk.Querier
app app.TestApp addresses []sdk.AccAddress
ctx sdk.Context
querier sdk.Querier
} }
func (suite *QuerierTestSuite) SetupTest() { func (suite *querierTestSuite) SetupTest() {
tApp := app.NewTestApp() suite.Suite.SetupTest()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates( // Set up auth GenesisState
_, addrs := app.GeneratePrivKeyAddressPairs(5)
coins := []sdk.Coins{}
for j := 0; j < 5; j++ {
coins = append(coins, cs(c("ukava", 10000000000), c("bnb", 10000000000), c("usdx", 10000000000)))
}
suite.addresses = addrs
authGS := app.NewAuthGenState(addrs, coins)
suite.App.InitializeFromGenesisStates(
authGS,
NewSwapGenStateMulti(), NewSwapGenStateMulti(),
) )
suite.querier = keeper.NewQuerier(suite.Keeper)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetSwapKeeper()
suite.querier = keeper.NewQuerier(suite.keeper)
} }
func (suite *QuerierTestSuite) TestUnkownRequest() { func (suite *querierTestSuite) TestUnkownRequest() {
ctx := suite.ctx.WithIsCheckTx(false) ctx := suite.Ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{"invalid-path"}, abci.RequestQuery{}) bz, err := suite.querier(ctx, []string{"invalid-path"}, abci.RequestQuery{})
suite.Nil(bz) suite.Nil(bz)
suite.EqualError(err, "unknown request: unknown swap query endpoint") suite.EqualError(err, "unknown request: unknown swap query endpoint")
} }
func (suite *QuerierTestSuite) TestQueryParams() { func (suite *querierTestSuite) TestQueryParams() {
ctx := suite.ctx.WithIsCheckTx(false) ctx := suite.Ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{}) bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Nil(err) suite.Nil(err)
suite.NotNil(bz) suite.NotNil(bz)
@ -55,11 +57,117 @@ func (suite *QuerierTestSuite) TestQueryParams() {
swapGenesisState := NewSwapGenStateMulti() swapGenesisState := NewSwapGenStateMulti()
gs := types.GenesisState{} gs := types.GenesisState{}
types.ModuleCdc.UnmarshalJSON(swapGenesisState["swap"], &gs) err = types.ModuleCdc.UnmarshalJSON(swapGenesisState["swap"], &gs)
suite.Require().NoError(err)
suite.Equal(gs.Params, p) suite.Equal(gs.Params, p)
} }
func TestQuerierTestSuite(t *testing.T) { func (suite *querierTestSuite) TestQueryPool() {
suite.Run(t, new(QuerierTestSuite)) // Set up pool in store
coinA := sdk.NewCoin("ukava", sdk.NewInt(10))
coinB := sdk.NewCoin("usdx", sdk.NewInt(200))
pool, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
suite.Nil(err)
poolRecord := types.NewPoolRecord(pool)
suite.Keeper.SetPool(suite.Ctx, poolRecord)
ctx := suite.Ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{"custom", types.QuerierRoute, types.QueryGetPool}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryPoolParams(poolRecord.PoolID)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetPool}, query)
suite.Nil(err)
suite.NotNil(bz)
var res types.PoolStatsQueryResult
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &res))
// Check that result matches expected result
totalCoins := pool.ShareValue(pool.TotalShares())
expectedResult := types.NewPoolStatsQueryResult(poolRecord.PoolID, totalCoins, pool.TotalShares())
suite.Equal(expectedResult, res)
}
func (suite *querierTestSuite) TestQueryPools() {
// Set up pools in store
coinA := sdk.NewCoin("ukava", sdk.NewInt(10))
coinB := sdk.NewCoin("usdx", sdk.NewInt(200))
coinC := sdk.NewCoin("usdx", sdk.NewInt(200))
poolAB, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
suite.Nil(err)
poolRecordAB := types.NewPoolRecord(poolAB)
suite.Keeper.SetPool(suite.Ctx, poolRecordAB)
poolAC, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinC))
suite.Nil(err)
poolRecordAC := types.NewPoolRecord(poolAC)
suite.Keeper.SetPool(suite.Ctx, poolRecordAC)
// Build a map of pools to compare to query results
pools := []types.PoolRecord{poolRecordAB, poolRecordAC}
poolsMap := make(map[string]types.PoolRecord)
for _, pool := range pools {
poolsMap[pool.PoolID] = pool
}
ctx := suite.Ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{types.QueryGetPools}, abci.RequestQuery{})
suite.Nil(err)
suite.NotNil(bz)
var res types.PoolStatsQueryResults
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &res))
// Check that all pools are accounted for
suite.Equal(len(poolsMap), len(res))
// Check that each individual result matches the expected result
for _, pool := range res {
expectedPool, ok := poolsMap[pool.Name]
suite.True(ok)
suite.Equal(expectedPool.PoolID, pool.Name)
suite.Equal(sdk.NewCoins(expectedPool.ReservesA, expectedPool.ReservesB), pool.Coins)
suite.Equal(expectedPool.TotalShares, pool.TotalShares)
}
}
func (suite *querierTestSuite) TestQueryDeposit() {
// Set up pool in store
coinA := sdk.NewCoin("ukava", sdk.NewInt(10))
coinB := sdk.NewCoin("usdx", sdk.NewInt(200))
pool, err := types.NewDenominatedPool(sdk.NewCoins(coinA, coinB))
suite.Nil(err)
poolRecord := types.NewPoolRecord(pool)
suite.Keeper.SetPool(suite.Ctx, poolRecord)
// Deposit into pool
owner := suite.addresses[0]
err = suite.Keeper.Deposit(suite.Ctx, owner, coinA, coinB, sdk.MustNewDecFromStr("0.20"))
suite.Nil(err)
ctx := suite.Ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{"custom", types.QuerierRoute, types.QueryGetDeposits}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryDepositsParams(1, 100, owner, poolRecord.PoolID)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetDeposits}, query)
suite.Nil(err)
suite.NotNil(bz)
var res types.DepositsQueryResults
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &res))
// As the only depositor all pool shares should belong to the owner
suite.Equal(poolRecord.TotalShares, res[0].SharesOwned)
}
func TestQuerierTestSuite(t *testing.T) {
suite.Run(t, new(querierTestSuite))
} }

90
x/swap/keeper/withdraw.go Normal file
View File

@ -0,0 +1,90 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/swap/types"
)
// Withdraw removes liquidity from an existing pool from an owners deposit, converting the provided shares for
// the returned pool liquidity.
//
// If 100% of the owners shares are removed, then the deposit is deleted. In addition, if all the pool shares
// are removed then the pool is deleted.
//
// The number of shares must be large enough to result in at least 1 unit of the smallest reserve in the pool.
// If the share input is below the minimum required for positive liquidity to be remove from both reserves, a
// insufficient error is returned.
//
// In addition, if the withdrawn liquidity for each reserve is below the provided minimum, a slippage exceeded
// error is returned.
func (k Keeper) Withdraw(ctx sdk.Context, owner sdk.AccAddress, shares sdk.Int, minCoinA, minCoinB sdk.Coin) error {
poolID := types.PoolID(minCoinA.Denom, minCoinB.Denom)
shareRecord, found := k.GetDepositorShares(ctx, owner, poolID)
if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit for account %s and pool %s", owner, poolID)
}
if shares.GT(shareRecord.SharesOwned) {
return sdkerrors.Wrapf(types.ErrInvalidShares, "withdraw of %s shares greater than %s shares owned", shares, shareRecord.SharesOwned)
}
poolRecord, found := k.GetPool(ctx, poolID)
if !found {
panic(fmt.Sprintf("pool %s not found", poolID))
}
pool, err := types.NewDenominatedPoolWithExistingShares(poolRecord.Reserves(), poolRecord.TotalShares)
if err != nil {
panic(fmt.Sprintf("invalid pool %s: %s", poolID, err))
}
withdrawnAmount := pool.RemoveLiquidity(shares)
if withdrawnAmount.AmountOf(minCoinA.Denom).IsZero() || withdrawnAmount.AmountOf(minCoinB.Denom).IsZero() {
return sdkerrors.Wrap(types.ErrInsufficientLiquidity, "shares must be increased")
}
if withdrawnAmount.AmountOf(minCoinA.Denom).LT(minCoinA.Amount) || withdrawnAmount.AmountOf(minCoinB.Denom).LT(minCoinB.Amount) {
return sdkerrors.Wrap(types.ErrSlippageExceeded, "minimum withdraw not met")
}
k.updatePool(ctx, poolID, pool)
k.updateShares(ctx, owner, poolID, shareRecord.SharesOwned.Sub(shares))
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, owner, withdrawnAmount)
if err != nil {
panic(err)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSwapWithdraw,
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
sdk.NewAttribute(types.AttributeKeyOwner, owner.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, withdrawnAmount.String()),
sdk.NewAttribute(types.AttributeKeyShares, shares.String()),
),
)
return nil
}
func (k Keeper) updatePool(ctx sdk.Context, poolID string, pool *types.DenominatedPool) {
if pool.TotalShares().IsZero() {
k.DeletePool(ctx, poolID)
} else {
k.SetPool(ctx, types.NewPoolRecord(pool))
}
}
func (k Keeper) updateShares(ctx sdk.Context, owner sdk.AccAddress, poolID string, shares sdk.Int) {
if shares.IsZero() {
k.DeleteDepositorShares(ctx, owner, poolID)
} else {
shareRecord := types.NewShareRecord(owner, poolID, shares)
k.SetDepositorShares(ctx, shareRecord)
}
}

View File

@ -0,0 +1,223 @@
package keeper_test
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/swap/types"
)
func (suite *keeperTestSuite) TestWithdraw_AllShares() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
err := suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), totalShares, reserves[0], reserves[1])
suite.Require().NoError(err)
suite.PoolDeleted(reserves[0].Denom, reserves[1].Denom)
suite.PoolSharesDeleted(owner.GetAddress(), reserves[0].Denom, reserves[1].Denom)
suite.AccountBalanceEqual(owner, reserves)
suite.ModuleAccountBalanceEqual(sdk.Coins(nil))
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
types.EventTypeSwapWithdraw,
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
sdk.NewAttribute(types.AttributeKeyOwner, owner.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, reserves.String()),
sdk.NewAttribute(types.AttributeKeyShares, totalShares.String()),
))
}
func (suite *keeperTestSuite) TestWithdraw_PartialShares() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
sharesToWithdraw := sdk.NewInt(15e6)
minCoinA := sdk.NewCoin("usdx", sdk.NewInt(25e6))
minCoinB := sdk.NewCoin("ukava", sdk.NewInt(5e6))
err := suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), sharesToWithdraw, minCoinA, minCoinB)
suite.Require().NoError(err)
sharesLeft := totalShares.Sub(sharesToWithdraw)
reservesLeft := sdk.NewCoins(reserves[0].Sub(minCoinB), reserves[1].Sub(minCoinA))
suite.PoolShareTotalEqual(poolID, sharesLeft)
suite.PoolDepositorSharesEqual(owner.GetAddress(), poolID, sharesLeft)
suite.PoolReservesEqual(poolID, reservesLeft)
suite.AccountBalanceEqual(owner, sdk.NewCoins(minCoinA, minCoinB))
suite.ModuleAccountBalanceEqual(reservesLeft)
suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
types.EventTypeSwapWithdraw,
sdk.NewAttribute(types.AttributeKeyPoolID, poolID),
sdk.NewAttribute(types.AttributeKeyOwner, owner.GetAddress().String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, sdk.NewCoins(minCoinA, minCoinB).String()),
sdk.NewAttribute(types.AttributeKeyShares, sharesToWithdraw.String()),
))
}
func (suite *keeperTestSuite) TestWithdraw_NoSharesOwned() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
accWithNoDeposit := sdk.AccAddress("some account")
err := suite.Keeper.Withdraw(suite.Ctx, accWithNoDeposit, totalShares, reserves[0], reserves[1])
suite.EqualError(err, fmt.Sprintf("deposit not found: no deposit for account %s and pool %s", accWithNoDeposit.String(), poolID))
}
func (suite *keeperTestSuite) TestWithdraw_GreaterThanSharesOwned() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
suite.setupPool(reserves, totalShares, owner.GetAddress())
sharesToWithdraw := totalShares.Add(sdk.OneInt())
err := suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), sharesToWithdraw, reserves[0], reserves[1])
suite.EqualError(err, fmt.Sprintf("invalid shares: withdraw of %s shares greater than %s shares owned", sharesToWithdraw, totalShares))
}
func (suite *keeperTestSuite) TestWithdraw_MinWithdraw() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
testCases := []struct {
shares sdk.Int
minCoinA sdk.Coin
minCoinB sdk.Coin
shouldFail bool
}{
{sdk.NewInt(1), sdk.NewCoin("ukava", sdk.NewInt(1)), sdk.NewCoin("usdx", sdk.NewInt(1)), true},
{sdk.NewInt(1), sdk.NewCoin("usdx", sdk.NewInt(5)), sdk.NewCoin("ukava", sdk.NewInt(1)), true},
{sdk.NewInt(2), sdk.NewCoin("ukava", sdk.NewInt(1)), sdk.NewCoin("usdx", sdk.NewInt(1)), true},
{sdk.NewInt(2), sdk.NewCoin("usdx", sdk.NewInt(5)), sdk.NewCoin("ukava", sdk.NewInt(1)), true},
{sdk.NewInt(3), sdk.NewCoin("ukava", sdk.NewInt(1)), sdk.NewCoin("usdx", sdk.NewInt(5)), false},
{sdk.NewInt(3), sdk.NewCoin("usdx", sdk.NewInt(5)), sdk.NewCoin("ukava", sdk.NewInt(1)), false},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("shares=%s minCoinA=%s minCoinB=%s", tc.shares, tc.minCoinA, tc.minCoinB), func() {
suite.SetupTest()
suite.setupPool(reserves, totalShares, owner.GetAddress())
err := suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), tc.shares, tc.minCoinA, tc.minCoinB)
if tc.shouldFail {
suite.EqualError(err, "insufficient liquidity: shares must be increased")
} else {
suite.NoError(err, "expected no liquidity error")
}
})
}
}
func (suite *keeperTestSuite) TestWithdraw_BelowMinimum() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
testCases := []struct {
shares sdk.Int
minCoinA sdk.Coin
minCoinB sdk.Coin
shouldFail bool
}{
{sdk.NewInt(15e6), sdk.NewCoin("ukava", sdk.NewInt(5000001)), sdk.NewCoin("usdx", sdk.NewInt(25e6)), true},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("shares=%s minCoinA=%s minCoinB=%s", tc.shares, tc.minCoinA, tc.minCoinB), func() {
suite.SetupTest()
suite.setupPool(reserves, totalShares, owner.GetAddress())
err := suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), tc.shares, tc.minCoinA, tc.minCoinB)
if tc.shouldFail {
suite.EqualError(err, "slippage exceeded: minimum withdraw not met")
} else {
suite.NoError(err, "expected no slippage error")
}
})
}
}
func (suite *keeperTestSuite) TestWithdraw_PanicOnMissingPool() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
suite.Keeper.DeletePool(suite.Ctx, poolID)
suite.PanicsWithValue("pool ukava/usdx not found", func() {
_ = suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), totalShares, reserves[0], reserves[1])
}, "expected missing pool record to panic")
}
func (suite *keeperTestSuite) TestWithdraw_PanicOnInvalidPool() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
poolID := suite.setupPool(reserves, totalShares, owner.GetAddress())
poolRecord, found := suite.Keeper.GetPool(suite.Ctx, poolID)
suite.Require().True(found, "expected pool record to exist")
poolRecord.TotalShares = sdk.ZeroInt()
suite.Keeper.SetPool(suite.Ctx, poolRecord)
suite.PanicsWithValue("invalid pool ukava/usdx: invalid pool: total shares must be greater than zero", func() {
_ = suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), totalShares, reserves[0], reserves[1])
}, "expected invalid pool record to panic")
}
func (suite *keeperTestSuite) TestWithdraw_PanicOnModuleInsufficientFunds() {
owner := suite.CreateAccount(sdk.Coins{})
reserves := sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(10e6)),
sdk.NewCoin("usdx", sdk.NewInt(50e6)),
)
totalShares := sdk.NewInt(30e6)
suite.setupPool(reserves, totalShares, owner.GetAddress())
suite.RemoveCoinsFromModule(sdk.NewCoins(
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
))
suite.Panics(func() {
_ = suite.Keeper.Withdraw(suite.Ctx, owner.GetAddress(), totalShares, reserves[0], reserves[1])
}, "expected panic when module account does not have enough funds")
}

View File

@ -0,0 +1,206 @@
package v0_11
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
)
const (
// ModuleName name that will be used throughout the module
ModuleName = "swap"
)
// Parameter keys and default values
var (
KeyAllowedPools = []byte("AllowedPools")
KeySwapFee = []byte("SwapFee")
DefaultAllowedPools = AllowedPools{}
DefaultSwapFee = sdk.ZeroDec()
MaxSwapFee = sdk.OneDec()
)
// AllowedPool defines a tradable pool
type AllowedPool struct {
TokenA string `json:"token_a" yaml:"token_a"`
TokenB string `json:"token_b" yaml:"token_b"`
}
// NewAllowedPool returns a new AllowedPool object
func NewAllowedPool(tokenA, tokenB string) AllowedPool {
return AllowedPool{
TokenA: tokenA,
TokenB: tokenB,
}
}
// Validate validates allowedPool attributes and returns an error if invalid
func (p AllowedPool) Validate() error {
err := sdk.ValidateDenom(p.TokenA)
if err != nil {
return err
}
err = sdk.ValidateDenom(p.TokenB)
if err != nil {
return err
}
if p.TokenA == p.TokenB {
return fmt.Errorf(
"pool cannot have two tokens of the same type, received '%s' and '%s'",
p.TokenA, p.TokenB,
)
}
if p.TokenA > p.TokenB {
return fmt.Errorf(
"invalid token order: '%s' must come before '%s'",
p.TokenB, p.TokenA,
)
}
return nil
}
// Name returns a unique name for a allowedPool in alphabetical order
func (p AllowedPool) Name() string {
return fmt.Sprintf("%s/%s", p.TokenA, p.TokenB)
}
// String pretty prints the allowedPool
func (p AllowedPool) String() string {
return fmt.Sprintf(`AllowedPool:
Name: %s
Token A: %s
Token B: %s
`, p.Name(), p.TokenA, p.TokenB)
}
// AllowedPools is a slice of AllowedPool
type AllowedPools []AllowedPool
// NewAllowedPools returns AllowedPools from the provided values
func NewAllowedPools(allowedPools ...AllowedPool) AllowedPools {
return AllowedPools(allowedPools)
}
// Validate validates each allowedPool and returns an error if there are any duplicates
func (p AllowedPools) Validate() error {
seenAllowedPools := make(map[string]bool)
for _, allowedPool := range p {
err := allowedPool.Validate()
if err != nil {
return err
}
if seen := seenAllowedPools[allowedPool.Name()]; seen {
return fmt.Errorf("duplicate pool: %s", allowedPool.Name())
}
seenAllowedPools[allowedPool.Name()] = true
}
return nil
}
// Params are governance parameters for the swap module
type Params struct {
AllowedPools AllowedPools `json:"allowed_pools" yaml:"allowed_pools"`
SwapFee sdk.Dec `json:"swap_fee" yaml:"swap_fee"`
}
// NewParams returns a new params object
func NewParams(pairs AllowedPools, swapFee sdk.Dec) Params {
return Params{
AllowedPools: pairs,
SwapFee: swapFee,
}
}
// DefaultParams returns default params for swap module
func DefaultParams() Params {
return NewParams(
DefaultAllowedPools,
DefaultSwapFee,
)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
AllowedPools: %s
SwapFee: %s`,
p.AllowedPools, p.SwapFee)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyAllowedPools, &p.AllowedPools, validateAllowedPoolsParams),
params.NewParamSetPair(KeySwapFee, &p.SwapFee, validateSwapFee),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateAllowedPoolsParams(p.AllowedPools); err != nil {
return err
}
return validateSwapFee(p.SwapFee)
}
func validateAllowedPoolsParams(i interface{}) error {
p, ok := i.(AllowedPools)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return p.Validate()
}
func validateSwapFee(i interface{}) error {
swapFee, ok := i.(sdk.Dec)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if swapFee.IsNil() || swapFee.IsNegative() || swapFee.GT(MaxSwapFee) {
return fmt.Errorf(fmt.Sprintf("invalid swap fee: %s", swapFee))
}
return nil
}
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
}
// NewGenesisState creates a new genesis state.
func NewGenesisState(params Params) GenesisState {
return GenesisState{
Params: params,
}
}
// Validate validates the module's genesis state
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
return nil
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultParams(),
)
}

View File

@ -2,6 +2,7 @@ package swap
import ( import (
"encoding/json" "encoding/json"
"math/rand"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -10,20 +11,21 @@ import (
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/types/module"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
// "github.com/kava-labs/kava/x/swap/simulation"
"github.com/kava-labs/kava/x/swap/client/cli" "github.com/kava-labs/kava/x/swap/client/cli"
"github.com/kava-labs/kava/x/swap/client/rest" "github.com/kava-labs/kava/x/swap/client/rest"
"github.com/kava-labs/kava/x/swap/keeper" "github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/simulation"
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
) )
var ( var (
_ module.AppModule = AppModule{} _ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{} _ module.AppModuleBasic = AppModuleBasic{}
// _ module.AppModuleSimulation = AppModule{} _ module.AppModuleSimulation = AppModule{}
) )
// AppModuleBasic app module basics object // AppModuleBasic app module basics object
@ -140,27 +142,27 @@ func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.Valid
//____________________________________________________________________________ //____________________________________________________________________________
// // GenerateGenesisState creates a randomized GenState of the swap module // GenerateGenesisState creates a randomized GenState of the swap module
// func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) { func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
// simulation.RandomizedGenState(simState) simulation.RandomizedGenState(simState)
// } }
// // ProposalContents doesn't return any content functions for governance proposals. // ProposalContents doesn't return any content functions for governance proposals.
// func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent { func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
// return nil return nil
// } }
// // RandomizedParams returns nil because swap has no params. // RandomizedParams returns nil because swap has no params.
// func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange { func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
// return simulation.ParamChanges(r) return simulation.ParamChanges(r)
// } }
// // RegisterStoreDecoder registers a decoder for swap module's types // RegisterStoreDecoder registers a decoder for swap module's types
// func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
// sdr[StoreKey] = simulation.DecodeStore sdr[StoreKey] = simulation.DecodeStore
// } }
// // WeightedOperations returns the all the swap module operations with their respective weights. // WeightedOperations returns the all the swap module operations with their respective weights.
// func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
// return nil return nil
// } }

View File

@ -0,0 +1,13 @@
package simulation
import (
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
)
// DecodeStore unmarshals the KVPair's Value to the module's corresponding type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
// TODO: as store keys are added to the module, test marshal/unmarshal of each key prefix
return ""
}

View File

@ -0,0 +1,100 @@
package simulation
import (
"fmt"
"math/rand"
"sort"
"strings"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/swap/types"
)
var (
//nolint
accs []simulation.Account
consistentPools = [2][2]string{{"ukava", "usdx"}, {"hard", "usdx"}}
)
// GenSwapFee generates a random SwapFee in range [0.01, 1.00]
func GenSwapFee(r *rand.Rand) sdk.Dec {
min := int(1)
max := int(100)
percentage := int64(r.Intn(int(max)-min) + min)
return sdk.NewDec(percentage).Quo(sdk.NewDec(100))
}
// GenAllowedPools generates random allowed pools
func GenAllowedPools(r *rand.Rand) types.AllowedPools {
var pools types.AllowedPools
// Generate a set [1, 10] of random pools
numRandPools := (r.Intn(10) + 1)
for i := 0; i < numRandPools; i++ {
tokenA, tokenB := genTokenDenoms(r)
for strings.Compare(tokenA, tokenB) == 0 {
tokenA, tokenB = genTokenDenoms(r)
}
newPool := types.NewAllowedPool(tokenA, tokenB)
pools = append(pools, newPool)
}
// Append consistent pools
for i := 0; i < len(consistentPools); i++ {
tokenA := consistentPools[i][0]
tokenB := consistentPools[i][1]
newPool := types.NewAllowedPool(tokenA, tokenB)
pools = append(pools, newPool)
}
return pools
}
func genTokenDenoms(r *rand.Rand) (string, string) {
tokenA := genTokenDenom(r)
tokenB := genTokenDenom(r)
for strings.Compare(tokenA, tokenB) == 0 {
tokenA = genTokenDenom(r)
}
tokens := []string{tokenA, tokenB}
sort.Strings(tokens)
return tokens[0], tokens[1]
}
func genTokenDenom(r *rand.Rand) string {
denom := strings.ToLower(simulation.RandStringOfLength(r, 3))
for err := sdk.ValidateDenom(denom); err != nil; {
denom = strings.ToLower(simulation.RandStringOfLength(r, 3))
}
return denom
}
// RandomizedGenState generates a random GenesisState
func RandomizedGenState(simState *module.SimulationState) {
accs = simState.Accounts
swapGenesis := loadRandomSwapGenState(simState)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, swapGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(swapGenesis)
}
func loadRandomSwapGenState(simState *module.SimulationState) types.GenesisState {
pools := GenAllowedPools(simState.Rand)
swapFee := GenSwapFee(simState.Rand)
swapGenesis := types.GenesisState{
Params: types.Params{
AllowedPools: pools,
SwapFee: swapFee,
},
}
if err := swapGenesis.Validate(); err != nil {
panic(err)
}
return swapGenesis
}

View File

@ -0,0 +1,12 @@
package simulation
import (
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/swap/types"
)
var (
//nolint
noOpMsg = simulation.NoOpMsg(types.ModuleName)
)

View File

@ -0,0 +1,32 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/swap/types"
)
const (
keyAllowedPools = "AllowedPools"
keySwapFee = "SwapFee"
)
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []simulation.ParamChange{
simulation.NewSimParamChange(types.ModuleName, keyAllowedPools,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%s\"", GenAllowedPools(r))
},
),
simulation.NewSimParamChange(types.ModuleName, keySwapFee,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%s\"", GenSwapFee(r))
},
),
}
}

281
x/swap/testutil/suite.go Normal file
View File

@ -0,0 +1,281 @@
package testutil
import (
"fmt"
"reflect"
"time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
kv "github.com/tendermint/tendermint/libs/kv"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Suite implements a test suite for the swap module integration tests
type Suite struct {
suite.Suite
Keeper swap.Keeper
App app.TestApp
Ctx sdk.Context
bankKeeper bank.Keeper
supplyKeeper supply.Keeper
}
// SetupTest instantiates a new app, keepers, and sets suite state
func (suite *Suite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
keeper := tApp.GetSwapKeeper()
bankKeeper := tApp.GetBankKeeper()
supplyKeeper := tApp.GetSupplyKeeper()
suite.Ctx = ctx
suite.App = tApp
suite.Keeper = keeper
suite.bankKeeper = bankKeeper
suite.supplyKeeper = supplyKeeper
}
// AddCoinsToModule adds coins to the swap module account
func (suite *Suite) AddCoinsToModule(amount sdk.Coins) {
macc, _ := suite.supplyKeeper.GetModuleAccountAndPermissions(suite.Ctx, swap.ModuleName)
_, err := suite.bankKeeper.AddCoins(suite.Ctx, macc.GetAddress(), amount)
suite.Require().NoError(err)
}
// RemoveCoinsFromModule adds coins to the swap module account
func (suite *Suite) RemoveCoinsFromModule(amount sdk.Coins) {
macc, _ := suite.supplyKeeper.GetModuleAccountAndPermissions(suite.Ctx, swap.ModuleName)
_, err := suite.bankKeeper.SubtractCoins(suite.Ctx, macc.GetAddress(), amount)
suite.Require().NoError(err)
}
// GetAccount gets an existing account
func (suite *Suite) GetAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.App.GetAccountKeeper()
return ak.GetAccount(suite.Ctx, addr)
}
// CreateAccount creates a new account from the provided balance
func (suite *Suite) CreateAccount(initialBalance sdk.Coins) authexported.Account {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ak := suite.App.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.Ctx, addrs[0])
err := acc.SetCoins(initialBalance)
suite.Require().NoError(err)
ak.SetAccount(suite.Ctx, acc)
return acc
}
// NewAccountFromAddr creates a new account from the provided address with the provided balance
func (suite *Suite) NewAccountFromAddr(addr sdk.AccAddress, balance sdk.Coins) authexported.Account {
ak := suite.App.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.Ctx, addr)
err := acc.SetCoins(balance)
suite.Require().NoError(err)
ak.SetAccount(suite.Ctx, acc)
return acc
}
// CreateVestingAccount creats a new vesting account from the provided balance and vesting balance
func (suite *Suite) CreateVestingAccount(initialBalance sdk.Coins, vestingBalance sdk.Coins) authexported.Account {
acc := suite.CreateAccount(initialBalance)
bacc := acc.(*auth.BaseAccount)
periods := vestingtypes.Periods{
vestingtypes.Period{
Length: 31556952,
Amount: vestingBalance,
},
}
vacc := vestingtypes.NewPeriodicVestingAccount(bacc, time.Now().Unix(), periods)
return vacc
}
// CreatePool creates a pool and stores it in state with the provided reserves
func (suite *Suite) CreatePool(reserves sdk.Coins) error {
depositor := suite.CreateAccount(reserves)
pool := swap.NewAllowedPool(reserves[0].Denom, reserves[1].Denom)
suite.Require().NoError(pool.Validate())
suite.Keeper.SetParams(suite.Ctx, swap.NewParams(swap.NewAllowedPools(pool), swap.DefaultSwapFee))
return suite.Keeper.Deposit(suite.Ctx, depositor.GetAddress(), reserves[0], reserves[1], sdk.MustNewDecFromStr("1"))
}
// AccountBalanceEqual asserts that the coins match the account balance
func (suite *Suite) AccountBalanceEqual(acc authexported.Account, coins sdk.Coins) {
ak := suite.App.GetAccountKeeper()
acc = ak.GetAccount(suite.Ctx, acc.GetAddress())
suite.Equal(coins, acc.GetCoins(), fmt.Sprintf("expected account balance to equal coins %s, but got %s", coins, acc.GetCoins()))
}
// AccountBalanceDelta asserts that the coins are within delta of the account balance
func (suite *Suite) AccountBalanceDelta(acc authexported.Account, coins sdk.Coins, delta float64) {
ak := suite.App.GetAccountKeeper()
acc = ak.GetAccount(suite.Ctx, acc.GetAddress())
accCoins := acc.GetCoins()
allCoins := coins.Add(accCoins...)
for _, coin := range allCoins {
suite.InDelta(
coins.AmountOf(coin.Denom).Int64(),
accCoins.AmountOf(coin.Denom).Int64(),
delta,
fmt.Sprintf("expected module account balance to be in delta %f of coins %s, but got %s", delta, coins, accCoins),
)
}
}
// ModuleAccountBalanceEqual asserts that the swap module account balance matches the provided coins
func (suite *Suite) ModuleAccountBalanceEqual(coins sdk.Coins) {
macc, _ := suite.supplyKeeper.GetModuleAccountAndPermissions(suite.Ctx, swap.ModuleName)
suite.Require().NotNil(macc, "expected module account to be defined")
suite.Equal(coins, macc.GetCoins(), fmt.Sprintf("expected module account balance to equal coins %s, but got %s", coins, macc.GetCoins()))
}
// ModuleAccountBalanceDelta asserts that the swap module account balance is within acceptable delta of the provided coins
func (suite *Suite) ModuleAccountBalanceDelta(coins sdk.Coins, delta float64) {
macc, _ := suite.supplyKeeper.GetModuleAccountAndPermissions(suite.Ctx, swap.ModuleName)
suite.Require().NotNil(macc, "expected module account to be defined")
allCoins := coins.Add(macc.GetCoins()...)
for _, coin := range allCoins {
suite.InDelta(
coins.AmountOf(coin.Denom).Int64(),
macc.GetCoins().AmountOf(coin.Denom).Int64(),
delta,
fmt.Sprintf("expected module account balance to be in delta %f of coins %s, but got %s", delta, coins, macc.GetCoins()),
)
}
}
// PoolLiquidityEqual asserts that the pool matching the provided coins has those reserves
func (suite *Suite) PoolLiquidityEqual(coins sdk.Coins) {
poolRecord, ok := suite.Keeper.GetPool(suite.Ctx, swap.PoolIDFromCoins(coins))
suite.Require().True(ok, "expected pool to exist")
reserves := sdk.NewCoins(poolRecord.ReservesA, poolRecord.ReservesB)
suite.Equal(coins, reserves, fmt.Sprintf("expected pool reserves of %s, got %s", coins, reserves))
}
// PoolDeleted asserts that the pool does not exist
func (suite *Suite) PoolDeleted(denomA, denomB string) {
_, ok := suite.Keeper.GetPool(suite.Ctx, swap.PoolID(denomA, denomB))
suite.Require().False(ok, "expected pool to not exist")
}
// PoolLiquidityDelta asserts that the pool matching the provided coins has those reserves within delta
func (suite *Suite) PoolLiquidityDelta(coins sdk.Coins, delta float64) {
poolRecord, ok := suite.Keeper.GetPool(suite.Ctx, swap.PoolIDFromCoins(coins))
suite.Require().True(ok, "expected pool to exist")
suite.InDelta(
poolRecord.ReservesA.Amount.Int64(),
coins.AmountOf(poolRecord.ReservesA.Denom).Int64(),
delta,
fmt.Sprintf("expected pool reserves within delta %f of %s, got %s", delta, coins, poolRecord.Reserves()),
)
suite.InDelta(
poolRecord.ReservesB.Amount.Int64(),
coins.AmountOf(poolRecord.ReservesB.Denom).Int64(),
delta,
fmt.Sprintf("expected pool reserves within delta %f of %s, got %s", delta, coins, poolRecord.Reserves()),
)
}
// PoolShareTotalEqual asserts the total shares match the stored pool
func (suite *Suite) PoolShareTotalEqual(poolID string, totalShares sdk.Int) {
poolRecord, found := suite.Keeper.GetPool(suite.Ctx, poolID)
suite.Require().True(found, fmt.Sprintf("expected pool %s to exist", poolID))
suite.Equal(totalShares, poolRecord.TotalShares, "expected pool total shares to be equal")
}
// PoolDepositorSharesEqual asserts the depositor owns the shares for the provided pool
func (suite *Suite) PoolDepositorSharesEqual(depositor sdk.AccAddress, poolID string, shares sdk.Int) {
shareRecord, found := suite.Keeper.GetDepositorShares(suite.Ctx, depositor, poolID)
suite.Require().True(found, fmt.Sprintf("expected share record to exist for depositor %s and pool %s", depositor.String(), poolID))
suite.Equal(shares, shareRecord.SharesOwned)
}
// PoolReservesEqual assets the stored pool reserves are equal to the provided reserves
func (suite *Suite) PoolReservesEqual(poolID string, reserves sdk.Coins) {
poolRecord, found := suite.Keeper.GetPool(suite.Ctx, poolID)
suite.Require().True(found, fmt.Sprintf("expected pool %s to exist", poolID))
suite.Equal(reserves, poolRecord.Reserves(), "expected pool reserves to be equal")
}
// PoolShareValueEqual asserts that the depositor shares are in state and the value matches the expected coins
func (suite *Suite) PoolShareValueEqual(depositor authexported.Account, pool swap.AllowedPool, coins sdk.Coins) {
poolRecord, ok := suite.Keeper.GetPool(suite.Ctx, pool.Name())
suite.Require().True(ok, fmt.Sprintf("expected pool %s to exist", pool.Name()))
shares, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor.GetAddress(), poolRecord.PoolID)
suite.Require().True(ok, fmt.Sprintf("expected shares to exist for depositor %s", depositor.GetAddress()))
storedPool, err := swap.NewDenominatedPoolWithExistingShares(sdk.NewCoins(poolRecord.ReservesA, poolRecord.ReservesB), poolRecord.TotalShares)
suite.Nil(err)
value := storedPool.ShareValue(shares.SharesOwned)
suite.Equal(coins, value, fmt.Sprintf("expected shares to equal %s, but got %s", coins, value))
}
// PoolShareValueDelta asserts that the depositor shares are in state and the value is within delta of the expected coins
func (suite *Suite) PoolShareValueDelta(depositor authexported.Account, pool swap.AllowedPool, coins sdk.Coins, delta float64) {
poolRecord, ok := suite.Keeper.GetPool(suite.Ctx, pool.Name())
suite.Require().True(ok, fmt.Sprintf("expected pool %s to exist", pool.Name()))
shares, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor.GetAddress(), poolRecord.PoolID)
suite.Require().True(ok, fmt.Sprintf("expected shares to exist for depositor %s", depositor.GetAddress()))
storedPool, err := swap.NewDenominatedPoolWithExistingShares(sdk.NewCoins(poolRecord.ReservesA, poolRecord.ReservesB), poolRecord.TotalShares)
suite.Nil(err)
value := storedPool.ShareValue(shares.SharesOwned)
for _, coin := range coins {
suite.InDelta(
coin.Amount.Int64(),
value.AmountOf(coin.Denom).Int64(),
delta,
fmt.Sprintf("expected shares to be within delta %f of %s, but got %s", delta, coins, value),
)
}
}
// PoolSharesDeleted asserts that the pool shares have been removed
func (suite *Suite) PoolSharesDeleted(depositor sdk.AccAddress, denomA, denomB string) {
_, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor, swap.PoolID(denomA, denomB))
suite.Require().False(ok, "expected pool shares to not exist")
}
// EventsContains asserts that the expected event is in the provided events
func (suite *Suite) EventsContains(events sdk.Events, expectedEvent sdk.Event) {
foundMatch := false
for _, event := range events {
if event.Type == expectedEvent.Type {
if reflect.DeepEqual(attrsToMap(expectedEvent.Attributes), attrsToMap(event.Attributes)) {
foundMatch = true
}
}
}
suite.True(foundMatch, fmt.Sprintf("event of type %s not found or did not match", expectedEvent.Type))
}
func attrsToMap(attrs []kv.Pair) []sdk.Attribute {
out := []sdk.Attribute{}
for _, attr := range attrs {
out = append(out, sdk.NewAttribute(string(attr.Key), string(attr.Value)))
}
return out
}

437
x/swap/types/base_pool.go Normal file
View File

@ -0,0 +1,437 @@
package types
import (
"fmt"
"math/big"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var (
zero = sdk.ZeroInt()
)
// calculateInitialShares calculates initial shares as sqrt(A*B), the geometric mean of A and B
func calculateInitialShares(reservesA, reservesB sdk.Int) sdk.Int {
// Big.Int allows multiplication without overflow at 255 bits.
// In addition, Sqrt converges to a correct solution for inputs
// where sdk.Int.ApproxSqrt does not converge due to exceeding
// 100 iterations.
var result big.Int
result.Mul(reservesA.BigInt(), reservesB.BigInt()).Sqrt(&result)
return sdk.NewIntFromBigInt(&result)
}
// BasePool implements a unitless constant-product liquidity pool.
//
// The pool is symmetric. For all A,B,s, any operation F on a pool (A,B,s) and pool (B,A,s)
// will result in equal state values of A', B', s': F(A,B,s) => (A',B',s'), F(B,A,s) => (B',A',s')
//
// In addition, the pool is protected from overflow in intermediate calculations, and will
// only overflow when A, B, or s become larger than the max sdk.Int.
//
// Pool operations with non-positive values are invalid, and all functions on a pool will panic
// when given zero or negative values.
type BasePool struct {
reservesA sdk.Int
reservesB sdk.Int
totalShares sdk.Int
}
// NewBasePool returns a pointer to a base pool with reserves and total shares initialized
func NewBasePool(reservesA, reservesB sdk.Int) (*BasePool, error) {
if reservesA.LTE(zero) || reservesB.LTE(zero) {
return nil, sdkerrors.Wrap(ErrInvalidPool, "reserves must be greater than zero")
}
totalShares := calculateInitialShares(reservesA, reservesB)
return &BasePool{
reservesA: reservesA,
reservesB: reservesB,
totalShares: totalShares,
}, nil
}
// NewBasePoolWithExistingShares returns a pointer to a base pool with existing shares
func NewBasePoolWithExistingShares(reservesA, reservesB, totalShares sdk.Int) (*BasePool, error) {
if reservesA.LTE(zero) || reservesB.LTE(zero) {
return nil, sdkerrors.Wrap(ErrInvalidPool, "reserves must be greater than zero")
}
if totalShares.LTE(zero) {
return nil, sdkerrors.Wrap(ErrInvalidPool, "total shares must be greater than zero")
}
return &BasePool{
reservesA: reservesA,
reservesB: reservesB,
totalShares: totalShares,
}, nil
}
// ReservesA returns the A reserves of the pool
func (p *BasePool) ReservesA() sdk.Int {
return p.reservesA
}
// ReservesB returns the B reserves of the pool
func (p *BasePool) ReservesB() sdk.Int {
return p.reservesB
}
// IsEmpty returns true if all reserves are zero and
// returns false if reserveA or reserveB is not empty
func (p *BasePool) IsEmpty() bool {
return p.reservesA.IsZero() && p.reservesB.IsZero()
}
// TotalShares returns the total number of shares in the pool
func (p *BasePool) TotalShares() sdk.Int {
return p.totalShares
}
// AddLiquidity adds liquidity to the pool returns the actual reservesA, reservesB deposits in addition
// to the number of shares created. The deposits are always less than or equal to the provided and desired
// values.
func (p *BasePool) AddLiquidity(desiredA sdk.Int, desiredB sdk.Int) (sdk.Int, sdk.Int, sdk.Int) {
// Panics if provided values are zero
p.assertDepositsArePositive(desiredA, desiredB)
// Reinitialize the pool if reserves are empty and return the initialized state.
if p.IsEmpty() {
p.reservesA = desiredA
p.reservesB = desiredB
p.totalShares = calculateInitialShares(desiredA, desiredB)
return p.ReservesA(), p.ReservesB(), p.TotalShares()
}
// Panics if reserveA or reserveB is zero.
p.assertReservesArePositive()
// In order to preserve the reserve ratio of the pool, we must deposit
// A and B in the same ratio of the existing reserves. In addition,
// we should not deposit more funds than requested.
//
// To meet these requirements, we first calculate the optimalB to deposit
// if we keep desiredA fixed. If this is less than or equal to the desiredB,
// then we use (desiredA, optimalB) as the deposit.
//
// If the optimalB is greater than the desiredB, we calculate the optimalA
// from the desiredB and use (optimalA, optimalB) as the deposit.
//
// These optimal values are calculated as:
//
// optimalB = reservesB * desiredA / reservesA
// optimalA = reservesA * desiredB / reservesB
//
// Which shows us:
//
// if optimalB < desiredB then optimalA > desiredA
// if optimalB = desiredB then optimalA = desiredA
// if optimalB > desiredB then optimalA < desiredA
//
// so we first check if optimalB <= desiredB, then deposit
// (desiredA, optimalB) else deposit (optimalA, desiredA).
//
// In order avoid precision loss, we rearrange the inequality
// of optimalB <= desiredB
// from:
// reservesB * desiredA / reservesA <= desiredB
// to:
// reservesB * desiredA <= desiredB * reservesA
//
// which also shares the same intermediate products
// as the calculations for optimalB and optimalA.
actualA := desiredA.BigInt()
actualB := desiredB.BigInt()
// productA = reservesB * desiredA
var productA big.Int
productA.Mul(p.reservesB.BigInt(), actualA)
// productB = reservesA * desiredB
var productB big.Int
productB.Mul(p.reservesA.BigInt(), actualB)
// optimalB <= desiredB
if productA.Cmp(&productB) <= 0 {
actualB.Quo(&productA, p.reservesA.BigInt())
} else { // optimalA < desiredA
actualA.Quo(&productB, p.reservesB.BigInt())
}
var sharesA big.Int
sharesA.Mul(actualA, p.totalShares.BigInt()).Quo(&sharesA, p.reservesA.BigInt())
var sharesB big.Int
sharesB.Mul(actualB, p.totalShares.BigInt()).Quo(&sharesB, p.reservesB.BigInt())
// a/A and b/B may not be equal due to discrete math and truncation errors,
// so use the smallest deposit ratio to calculate the number of shares
//
// If we do not use the min or max ratio, then the result becomes
// dependent on the order of reserves in the pool
//
// Min is used to always ensure the share ratio is never larger
// than the deposit ratio for either A or B, ensuring there are no
// cases where a withdraw will allow funds to be removed at a higher ratio
// than it was deposited.
var shares sdk.Int
if sharesA.Cmp(&sharesB) <= 0 {
shares = sdk.NewIntFromBigInt(&sharesA)
} else {
shares = sdk.NewIntFromBigInt(&sharesB)
}
depositA := sdk.NewIntFromBigInt(actualA)
depositB := sdk.NewIntFromBigInt(actualB)
// update internal pool state
p.reservesA = p.reservesA.Add(depositA)
p.reservesB = p.reservesB.Add(depositB)
p.totalShares = p.totalShares.Add(shares)
return depositA, depositB, shares
}
// RemoveLiquidity removes liquidity from the pool and panics if the
// shares provided are greater than the total shares of the pool
// or the shares are not positive.
// In addition, also panics if reserves go negative, which should not happen.
// If panic occurs, it is a bug.
func (p *BasePool) RemoveLiquidity(shares sdk.Int) (sdk.Int, sdk.Int) {
// calculate amount to withdraw from the pool based
// on the number of shares provided. s/S * reserves
withdrawA, withdrawB := p.ShareValue(shares)
// update internal pool state
p.reservesA = p.reservesA.Sub(withdrawA)
p.reservesB = p.reservesB.Sub(withdrawB)
p.totalShares = p.totalShares.Sub(shares)
// Panics if reserveA or reserveB are negative
// A zero value (100% withdraw) is OK and should not panic.
p.assertReservesAreNotNegative()
return withdrawA, withdrawB
}
// SwapExactAForB trades an exact value of a for b. Returns the positive amount b
// that is removed from the pool and the portion of a that is used for paying the fee.
func (p *BasePool) SwapExactAForB(a sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
b, feeValue := p.calculateOutputForExactInput(a, p.reservesA, p.reservesB, fee)
p.assertInvariantAndUpdateReserves(
p.reservesA.Add(a), feeValue, p.reservesB.Sub(b), sdk.ZeroInt(),
)
return b, feeValue
}
// SwapExactBForA trades an exact value of b for a. Returns the positive amount a
// that is removed from the pool and the portion of b that is used for paying the fee.
func (p *BasePool) SwapExactBForA(b sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
a, feeValue := p.calculateOutputForExactInput(b, p.reservesB, p.reservesA, fee)
p.assertInvariantAndUpdateReserves(
p.reservesA.Sub(a), sdk.ZeroInt(), p.reservesB.Add(b), feeValue,
)
return a, feeValue
}
// calculateOutputForExactInput calculates the output amount of a swap using a fixed input, returning this amount in
// addition to the amount of input that is used to pay the fee.
//
// The fee is ceiled, ensuring a minimum fee of 1 and ensuring fees of a trade can not be reduced
// by splitting a trade into multiple trades.
//
// The swap output is truncated to ensure the pool invariant is always greater than or equal to the previous invariant.
func (p *BasePool) calculateOutputForExactInput(in, inReserves, outReserves sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
p.assertSwapInputIsValid(in)
p.assertFeeIsValid(fee)
inAfterFee := in.ToDec().Mul(sdk.OneDec().Sub(fee)).TruncateInt()
var result big.Int
result.Mul(outReserves.BigInt(), inAfterFee.BigInt())
result.Quo(&result, inReserves.Add(inAfterFee).BigInt())
out := sdk.NewIntFromBigInt(&result)
feeValue := in.Sub(inAfterFee)
return out, feeValue
}
// SwapAForExactB trades a for an exact b. Returns the positive amount a
// that is added to the pool, and the portion of a that is used to pay the fee.
func (p *BasePool) SwapAForExactB(b sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
a, feeValue := p.calculateInputForExactOutput(b, p.reservesB, p.reservesA, fee)
p.assertInvariantAndUpdateReserves(
p.reservesA.Add(a), feeValue, p.reservesB.Sub(b), sdk.ZeroInt(),
)
return a, feeValue
}
// SwapBForExactA trades b for an exact a. Returns the positive amount b
// that is added to the pool, and the portion of b that is used to pay the fee.
func (p *BasePool) SwapBForExactA(a sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
b, feeValue := p.calculateInputForExactOutput(a, p.reservesA, p.reservesB, fee)
p.assertInvariantAndUpdateReserves(
p.reservesA.Sub(a), sdk.ZeroInt(), p.reservesB.Add(b), feeValue,
)
return b, feeValue
}
// calculateInputForExactOutput calculates the input amount of a swap using a fixed output, returning this amount in
// addition to the amount of input that is used to pay the fee.
//
// The fee is ceiled, ensuring a minimum fee of 1 and ensuring fees of a trade can not be reduced
// by splitting a trade into multiple trades.
//
// The swap input is ceiled to ensure the pool invariant is always greater than or equal to the previous invariant.
func (p *BasePool) calculateInputForExactOutput(out, outReserves, inReserves sdk.Int, fee sdk.Dec) (sdk.Int, sdk.Int) {
p.assertSwapOutputIsValid(out, outReserves)
p.assertFeeIsValid(fee)
var result big.Int
result.Mul(inReserves.BigInt(), out.BigInt())
newOutReserves := outReserves.Sub(out)
var remainder big.Int
result.QuoRem(&result, newOutReserves.BigInt(), &remainder)
inWithoutFee := sdk.NewIntFromBigInt(&result)
if remainder.Sign() != 0 {
inWithoutFee = inWithoutFee.Add(sdk.OneInt())
}
in := inWithoutFee.ToDec().Quo(sdk.OneDec().Sub(fee)).Ceil().TruncateInt()
feeValue := in.Sub(inWithoutFee)
return in, feeValue
}
// ShareValue returns the value of the provided shares and panics
// if the shares are greater than the total shares of the pool or
// if the shares are not positive.
func (p *BasePool) ShareValue(shares sdk.Int) (sdk.Int, sdk.Int) {
p.assertSharesArePositive(shares)
p.assertSharesAreLessThanTotal(shares)
var resultA big.Int
resultA.Mul(p.reservesA.BigInt(), shares.BigInt())
resultA.Quo(&resultA, p.totalShares.BigInt())
var resultB big.Int
resultB.Mul(p.reservesB.BigInt(), shares.BigInt())
resultB.Quo(&resultB, p.totalShares.BigInt())
return sdk.NewIntFromBigInt(&resultA), sdk.NewIntFromBigInt(&resultB)
}
// assertInvariantAndUpdateRerserves asserts the constant product invariant is not violated, subtracting
// any fees first, then updates the pool reserves. Panics if invariant is violated.
func (p *BasePool) assertInvariantAndUpdateReserves(newReservesA, feeA, newReservesB, feeB sdk.Int) {
var invariant big.Int
invariant.Mul(p.reservesA.BigInt(), p.reservesB.BigInt())
var newInvariant big.Int
newInvariant.Mul(newReservesA.Sub(feeA).BigInt(), newReservesB.Sub(feeB).BigInt())
p.assertInvariant(&invariant, &newInvariant)
p.reservesA = newReservesA
p.reservesB = newReservesB
}
// assertSwapInputIsValid checks if the provided swap input is positive
// and panics if it is 0 or negative
func (p *BasePool) assertSwapInputIsValid(input sdk.Int) {
if !input.IsPositive() {
panic("invalid value: swap input must be positive")
}
}
// assertSwapOutputIsValid checks if the provided swap input is positive and
// less than the provided reserves.
func (p *BasePool) assertSwapOutputIsValid(output sdk.Int, reserves sdk.Int) {
if !output.IsPositive() {
panic("invalid value: swap output must be positive")
}
if output.GTE(reserves) {
panic("invalid value: swap output must be less than reserves")
}
}
// assertFeeIsValid checks if the provided fee is less
func (p *BasePool) assertFeeIsValid(fee sdk.Dec) {
if fee.IsNegative() || fee.GTE(sdk.OneDec()) {
panic("invalid value: fee must be between 0 and 1")
}
}
// assertSharesPositive panics if shares is zero or negative
func (p *BasePool) assertSharesArePositive(shares sdk.Int) {
if !shares.IsPositive() {
panic("invalid value: shares must be positive")
}
}
// assertSharesLessThanTotal panics if the number of shares is greater than the total shares
func (p *BasePool) assertSharesAreLessThanTotal(shares sdk.Int) {
if shares.GT(p.totalShares) {
panic(fmt.Sprintf("out of bounds: shares %s > total shares %s", shares, p.totalShares))
}
}
// assertDepositsPositive panics if a deposit is zero or negative
func (p *BasePool) assertDepositsArePositive(depositA, depositB sdk.Int) {
if !depositA.IsPositive() {
panic("invalid value: deposit A must be positive")
}
if !depositB.IsPositive() {
panic("invalid state: deposit B must be positive")
}
}
// assertReservesArePositive panics if any reserves are zero. This is an invalid
// state that should never happen. If this panic is seen, it is a bug.
func (p *BasePool) assertReservesArePositive() {
if !p.reservesA.IsPositive() {
panic("invalid state: reserves A must be positive")
}
if !p.reservesB.IsPositive() {
panic("invalid state: reserves B must be positive")
}
}
// assertReservesAreNotNegative panics if any reserves are negative. This is an invalid
// state that should never happen. If this panic is seen, it is a bug.
func (p *BasePool) assertReservesAreNotNegative() {
if p.reservesA.IsNegative() {
panic("invalid state: reserves A can not be negative")
}
if p.reservesB.IsNegative() {
panic("invalid state: reserves B can not be negative")
}
}
// assertInvariant panics if the new invariant is less than the previous invariant. This
// is an invalid state that should never happen. If this panic is seen, it is a bug.
func (p *BasePool) assertInvariant(prevInvariant, newInvariant *big.Int) {
// invariant > newInvariant
if prevInvariant.Cmp(newInvariant) == 1 {
panic(fmt.Sprintf("invalid state: invariant %s decreased to %s", prevInvariant.String(), newInvariant.String()))
}
}

View File

@ -0,0 +1,589 @@
package types_test
import (
"fmt"
"math/big"
"testing"
types "github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// i creates a new sdk.Int from int64
func i(n int64) sdk.Int {
return sdk.NewInt(n)
}
// s returns a new sdk.Int from a string
func s(str string) sdk.Int {
num, ok := sdk.NewIntFromString(str)
if !ok {
panic(fmt.Sprintf("overflow creating Int from %s", str))
}
return num
}
// d creates a new sdk.Dec from a string
func d(str string) sdk.Dec {
return sdk.MustNewDecFromStr(str)
}
// exp takes a sdk.Int and computes the power
// helper to generate large numbers
func exp(n sdk.Int, power int64) sdk.Int {
b := n.BigInt()
b.Exp(b, big.NewInt(power), nil)
return sdk.NewIntFromBigInt(b)
}
func TestBasePool_NewPool_Validation(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
expectedErr string
}{
{i(0), i(1e6), "invalid pool: reserves must be greater than zero"},
{i(0), i(0), "invalid pool: reserves must be greater than zero"},
{i(-1), i(1e6), "invalid pool: reserves must be greater than zero"},
{i(1e6), i(-1), "invalid pool: reserves must be greater than zero"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.EqualError(t, err, tc.expectedErr)
assert.Nil(t, pool)
})
}
}
func TestBasePool_NewPoolWithExistingShares_Validation(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
totalShares sdk.Int
expectedErr string
}{
{i(0), i(1e6), i(1), "invalid pool: reserves must be greater than zero"},
{i(0), i(0), i(1), "invalid pool: reserves must be greater than zero"},
{i(-1), i(1e6), i(3), "invalid pool: reserves must be greater than zero"},
{i(1e6), i(-1), i(100), "invalid pool: reserves must be greater than zero"},
{i(1e6), i(-1), i(3), "invalid pool: reserves must be greater than zero"},
{i(1e6), i(1e6), i(0), "invalid pool: total shares must be greater than zero"},
{i(1e6), i(1e6), i(-1), "invalid pool: total shares must be greater than zero"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s shares=%s", tc.reservesA, tc.reservesB, tc.totalShares), func(t *testing.T) {
pool, err := types.NewBasePoolWithExistingShares(tc.reservesA, tc.reservesB, tc.totalShares)
require.EqualError(t, err, tc.expectedErr)
assert.Nil(t, pool)
})
}
}
func TestBasePool_InitialState(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
expectedShares sdk.Int
}{
{i(1), i(1), i(1)},
{i(100), i(100), i(100)},
{i(100), i(10000000), i(31622)},
{i(1e5), i(5e6), i(707106)},
{i(1e6), i(5e6), i(2236067)},
{i(1e15), i(7e15), i(2645751311064590)},
{i(1), i(6e18), i(2449489742)},
{i(1.345678e18), i(4.313456e18), i(2409257736973775913)},
// handle sqrt of large numbers, sdk.Int.ApproxSqrt() doesn't converge in 100 iterations
{i(145345664).Mul(exp(i(10), 26)), i(6432294561).Mul(exp(i(10), 20)), s("96690543695447979624812468142651")},
{i(465432423).Mul(exp(i(10), 50)), i(4565432).Mul(exp(i(10), 50)), s("4609663846531258725944608083913166083991595286362304230475")},
{exp(i(2), 253), exp(i(2), 253), s("14474011154664524427946373126085988481658748083205070504932198000989141204992")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.Nil(t, err)
assert.Equal(t, tc.reservesA, pool.ReservesA())
assert.Equal(t, tc.reservesB, pool.ReservesB())
assert.Equal(t, tc.expectedShares, pool.TotalShares())
})
}
}
func TestBasePool_ExistingState(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
totalShares sdk.Int
}{
{i(1), i(1), i(1)},
{i(100), i(100), i(100)},
{i(1e5), i(5e6), i(707106)},
{i(1e15), i(7e15), i(2645751311064590)},
{i(465432423).Mul(exp(i(10), 50)), i(4565432).Mul(exp(i(10), 50)), s("4609663846531258725944608083913166083991595286362304230475")},
{exp(i(2), 253), exp(i(2), 253), s("14474011154664524427946373126085988481658748083205070504932198000989141204992")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s shares=%s", tc.reservesA, tc.reservesB, tc.totalShares), func(t *testing.T) {
pool, err := types.NewBasePoolWithExistingShares(tc.reservesA, tc.reservesB, tc.totalShares)
require.Nil(t, err)
assert.Equal(t, tc.reservesA, pool.ReservesA())
assert.Equal(t, tc.reservesB, pool.ReservesB())
assert.Equal(t, tc.totalShares, pool.TotalShares())
})
}
}
func TestBasePool_ShareValue_PoolCreator(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
}{
{i(1), i(1)},
{i(100), i(100)},
{i(100), i(10000000)},
{i(1e5), i(5e6)},
{i(1e15), i(7e15)},
{i(1), i(6e18)},
{i(1.345678e18), i(4.313456e18)},
// ensure no overflows in intermediate values
{exp(i(2), 253), exp(i(2), 253)},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
assert.NoError(t, err)
a, b := pool.ShareValue(pool.TotalShares())
// pool creators experience zero truncation error and always
// and always receive their original balance on a 100% withdraw
// when there are no other deposits that result in a fractional share ownership
assert.Equal(t, tc.reservesA, a, "share value of reserves A not equal")
assert.Equal(t, tc.reservesB, b, "share value of reserves B not equal")
})
}
}
func TestBasePool_AddLiquidity(t *testing.T) {
testCases := []struct {
initialA sdk.Int
initialB sdk.Int
desiredA sdk.Int
desiredB sdk.Int
expectedA sdk.Int
expectedB sdk.Int
expectedShares sdk.Int
}{
{i(1), i(1), i(1), i(1), i(1), i(1), i(1)}, // small pool, i(100)% deposit
{i(10), i(10), i(5), i(5), i(5), i(5), i(5)}, // i(50)% deposit
{i(10), i(10), i(3), i(3), i(3), i(3), i(3)}, // i(30)% deposit
{i(10), i(10), i(1), i(1), i(1), i(1), i(1)}, // i(10)% deposit
// small pools, unequal deposit ratios
{i(11), i(10), i(5), i(6), i(5), i(4), i(4)},
{i(11), i(10), i(5), i(5), i(5), i(4), i(4)},
// this test case fails if we don't use min share ratio
{i(11), i(10), i(5), i(4), i(4), i(4), i(3)},
// small pools, unequal deposit ratios, reversed
{i(10), i(11), i(6), i(5), i(4), i(5), i(4)},
{i(10), i(11), i(5), i(5), i(4), i(5), i(4)},
// this test case fails if we don't use min share ratio
{i(10), i(11), i(4), i(5), i(4), i(4), i(3)},
{i(10e6), i(11e6), i(5e6), i(5e6), i(4545454), i(5e6), i(4767312)},
{i(11e6), i(10e6), i(5e6), i(5e6), i(5e6), i(4545454), i(4767312)},
// pool size near max of sdk.Int, ensure intermidiate calculations do not overflow
{exp(i(10), 70), exp(i(10), 70), i(1e18), i(1e18), i(1e18), i(1e18), i(1e18)},
}
for _, tc := range testCases {
name := fmt.Sprintf("initialA=%s initialB=%s desiredA=%s desiredB=%s", tc.initialA, tc.initialB, tc.desiredA, tc.desiredB)
t.Run(name, func(t *testing.T) {
pool, err := types.NewBasePool(tc.initialA, tc.initialB)
require.NoError(t, err)
initialShares := pool.TotalShares()
actualA, actualB, actualShares := pool.AddLiquidity(tc.desiredA, tc.desiredB)
// assert correct values are retruned
assert.Equal(t, tc.expectedA, actualA, "deposited A liquidity not equal")
assert.Equal(t, tc.expectedB, actualB, "deposited B liquidity not equal")
assert.Equal(t, tc.expectedShares, actualShares, "calculated shares not equal")
// assert pool liquidity and shares are updated
assert.Equal(t, tc.initialA.Add(actualA), pool.ReservesA(), "total reserves A not equal")
assert.Equal(t, tc.initialB.Add(actualB), pool.ReservesB(), "total reserves B not equal")
assert.Equal(t, initialShares.Add(actualShares), pool.TotalShares(), "total shares not equal")
leftA := actualShares.BigInt()
leftA.Mul(leftA, tc.initialA.BigInt())
rightA := initialShares.BigInt()
rightA.Mul(rightA, actualA.BigInt())
leftB := actualShares.BigInt()
leftB.Mul(leftB, tc.initialB.BigInt())
rightB := initialShares.BigInt()
rightB.Mul(rightB, actualB.BigInt())
// assert that the share ratio is less than or equal to the deposit ratio
// actualShares / initialShares <= actualA / initialA
assert.True(t, leftA.Cmp(rightA) <= 0, "share ratio is greater than deposit A ratio")
// actualShares / initialShares <= actualB / initialB
assert.True(t, leftB.Cmp(rightB) <= 0, "share ratio is greater than deposit B ratio")
// assert that share value of returned shares is not greater than the deposited amount
shareValueA, shareValueB := pool.ShareValue(actualShares)
assert.True(t, shareValueA.LTE(actualA), "share value A greater than deposited A")
assert.True(t, shareValueB.LTE(actualB), "share value B greater than deposited B")
})
}
}
func TestBasePool_RemoveLiquidity(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
shares sdk.Int
expectedA sdk.Int
expectedB sdk.Int
}{
{i(1), i(1), i(1), i(1), i(1)},
{i(100), i(100), i(50), i(50), i(50)},
{i(100), i(10000000), i(10435), i(32), i(3299917)},
{i(10000000), i(100), i(10435), i(3299917), i(32)},
{i(1.345678e18), i(4.313456e18), i(3.134541e17), i(175078108044025869), i(561197935621412888)},
// ensure no overflows in intermediate values
{exp(i(10), 70), exp(i(10), 70), i(1e18), i(1e18), i(1e18)},
{exp(i(2), 253), exp(i(2), 253), exp(i(2), 253), exp(i(2), 253), exp(i(2), 253)},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s shares=%s", tc.reservesA, tc.reservesB, tc.shares), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
assert.NoError(t, err)
initialShares := pool.TotalShares()
a, b := pool.RemoveLiquidity(tc.shares)
// pool creators experience zero truncation error and always
// and always receive their original balance on a 100% withdraw
// when there are no other deposits that result in a fractional share ownership
assert.Equal(t, tc.expectedA, a, "withdrawn A not equal")
assert.Equal(t, tc.expectedB, b, "withdrawn B not equal")
// asset that pool state is updated
assert.Equal(t, tc.reservesA.Sub(a), pool.ReservesA(), "reserves A after withdraw not equal")
assert.Equal(t, tc.reservesB.Sub(b), pool.ReservesB(), "reserves B after withdraw not equal")
assert.Equal(t, initialShares.Sub(tc.shares), pool.TotalShares(), "total shares after withdraw not equal")
})
}
}
func TestBasePool_Panic_OutOfBounds(t *testing.T) {
pool, err := types.NewBasePool(sdk.NewInt(100), sdk.NewInt(100))
require.NoError(t, err)
assert.Panics(t, func() { pool.ShareValue(pool.TotalShares().Add(sdk.NewInt(1))) }, "ShareValue did not panic when shares > totalShares")
assert.Panics(t, func() { pool.RemoveLiquidity(pool.TotalShares().Add(sdk.NewInt(1))) }, "RemoveLiquidity did not panic when shares > totalShares")
}
func TestBasePool_EmptyAndRefill(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
}{
{i(1), i(1)},
{i(100), i(100)},
{i(100), i(10000000)},
{i(1e5), i(5e6)},
{i(1e6), i(5e6)},
{i(1e15), i(7e15)},
{i(1), i(6e18)},
{i(1.345678e18), i(4.313456e18)},
{i(145345664).Mul(exp(i(10), 26)), i(6432294561).Mul(exp(i(10), 20))},
{i(465432423).Mul(exp(i(10), 50)), i(4565432).Mul(exp(i(10), 50))},
{exp(i(2), 253), exp(i(2), 253)},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.NoError(t, err)
initialShares := pool.TotalShares()
pool.RemoveLiquidity(initialShares)
assert.True(t, pool.IsEmpty())
assert.True(t, pool.TotalShares().IsZero(), "total shares are not depleted")
pool.AddLiquidity(tc.reservesA, tc.reservesB)
assert.Equal(t, initialShares, pool.TotalShares(), "total shares not equal")
})
}
}
func TestBasePool_Panics_AddLiquidity(t *testing.T) {
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.AddLiquidity(i(0), i(1e6))
}, "did not panic when reserve A is zero")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.AddLiquidity(i(-1), i(1e6))
}, "did not panic when reserve A is negative")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.AddLiquidity(i(1e6), i(0))
}, "did not panic when reserve B is zero")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.AddLiquidity(i(1e6), i(0))
}, "did not panic when reserve B is zero")
}
func TestBasePool_Panics_RemoveLiquidity(t *testing.T) {
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.RemoveLiquidity(i(0))
}, "did not panic when shares are zero")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.RemoveLiquidity(i(-1))
}, "did not panic when shares are negative")
}
func TestBasePool_ReservesOnlyDepletedWithLastShare(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
}{
{i(5), i(5)},
{i(100), i(100)},
{i(100), i(10000000)},
{i(1e5), i(5e6)}, {i(1e6), i(5e6)},
{i(1e15), i(7e15)},
{i(1), i(6e18)},
{i(1.345678e18), i(4.313456e18)},
{i(145345664).Mul(exp(i(10), 26)), i(6432294561).Mul(exp(i(10), 20))},
{i(465432423).Mul(exp(i(10), 50)), i(4565432).Mul(exp(i(10), 50))},
{exp(i(2), 253), exp(i(2), 253)},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.NoError(t, err)
initialShares := pool.TotalShares()
pool.RemoveLiquidity(initialShares.Sub(i(1)))
assert.False(t, pool.ReservesA().IsZero(), "reserves A equal to zero")
assert.False(t, pool.ReservesB().IsZero(), "reserves B equal to zero")
pool.RemoveLiquidity(i(1))
assert.True(t, pool.IsEmpty())
})
}
}
func TestBasePool_Swap_ExactInput(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
exactInput sdk.Int
fee sdk.Dec
expectedOutput sdk.Int
expectedFee sdk.Int
}{
// test small pools
{i(10), i(10), i(1), d("0.003"), i(0), i(1)},
{i(10), i(10), i(3), d("0.003"), i(1), i(1)},
{i(10), i(10), i(10), d("0.003"), i(4), i(1)},
{i(10), i(10), i(91), d("0.003"), i(9), i(1)},
// test fee values and ceil
{i(1e6), i(1e6), i(1000), d("0.003"), i(996), i(3)},
{i(1e6), i(1e6), i(1000), d("0.0031"), i(995), i(4)},
{i(1e6), i(1e6), i(1000), d("0.0039"), i(995), i(4)},
{i(1e6), i(1e6), i(1000), d("0.001"), i(998), i(1)},
{i(1e6), i(1e6), i(1000), d("0.025"), i(974), i(25)},
{i(1e6), i(1e6), i(1000), d("0.1"), i(899), i(100)},
{i(1e6), i(1e6), i(1000), d("0.5"), i(499), i(500)},
// test various random pools and swaps
{i(10e6), i(500e6), i(1e6), d("0.0025"), i(45351216), i(2500)},
{i(10e6), i(500e6), i(8e6), d("0.003456"), i(221794899), i(27648)},
// test very large pools and swaps
{exp(i(2), 250), exp(i(2), 250), exp(i(2), 249), d("0.003"), s("601876423139828614225164081027182620796370196819963934493551943901658899790"), s("2713877091499598330239944961141122840311015265600950719674787125185463976")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s exactInput=%s fee=%s", tc.reservesA, tc.reservesB, tc.exactInput, tc.fee), func(t *testing.T) {
poolA, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.NoError(t, err)
swapA, feeA := poolA.SwapExactAForB(tc.exactInput, tc.fee)
poolB, err := types.NewBasePool(tc.reservesB, tc.reservesA)
require.NoError(t, err)
swapB, feeB := poolB.SwapExactBForA(tc.exactInput, tc.fee)
// pool must be symmetric - if we swap reserves, then swap opposite direction
// then the results should be equal
require.Equal(t, swapA, swapB, "expected swap methods to have equal swap results")
require.Equal(t, feeA, feeB, "expected swap methods to have equal fee results")
require.Equal(t, poolA.ReservesA(), poolB.ReservesB(), "expected reserves A to be equal")
require.Equal(t, poolA.ReservesB(), poolB.ReservesA(), "expected reserves B to be equal")
assert.Equal(t, tc.expectedOutput, swapA, "returned swap not equal")
assert.Equal(t, tc.expectedFee, feeA, "returned fee not equal")
expectedReservesA := tc.reservesA.Add(tc.exactInput)
expectedReservesB := tc.reservesB.Sub(tc.expectedOutput)
assert.Equal(t, expectedReservesA, poolA.ReservesA(), "expected new reserves A not equal")
assert.Equal(t, expectedReservesB, poolA.ReservesB(), "expected new reserves B not equal")
})
}
}
func TestBasePool_Swap_ExactOutput(t *testing.T) {
testCases := []struct {
reservesA sdk.Int
reservesB sdk.Int
exactOutput sdk.Int
fee sdk.Dec
expectedInput sdk.Int
expectedFee sdk.Int
}{
// test small pools
{i(10), i(10), i(1), d("0.003"), i(3), i(1)},
{i(10), i(10), i(9), d("0.003"), i(91), i(1)},
// test fee values and ceil
{i(1e6), i(1e6), i(996), d("0.003"), i(1000), i(3)},
{i(1e6), i(1e6), i(995), d("0.0031"), i(1000), i(4)},
{i(1e6), i(1e6), i(995), d("0.0039"), i(1000), i(4)},
{i(1e6), i(1e6), i(998), d("0.001"), i(1000), i(1)},
{i(1e6), i(1e6), i(974), d("0.025"), i(1000), i(25)},
{i(1e6), i(1e6), i(899), d("0.1"), i(1000), i(100)},
{i(1e6), i(1e6), i(499), d("0.5"), i(1000), i(500)},
// test various random pools and swaps
{i(10e6), i(500e6), i(45351216), d("0.0025"), i(1e6), i(2500)},
{i(10e6), i(500e6), i(221794899), d("0.003456"), i(8e6), i(27648)},
// test very large pools and swaps
{exp(i(2), 250), exp(i(2), 250), s("601876423139828614225164081027182620796370196819963934493551943901658899790"), d("0.003"), s("904625697166532776746648320380374280103671755200316906558262375061821325311"), s("2713877091499598330239944961141122840311015265600950719674787125185463976")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s exactOutput=%s fee=%s", tc.reservesA, tc.reservesB, tc.exactOutput, tc.fee), func(t *testing.T) {
poolA, err := types.NewBasePool(tc.reservesA, tc.reservesB)
require.NoError(t, err)
swapA, feeA := poolA.SwapAForExactB(tc.exactOutput, tc.fee)
poolB, err := types.NewBasePool(tc.reservesB, tc.reservesA)
require.NoError(t, err)
swapB, feeB := poolB.SwapBForExactA(tc.exactOutput, tc.fee)
// pool must be symmetric - if we swap reserves, then swap opposite direction
// then the results should be equal
require.Equal(t, swapA, swapB, "expected swap methods to have equal swap results")
require.Equal(t, feeA, feeB, "expected swap methods to have equal fee results")
require.Equal(t, poolA.ReservesA(), poolB.ReservesB(), "expected reserves A to be equal")
require.Equal(t, poolA.ReservesB(), poolB.ReservesA(), "expected reserves B to be equal")
assert.Equal(t, tc.expectedInput.String(), swapA.String(), "returned swap not equal")
assert.Equal(t, tc.expectedFee, feeA, "returned fee not equal")
expectedReservesA := tc.reservesA.Add(tc.expectedInput)
expectedReservesB := tc.reservesB.Sub(tc.exactOutput)
assert.Equal(t, expectedReservesA, poolA.ReservesA(), "expected new reserves A not equal")
assert.Equal(t, expectedReservesB, poolA.ReservesB(), "expected new reserves B not equal")
})
}
}
func TestBasePool_Panics_Swap_ExactInput(t *testing.T) {
testCases := []struct {
swap sdk.Int
fee sdk.Dec
}{
{i(0), d("0.003")},
{i(-1), d("0.003")},
{i(1), d("1")},
{i(1), d("-0.003")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("swap=%s fee=%s", tc.swap, tc.fee), func(t *testing.T) {
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.SwapExactAForB(tc.swap, tc.fee)
}, "SwapExactAForB did not panic")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.SwapExactBForA(tc.swap, tc.fee)
}, "SwapExactBForA did not panic")
})
}
}
func TestBasePool_Panics_Swap_ExactOutput(t *testing.T) {
testCases := []struct {
swap sdk.Int
fee sdk.Dec
}{
{i(0), d("0.003")},
{i(-1), d("0.003")},
{i(1), d("1")},
{i(1), d("-0.003")},
{i(1000000), d("0.003")},
{i(1000001), d("0.003")},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("swap=%s fee=%s", tc.swap, tc.fee), func(t *testing.T) {
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.SwapAForExactB(tc.swap, tc.fee)
}, "SwapAForExactB did not panic")
assert.Panics(t, func() {
pool, err := types.NewBasePool(i(1e6), i(1e6))
require.NoError(t, err)
pool.SwapBForExactA(tc.swap, tc.fee)
}, "SwapBForExactA did not panic")
})
}
}

View File

@ -14,5 +14,6 @@ func init() {
// RegisterCodec registers the necessary types for swap module // RegisterCodec registers the necessary types for swap module
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(AllowedPool{}, "swap/AllowedPool", nil) cdc.RegisterConcrete(MsgDeposit{}, "swap/MsgDeposit", nil)
cdc.RegisterConcrete(MsgWithdraw{}, "swap/MsgWithdraw", nil)
} }

View File

@ -0,0 +1,13 @@
package types_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
)
func init() {
kavaConfig := sdk.GetConfig()
app.SetBech32AddressPrefixes(kavaConfig)
app.SetBip44CoinType(kavaConfig)
kavaConfig.Seal()
}

View File

@ -0,0 +1,159 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DenominatedPool implements a denominated constant-product liquidity pool
type DenominatedPool struct {
// all pool operations are implemented in a unitless base pool
pool *BasePool
// track units of the reserveA and reserveB in base pool
denomA string
denomB string
}
// NewDenominatedPool creates a new denominated pool from reserve coins
func NewDenominatedPool(reserves sdk.Coins) (*DenominatedPool, error) {
if len(reserves) != 2 {
return nil, sdkerrors.Wrap(ErrInvalidPool, "reserves must have two denominations")
}
// Coins should always sorted, so this is deterministic, though it does not need to be.
// The base pool calculation results do not depend on reserve order.
reservesA := reserves[0]
reservesB := reserves[1]
pool, err := NewBasePool(reservesA.Amount, reservesB.Amount)
if err != nil {
return nil, err
}
return &DenominatedPool{
pool: pool,
denomA: reservesA.Denom,
denomB: reservesB.Denom,
}, nil
}
// NewDenominatedPoolWithExistingShares creates a new denominated pool from reserve coins
func NewDenominatedPoolWithExistingShares(reserves sdk.Coins, totalShares sdk.Int) (*DenominatedPool, error) {
if len(reserves) != 2 {
return nil, sdkerrors.Wrap(ErrInvalidPool, "reserves must have two denominations")
}
// Coins should always sorted, so this is deterministic, though it does not need to be.
// The base pool calculation results do not depend on reserve order.
reservesA := reserves[0]
reservesB := reserves[1]
pool, err := NewBasePoolWithExistingShares(reservesA.Amount, reservesB.Amount, totalShares)
if err != nil {
return nil, err
}
return &DenominatedPool{
pool: pool,
denomA: reservesA.Denom,
denomB: reservesB.Denom,
}, nil
}
// Reserves returns the reserves held in the pool
func (p *DenominatedPool) Reserves() sdk.Coins {
return p.coins(p.pool.ReservesA(), p.pool.ReservesB())
}
// TotalShares returns the total shares for the pool
func (p *DenominatedPool) TotalShares() sdk.Int {
return p.pool.TotalShares()
}
// IsEmpty returns true if the pool is empty
func (p *DenominatedPool) IsEmpty() bool {
return p.pool.IsEmpty()
}
// AddLiquidity adds liquidity to the reserves and returns the added amount and shares created
func (p *DenominatedPool) AddLiquidity(deposit sdk.Coins) (sdk.Coins, sdk.Int) {
desiredA := deposit.AmountOf(p.denomA)
desiredB := deposit.AmountOf(p.denomB)
actualA, actualB, shares := p.pool.AddLiquidity(desiredA, desiredB)
return p.coins(actualA, actualB), shares
}
// RemoveLiquidity removes liquidity from the pool
func (p *DenominatedPool) RemoveLiquidity(shares sdk.Int) sdk.Coins {
withdrawnA, withdrawnB := p.pool.RemoveLiquidity(shares)
return p.coins(withdrawnA, withdrawnB)
}
// ShareValue returns the value of the provided shares
func (p *DenominatedPool) ShareValue(shares sdk.Int) sdk.Coins {
valueA, valueB := p.pool.ShareValue(shares)
return p.coins(valueA, valueB)
}
// SwapWithExactInput trades an exact input coin for the other. Returns the positive other coin amount
// that is removed from the pool and the portion of the input coin that is used for the fee.
// It panics if the input denom does not match the pool reserves.
func (p *DenominatedPool) SwapWithExactInput(swapInput sdk.Coin, fee sdk.Dec) (sdk.Coin, sdk.Coin) {
var (
swapOutput sdk.Int
feePaid sdk.Int
)
switch swapInput.Denom {
case p.denomA:
swapOutput, feePaid = p.pool.SwapExactAForB(swapInput.Amount, fee)
return p.coinB(swapOutput), p.coinA(feePaid)
case p.denomB:
swapOutput, feePaid = p.pool.SwapExactBForA(swapInput.Amount, fee)
return p.coinA(swapOutput), p.coinB(feePaid)
default:
panic(fmt.Sprintf("invalid denomination: denom '%s' does not match pool reserves", swapInput.Denom))
}
}
// SwapWithExactOutput trades a coin for an exact output coin b. Returns the positive input coin
// that is added to the pool, and the portion of that input that is used to pay the fee.
// Panics if the output denom does not match the pool reserves.
func (p *DenominatedPool) SwapWithExactOutput(swapOutput sdk.Coin, fee sdk.Dec) (sdk.Coin, sdk.Coin) {
var (
swapInput sdk.Int
feePaid sdk.Int
)
switch swapOutput.Denom {
case p.denomA:
swapInput, feePaid = p.pool.SwapBForExactA(swapOutput.Amount, fee)
return p.coinB(swapInput), p.coinB(feePaid)
case p.denomB:
swapInput, feePaid = p.pool.SwapAForExactB(swapOutput.Amount, fee)
return p.coinA(swapInput), p.coinA(feePaid)
default:
panic(fmt.Sprintf("invalid denomination: denom '%s' does not match pool reserves", swapOutput.Denom))
}
}
// coins returns a new coins slice with correct reserve denoms from ordered sdk.Ints
func (p *DenominatedPool) coins(amountA, amountB sdk.Int) sdk.Coins {
return sdk.NewCoins(p.coinA(amountA), p.coinB(amountB))
}
// coinA returns a new coin denominated in denomA
func (p *DenominatedPool) coinA(amount sdk.Int) sdk.Coin {
return sdk.NewCoin(p.denomA, amount)
}
// coinA returns a new coin denominated in denomB
func (p *DenominatedPool) coinB(amount sdk.Int) sdk.Coin {
return sdk.NewCoin(p.denomB, amount)
}

View File

@ -0,0 +1,182 @@
package types_test
import (
"fmt"
"testing"
types "github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// create a new ukava coin from int64
func ukava(amount int64) sdk.Coin {
return sdk.NewCoin("ukava", sdk.NewInt(amount))
}
// create a new usdx coin from int64
func usdx(amount int64) sdk.Coin {
return sdk.NewCoin("usdx", sdk.NewInt(amount))
}
// create a new hard coin from int64
func hard(amount int64) sdk.Coin {
return sdk.NewCoin("hard", sdk.NewInt(amount))
}
func TestDenominatedPool_NewDenominatedPool_Validation(t *testing.T) {
testCases := []struct {
reservesA sdk.Coin
reservesB sdk.Coin
expectedErr string
}{
{ukava(0), usdx(1e6), "invalid pool: reserves must have two denominations"},
{ukava(1e6), usdx(0), "invalid pool: reserves must have two denominations"},
{usdx(0), ukava(1e6), "invalid pool: reserves must have two denominations"},
{usdx(0), ukava(1e6), "invalid pool: reserves must have two denominations"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewDenominatedPool(sdk.NewCoins(tc.reservesA, tc.reservesB))
require.EqualError(t, err, tc.expectedErr)
assert.Nil(t, pool)
})
}
}
func TestDenominatedPool_NewDenominatedPoolWithExistingShares_Validation(t *testing.T) {
testCases := []struct {
reservesA sdk.Coin
reservesB sdk.Coin
totalShares sdk.Int
expectedErr string
}{
{ukava(0), usdx(1e6), i(1), "invalid pool: reserves must have two denominations"},
{usdx(0), ukava(1e6), i(1), "invalid pool: reserves must have two denominations"},
{ukava(1e6), usdx(1e6), i(0), "invalid pool: total shares must be greater than zero"},
{usdx(1e6), ukava(1e6), i(-1), "invalid pool: total shares must be greater than zero"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("reservesA=%s reservesB=%s", tc.reservesA, tc.reservesB), func(t *testing.T) {
pool, err := types.NewDenominatedPoolWithExistingShares(sdk.NewCoins(tc.reservesA, tc.reservesB), tc.totalShares)
require.EqualError(t, err, tc.expectedErr)
assert.Nil(t, pool)
})
}
}
func TestDenominatedPool_InitialState(t *testing.T) {
reserves := sdk.NewCoins(ukava(1e6), usdx(5e6))
totalShares := i(2236067)
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
assert.Equal(t, pool.Reserves(), reserves)
assert.Equal(t, pool.TotalShares(), totalShares)
}
func TestDenominatedPool_InitialState_ExistingShares(t *testing.T) {
reserves := sdk.NewCoins(ukava(1e6), usdx(5e6))
totalShares := i(2e6)
pool, err := types.NewDenominatedPoolWithExistingShares(reserves, totalShares)
require.NoError(t, err)
assert.Equal(t, pool.Reserves(), reserves)
assert.Equal(t, pool.TotalShares(), totalShares)
}
func TestDenominatedPool_ShareValue(t *testing.T) {
reserves := sdk.NewCoins(ukava(10e6), usdx(50e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
assert.Equal(t, reserves, pool.ShareValue(pool.TotalShares()))
halfReserves := sdk.NewCoins(ukava(4999999), usdx(24999998))
assert.Equal(t, halfReserves, pool.ShareValue(pool.TotalShares().Quo(i(2))))
}
func TestDenominatedPool_AddLiquidity(t *testing.T) {
reserves := sdk.NewCoins(ukava(10e6), usdx(50e6))
desired := sdk.NewCoins(ukava(1e6), usdx(1e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
initialShares := pool.TotalShares()
deposit, shares := pool.AddLiquidity(desired)
require.True(t, shares.IsPositive())
require.True(t, deposit.IsAllPositive())
assert.Equal(t, reserves.Add(deposit...), pool.Reserves())
assert.Equal(t, initialShares.Add(shares), pool.TotalShares())
}
func TestDenominatedPool_RemoveLiquidity(t *testing.T) {
reserves := sdk.NewCoins(ukava(10e6), usdx(50e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
withdraw := pool.RemoveLiquidity(pool.TotalShares())
assert.True(t, pool.Reserves().IsZero())
assert.True(t, pool.TotalShares().IsZero())
assert.True(t, pool.IsEmpty())
assert.Equal(t, reserves, withdraw)
}
func TestDenominatedPool_SwapWithExactInput(t *testing.T) {
reserves := sdk.NewCoins(ukava(10e6), usdx(50e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
output, fee := pool.SwapWithExactInput(ukava(1e6), d("0.003"))
assert.Equal(t, usdx(4533054), output)
assert.Equal(t, ukava(3000), fee)
assert.Equal(t, sdk.NewCoins(ukava(11e6), usdx(45466946)), pool.Reserves())
pool, err = types.NewDenominatedPool(reserves)
require.NoError(t, err)
output, fee = pool.SwapWithExactInput(usdx(5e6), d("0.003"))
assert.Equal(t, ukava(906610), output)
assert.Equal(t, usdx(15000), fee)
assert.Equal(t, sdk.NewCoins(ukava(9093390), usdx(55e6)), pool.Reserves())
assert.Panics(t, func() { pool.SwapWithExactInput(hard(1e6), d("0.003")) }, "SwapWithExactInput did not panic on invalid denomination")
}
func TestDenominatedPool_SwapWithExactOuput(t *testing.T) {
reserves := sdk.NewCoins(ukava(10e6), usdx(50e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
input, fee := pool.SwapWithExactOutput(ukava(1e6), d("0.003"))
assert.Equal(t, usdx(5572273), input)
assert.Equal(t, usdx(16717), fee)
assert.Equal(t, sdk.NewCoins(ukava(9e6), usdx(55572273)), pool.Reserves())
pool, err = types.NewDenominatedPool(reserves)
require.NoError(t, err)
input, fee = pool.SwapWithExactOutput(usdx(5e6), d("0.003"))
assert.Equal(t, ukava(1114456), input)
assert.Equal(t, ukava(3344), fee)
assert.Equal(t, sdk.NewCoins(ukava(11114456), usdx(45e6)), pool.Reserves())
assert.Panics(t, func() { pool.SwapWithExactOutput(hard(1e6), d("0.003")) }, "SwapWithExactOutput did not panic on invalid denomination")
}

View File

@ -4,8 +4,17 @@ import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
) )
// DONTCOVER // swap module errors
var ( var (
ErrCustom = sdkerrors.Register(ModuleName, 2, "") ErrNotAllowed = sdkerrors.Register(ModuleName, 2, "not allowed")
ErrInvalidDeadline = sdkerrors.Register(ModuleName, 3, "invalid deadline")
ErrDeadlineExceeded = sdkerrors.Register(ModuleName, 4, "deadline exceeded")
ErrSlippageExceeded = sdkerrors.Register(ModuleName, 5, "slippage exceeded")
ErrInvalidPool = sdkerrors.Register(ModuleName, 6, "invalid pool")
ErrInvalidSlippage = sdkerrors.Register(ModuleName, 7, "invalid slippage")
ErrInsufficientLiquidity = sdkerrors.Register(ModuleName, 8, "insufficient liquidity")
ErrInvalidShares = sdkerrors.Register(ModuleName, 9, "invalid shares")
ErrDepositNotFound = sdkerrors.Register(ModuleName, 10, "deposit not found")
ErrInvalidCoin = sdkerrors.Register(ModuleName, 11, "invalid coin")
ErrNotImplemented = sdkerrors.Register(ModuleName, 12, "not implemented")
) )

View File

@ -2,5 +2,11 @@ package types
// Event types for swap module // Event types for swap module
const ( const (
// EventTypeCustom = "" AttributeValueCategory = ModuleName
EventTypeSwapDeposit = "swap_deposit"
EventTypeSwapWithdraw = "swap_withdraw"
AttributeKeyPoolID = "pool_id"
AttributeKeyDepositor = "depositor"
AttributeKeyShares = "shares"
AttributeKeyOwner = "owner"
) )

View File

@ -0,0 +1,22 @@
package types // noalias
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// SupplyKeeper defines the expected supply keeper (noalias)
type SupplyKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, name string) exported.ModuleAccountI
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
}
// AccountKeeper defines the expected keeper interface for interacting with account (noalias)
type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
SetAccount(ctx sdk.Context, acc authexported.Account)
}

View File

@ -1,9 +1,16 @@
package types package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const ( const (
// ModuleName name that will be used throughout the module // ModuleName name that will be used throughout the module
ModuleName = "swap" ModuleName = "swap"
// ModuleAccountName name of module account used to hold liquidity
ModuleAccountName = "swap"
// StoreKey Top level store key where all module items will be stored // StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName StoreKey = ModuleName
@ -17,6 +24,27 @@ const (
DefaultParamspace = ModuleName DefaultParamspace = ModuleName
) )
// key prefixes for store
var ( var (
// KeyPrefix = []byte{0x01} PoolKeyPrefix = []byte{0x01}
DepositorPoolSharesPrefix = []byte{0x02}
sep = []byte(":")
) )
// PoolKey returns a key generated from a poolID
func PoolKey(poolID string) []byte {
return []byte(poolID)
}
// DepositorPoolSharesKey returns a key from a depositor and poolID
func DepositorPoolSharesKey(depositor sdk.AccAddress, poolID string) []byte {
return createKey(depositor, sep, []byte(poolID))
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

18
x/swap/types/keys_test.go Normal file
View File

@ -0,0 +1,18 @@
package types_test
import (
"testing"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
)
func TestKeys(t *testing.T) {
key := types.PoolKey("ukava/usdx")
assert.Equal(t, "ukava/usdx", string(key))
key = types.DepositorPoolSharesKey(sdk.AccAddress("testaddress1"), "ukava/usdx")
assert.Equal(t, string(sdk.AccAddress("testaddress1"))+":ukava/usdx", string(key))
}

View File

@ -1 +1,181 @@
package types package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var (
_ sdk.Msg = &MsgDeposit{}
_ MsgWithDeadline = &MsgDeposit{}
_ sdk.Msg = &MsgWithdraw{}
_ MsgWithDeadline = &MsgWithdraw{}
)
// MsgWithDeadline allows messages to define a deadline of when they are considered invalid
type MsgWithDeadline interface {
GetDeadline() time.Time
DeadlineExceeded(blockTime time.Time) bool
}
// MsgDeposit deposits liquidity into a pool
type MsgDeposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
TokenA sdk.Coin `json:"token_a" yaml:"token_a"`
TokenB sdk.Coin `json:"token_b" yaml:"token_b"`
Slippage sdk.Dec `json:"slippage" yaml:"slippage"`
Deadline int64 `json:"deadline" yaml:"deadline"`
}
// NewMsgDeposit returns a new MsgDeposit
func NewMsgDeposit(depositor sdk.AccAddress, tokenA sdk.Coin, tokenB sdk.Coin, slippage sdk.Dec, deadline int64) MsgDeposit {
return MsgDeposit{
Depositor: depositor,
TokenA: tokenA,
TokenB: tokenB,
Slippage: slippage,
Deadline: deadline,
}
}
// Route return the message type used for routing the message.
func (msg MsgDeposit) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgDeposit) Type() string { return "swap_deposit" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgDeposit) ValidateBasic() error {
if msg.Depositor.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "depositor address cannot be empty")
}
if !msg.TokenA.IsValid() || msg.TokenA.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "token a deposit amount %s", msg.TokenA)
}
if !msg.TokenB.IsValid() || msg.TokenB.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "token b deposit amount %s", msg.TokenB)
}
if msg.TokenA.Denom == msg.TokenB.Denom {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "denominations can not be equal")
}
if msg.Slippage.IsNil() {
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage must be set")
}
if msg.Slippage.IsNegative() {
return sdkerrors.Wrapf(ErrInvalidSlippage, "slippage can not be negative")
}
if msg.Deadline <= 0 {
return sdkerrors.Wrapf(ErrInvalidDeadline, "deadline %d", msg.Deadline)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgDeposit) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgDeposit) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Depositor}
}
// GetDeadline returns the time at which the msg is considered invalid
func (msg MsgDeposit) GetDeadline() time.Time {
return time.Unix(msg.Deadline, 0)
}
// DeadlineExceeded returns if the msg has exceeded it's deadline
func (msg MsgDeposit) DeadlineExceeded(blockTime time.Time) bool {
return blockTime.Unix() >= msg.Deadline
}
// MsgWithdraw deposits liquidity into a pool
type MsgWithdraw struct {
From sdk.AccAddress `json:"from" yaml:"from"`
Shares sdk.Int `json:"shares" yaml:"shares"`
MinTokenA sdk.Coin `json:"min_token_a" yaml:"min_token_a"`
MinTokenB sdk.Coin `json:"min_token_b" yaml:"min_token_b"`
Deadline int64 `json:"deadline" yaml:"deadline"`
}
// NewMsgWithdraw returns a new MsgWithdraw
func NewMsgWithdraw(from sdk.AccAddress, shares sdk.Int, minTokenA, minTokenB sdk.Coin, deadline int64) MsgWithdraw {
return MsgWithdraw{
From: from,
Shares: shares,
MinTokenA: minTokenA,
MinTokenB: minTokenB,
Deadline: deadline,
}
}
// Route return the message type used for routing the message.
func (msg MsgWithdraw) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgWithdraw) Type() string { return "swap_withdraw" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgWithdraw) ValidateBasic() error {
if msg.From.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from address cannot be empty")
}
if msg.Shares.IsNil() {
return sdkerrors.Wrapf(ErrInvalidShares, "shares must be set")
}
if msg.Shares.IsZero() || msg.Shares.IsNegative() {
return sdkerrors.Wrapf(ErrInvalidShares, msg.Shares.String())
}
if !msg.MinTokenA.IsValid() || msg.MinTokenA.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "min token a amount %s", msg.MinTokenA)
}
if !msg.MinTokenB.IsValid() || msg.MinTokenB.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "min token b amount %s", msg.MinTokenB)
}
if msg.MinTokenA.Denom == msg.MinTokenB.Denom {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "denominations can not be equal")
}
if msg.Deadline <= 0 {
return sdkerrors.Wrapf(ErrInvalidDeadline, "deadline %d", msg.Deadline)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgWithdraw) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgWithdraw) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.From}
}
// GetDeadline returns the time at which the msg is considered invalid
func (msg MsgWithdraw) GetDeadline() time.Time {
return time.Unix(msg.Deadline, 0)
}
// DeadlineExceeded returns if the msg has exceeded it's deadline
func (msg MsgWithdraw) DeadlineExceeded(blockTime time.Time) bool {
return blockTime.Unix() >= msg.Deadline
}

408
x/swap/types/msg_test.go Normal file
View File

@ -0,0 +1,408 @@
package types_test
import (
"testing"
"time"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMsgDeposit_Attributes(t *testing.T) {
msg := types.MsgDeposit{}
assert.Equal(t, "swap", msg.Route())
assert.Equal(t, "swap_deposit", msg.Type())
}
func TestMsgDeposit_Signing(t *testing.T) {
signData := `{"type":"swap/MsgDeposit","value":{"deadline":"1623606299","depositor":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","slippage":"0.010000000000000000","token_a":{"amount":"1000000","denom":"ukava"},"token_b":{"amount":"5000000","denom":"usdx"}}}`
signBytes := []byte(signData)
addr, err := sdk.AccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
require.NoError(t, err)
msg := types.NewMsgDeposit(addr, sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), sdk.MustNewDecFromStr("0.01"), 1623606299)
assert.Equal(t, []sdk.AccAddress{addr}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsgDeposit_Validation(t *testing.T) {
validMsg := types.NewMsgDeposit(
sdk.AccAddress("test1"),
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
sdk.MustNewDecFromStr("0.01"),
1623606299,
)
require.NoError(t, validMsg.ValidateBasic())
testCases := []struct {
name string
depositor sdk.AccAddress
tokenA sdk.Coin
tokenB sdk.Coin
slippage sdk.Dec
deadline int64
expectedErr string
}{
{
name: "empty address",
depositor: sdk.AccAddress(""),
tokenA: validMsg.TokenA,
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid address: depositor address cannot be empty",
},
{
name: "negative token a",
depositor: validMsg.Depositor,
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token a deposit amount -1ukava",
},
{
name: "zero token a",
depositor: validMsg.Depositor,
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token a deposit amount 0ukava",
},
{
name: "invalid denom token a",
depositor: validMsg.Depositor,
tokenA: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token a deposit amount 1000000UKAVA",
},
{
name: "negative token b",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token b deposit amount -1ukava",
},
{
name: "zero token b",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token b deposit amount 0ukava",
},
{
name: "invalid denom token b",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: token b deposit amount 1000000UKAVA",
},
{
name: "denoms can not be the same",
depositor: validMsg.Depositor,
tokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
tokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
slippage: validMsg.Slippage,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: denominations can not be equal",
},
{
name: "zero deadline",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: 0,
expectedErr: "invalid deadline: deadline 0",
},
{
name: "negative deadline",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: validMsg.TokenB,
slippage: validMsg.Slippage,
deadline: -1,
expectedErr: "invalid deadline: deadline -1",
},
{
name: "negative slippage",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: validMsg.TokenB,
slippage: sdk.MustNewDecFromStr("-0.01"),
deadline: validMsg.Deadline,
expectedErr: "invalid slippage: slippage can not be negative",
},
{
name: "nil slippage",
depositor: validMsg.Depositor,
tokenA: validMsg.TokenA,
tokenB: validMsg.TokenB,
slippage: sdk.Dec{},
deadline: validMsg.Deadline,
expectedErr: "invalid slippage: slippage must be set",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
msg := types.NewMsgDeposit(tc.depositor, tc.tokenA, tc.tokenB, tc.slippage, tc.deadline)
err := msg.ValidateBasic()
assert.EqualError(t, err, tc.expectedErr)
})
}
}
func TestMsgDeposit_Deadline(t *testing.T) {
blockTime := time.Now()
testCases := []struct {
name string
deadline int64
isExceeded bool
}{
{
name: "deadline in future",
deadline: blockTime.Add(1 * time.Second).Unix(),
isExceeded: false,
},
{
name: "deadline in past",
deadline: blockTime.Add(-1 * time.Second).Unix(),
isExceeded: true,
},
{
name: "deadline is equal",
deadline: blockTime.Unix(),
isExceeded: true,
},
}
for _, tc := range testCases {
msg := types.NewMsgDeposit(
sdk.AccAddress("test1"),
sdk.NewCoin("ukava", sdk.NewInt(1e6)),
sdk.NewCoin("usdx", sdk.NewInt(5e6)),
sdk.MustNewDecFromStr("0.01"),
tc.deadline,
)
require.NoError(t, msg.ValidateBasic())
assert.Equal(t, tc.isExceeded, msg.DeadlineExceeded(blockTime))
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
}
}
func TestMsgWithdraw_Attributes(t *testing.T) {
msg := types.MsgWithdraw{}
assert.Equal(t, "swap", msg.Route())
assert.Equal(t, "swap_withdraw", msg.Type())
}
func TestMsgWithdraw_Signing(t *testing.T) {
signData := `{"type":"swap/MsgWithdraw","value":{"deadline":"1623606299","from":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","min_token_a":{"amount":"1000000","denom":"ukava"},"min_token_b":{"amount":"2000000","denom":"usdx"},"shares":"1500000"}}`
signBytes := []byte(signData)
addr, err := sdk.AccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
require.NoError(t, err)
msg := types.NewMsgWithdraw(
addr,
sdk.NewInt(1500000),
sdk.NewCoin("ukava", sdk.NewInt(1000000)),
sdk.NewCoin("usdx", sdk.NewInt(2000000)),
1623606299,
)
assert.Equal(t, []sdk.AccAddress{addr}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsgWithdraw_Validation(t *testing.T) {
validMsg := types.NewMsgWithdraw(
sdk.AccAddress("test1"),
sdk.NewInt(1500000),
sdk.NewCoin("ukava", sdk.NewInt(1000000)),
sdk.NewCoin("usdx", sdk.NewInt(2000000)),
1623606299,
)
require.NoError(t, validMsg.ValidateBasic())
testCases := []struct {
name string
from sdk.AccAddress
shares sdk.Int
minTokenA sdk.Coin
minTokenB sdk.Coin
deadline int64
expectedErr string
}{
{
name: "empty address",
from: sdk.AccAddress(""),
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid address: from address cannot be empty",
},
{
name: "zero token a",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: min token a amount 0ukava",
},
{
name: "invalid denom token a",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid coins: min token a amount 1000000UKAVA",
},
{
name: "negative token b",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
deadline: validMsg.Deadline,
expectedErr: "invalid coins: min token b amount -1ukava",
},
{
name: "zero token b",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(0)},
deadline: validMsg.Deadline,
expectedErr: "invalid coins: min token b amount 0ukava",
},
{
name: "invalid denom token b",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: sdk.Coin{Denom: "UKAVA", Amount: sdk.NewInt(1e6)},
deadline: validMsg.Deadline,
expectedErr: "invalid coins: min token b amount 1000000UKAVA",
},
{
name: "denoms can not be the same",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
minTokenB: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(1e6)},
deadline: validMsg.Deadline,
expectedErr: "invalid coins: denominations can not be equal",
},
{
name: "zero shares",
from: validMsg.From,
shares: sdk.ZeroInt(),
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid shares: 0",
},
{
name: "negative shares",
from: validMsg.From,
shares: sdk.NewInt(-1),
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid shares: -1",
},
{
name: "nil shares",
from: validMsg.From,
shares: sdk.Int{},
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: validMsg.Deadline,
expectedErr: "invalid shares: shares must be set",
},
{
name: "zero deadline",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: 0,
expectedErr: "invalid deadline: deadline 0",
},
{
name: "negative deadline",
from: validMsg.From,
shares: validMsg.Shares,
minTokenA: validMsg.MinTokenA,
minTokenB: validMsg.MinTokenB,
deadline: -1,
expectedErr: "invalid deadline: deadline -1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
msg := types.NewMsgWithdraw(tc.from, tc.shares, tc.minTokenA, tc.minTokenB, tc.deadline)
err := msg.ValidateBasic()
assert.EqualError(t, err, tc.expectedErr)
})
}
}
func TestMsgWithdraw_Deadline(t *testing.T) {
blockTime := time.Now()
testCases := []struct {
name string
deadline int64
isExceeded bool
}{
{
name: "deadline in future",
deadline: blockTime.Add(1 * time.Second).Unix(),
isExceeded: false,
},
{
name: "deadline in past",
deadline: blockTime.Add(-1 * time.Second).Unix(),
isExceeded: true,
},
{
name: "deadline is equal",
deadline: blockTime.Unix(),
isExceeded: true,
},
}
for _, tc := range testCases {
msg := types.NewMsgWithdraw(
sdk.AccAddress("test1"),
sdk.NewInt(1500000),
sdk.NewCoin("ukava", sdk.NewInt(1000000)),
sdk.NewCoin("usdx", sdk.NewInt(2000000)),
tc.deadline,
)
require.NoError(t, msg.ValidateBasic())
assert.Equal(t, tc.isExceeded, msg.DeadlineExceeded(blockTime))
assert.Equal(t, time.Unix(tc.deadline, 0), msg.GetDeadline())
}
}

View File

@ -51,7 +51,7 @@ func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{}) return params.NewKeyTable().RegisterParamSet(&Params{})
} }
// ParamSetAllowedPools implements the ParamSet interface and returns all the key/value pairs // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs { func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{ return params.ParamSetPairs{
params.NewParamSetPair(KeyAllowedPools, &p.AllowedPools, validateAllowedPoolsParams), params.NewParamSetPair(KeyAllowedPools, &p.AllowedPools, validateAllowedPoolsParams),
@ -83,9 +83,92 @@ func validateSwapFee(i interface{}) error {
return fmt.Errorf("invalid parameter type: %T", i) return fmt.Errorf("invalid parameter type: %T", i)
} }
if swapFee.IsNil() || swapFee.IsNegative() || swapFee.GT(MaxSwapFee) { if swapFee.IsNil() || swapFee.IsNegative() || swapFee.GTE(MaxSwapFee) {
return fmt.Errorf(fmt.Sprintf("invalid swap fee: %s", swapFee)) return fmt.Errorf(fmt.Sprintf("invalid swap fee: %s", swapFee))
} }
return nil return nil
} }
// AllowedPool defines a tradable pool
type AllowedPool struct {
TokenA string `json:"token_a" yaml:"token_a"`
TokenB string `json:"token_b" yaml:"token_b"`
}
// NewAllowedPool returns a new AllowedPool object
func NewAllowedPool(tokenA, tokenB string) AllowedPool {
return AllowedPool{
TokenA: tokenA,
TokenB: tokenB,
}
}
// Validate validates allowedPool attributes and returns an error if invalid
func (p AllowedPool) Validate() error {
err := sdk.ValidateDenom(p.TokenA)
if err != nil {
return err
}
err = sdk.ValidateDenom(p.TokenB)
if err != nil {
return err
}
if p.TokenA == p.TokenB {
return fmt.Errorf(
"pool cannot have two tokens of the same type, received '%s' and '%s'",
p.TokenA, p.TokenB,
)
}
if p.TokenA > p.TokenB {
return fmt.Errorf(
"invalid token order: '%s' must come before '%s'",
p.TokenB, p.TokenA,
)
}
return nil
}
// Name returns the name for the allowed pool
func (p AllowedPool) Name() string {
return PoolID(p.TokenA, p.TokenB)
}
// String pretty prints the allowedPool
func (p AllowedPool) String() string {
return fmt.Sprintf(`AllowedPool:
Name: %s
Token A: %s
Token B: %s
`, p.Name(), p.TokenA, p.TokenB)
}
// AllowedPools is a slice of AllowedPool
type AllowedPools []AllowedPool
// NewAllowedPools returns AllowedPools from the provided values
func NewAllowedPools(allowedPools ...AllowedPool) AllowedPools {
return AllowedPools(allowedPools)
}
// Validate validates each allowedPool and returns an error if there are any duplicates
func (p AllowedPools) Validate() error {
seenAllowedPools := make(map[string]bool)
for _, allowedPool := range p {
err := allowedPool.Validate()
if err != nil {
return err
}
if seen := seenAllowedPools[allowedPool.Name()]; seen {
return fmt.Errorf("duplicate pool: %s", allowedPool.Name())
}
seenAllowedPools[allowedPool.Name()] = true
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/kava-labs/kava/x/swap/types" "github.com/kava-labs/kava/x/swap/types"
@ -86,7 +87,7 @@ func TestParams_ParamSetPairs_AllowedPools(t *testing.T) {
var paramSetPair *paramstypes.ParamSetPair var paramSetPair *paramstypes.ParamSetPair
for _, pair := range defaultParams.ParamSetPairs() { for _, pair := range defaultParams.ParamSetPairs() {
if bytes.Compare(pair.Key, types.KeyAllowedPools) == 0 { if bytes.Equal(pair.Key, types.KeyAllowedPools) {
paramSetPair = &pair paramSetPair = &pair
break break
} }
@ -107,7 +108,7 @@ func TestParams_ParamSetPairs_SwapFee(t *testing.T) {
var paramSetPair *paramstypes.ParamSetPair var paramSetPair *paramstypes.ParamSetPair
for _, pair := range defaultParams.ParamSetPairs() { for _, pair := range defaultParams.ParamSetPairs() {
if bytes.Compare(pair.Key, types.KeySwapFee) == 0 { if bytes.Equal(pair.Key, types.KeySwapFee) {
paramSetPair = &pair paramSetPair = &pair
break break
} }
@ -183,7 +184,7 @@ func TestParams_Validation(t *testing.T) {
testFn: func(params *types.Params) { testFn: func(params *types.Params) {
params.SwapFee = sdk.OneDec() params.SwapFee = sdk.OneDec()
}, },
expectedErr: "", expectedErr: "invalid swap fee: 1.000000000000000000",
}, },
} }
@ -202,7 +203,7 @@ func TestParams_Validation(t *testing.T) {
var paramSetPair *paramstypes.ParamSetPair var paramSetPair *paramstypes.ParamSetPair
for _, pair := range params.ParamSetPairs() { for _, pair := range params.ParamSetPairs() {
if bytes.Compare(pair.Key, tc.key) == 0 { if bytes.Equal(pair.Key, tc.key) {
paramSetPair = &pair paramSetPair = &pair
break break
} }
@ -231,3 +232,177 @@ func TestParams_String(t *testing.T) {
assert.Contains(t, output, "ukava/usdx") assert.Contains(t, output, "ukava/usdx")
assert.Contains(t, output, "0.5") assert.Contains(t, output, "0.5")
} }
func TestAllowedPool_Validation(t *testing.T) {
testCases := []struct {
name string
allowedPool types.AllowedPool
expectedErr string
}{
{
name: "blank token a",
allowedPool: types.NewAllowedPool("", "ukava"),
expectedErr: "invalid denom: ",
},
{
name: "blank token b",
allowedPool: types.NewAllowedPool("ukava", ""),
expectedErr: "invalid denom: ",
},
{
name: "invalid token a",
allowedPool: types.NewAllowedPool("1ukava", "ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "invalid token b",
allowedPool: types.NewAllowedPool("ukava", "1ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "no uppercase letters token a",
allowedPool: types.NewAllowedPool("uKava", "ukava"),
expectedErr: "invalid denom: uKava",
},
{
name: "no uppercase letters token b",
allowedPool: types.NewAllowedPool("ukava", "UKAVA"),
expectedErr: "invalid denom: UKAVA",
},
{
name: "matching tokens",
allowedPool: types.NewAllowedPool("ukava", "ukava"),
expectedErr: "pool cannot have two tokens of the same type, received 'ukava' and 'ukava'",
},
{
name: "invalid token order",
allowedPool: types.NewAllowedPool("usdx", "ukava"),
expectedErr: "invalid token order: 'ukava' must come before 'usdx'",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPool.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}
// ensure no regression in case insentive token matching if
// sdk.ValidateDenom ever allows upper case letters
func TestAllowedPool_TokenMatch(t *testing.T) {
allowedPool := types.NewAllowedPool("UKAVA", "ukava")
err := allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("hard", "haRd")
err = allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("Usdx", "uSdX")
err = allowedPool.Validate()
assert.Error(t, err)
}
func TestAllowedPool_String(t *testing.T) {
allowedPool := types.NewAllowedPool("hard", "ukava")
require.NoError(t, allowedPool.Validate())
output := `AllowedPool:
Name: hard/ukava
Token A: hard
Token B: ukava
`
assert.Equal(t, output, allowedPool.String())
}
func TestAllowedPool_Name(t *testing.T) {
testCases := []struct {
tokens string
name string
}{
{
tokens: "atoken btoken",
name: "atoken/btoken",
},
{
tokens: "aaa aaaa",
name: "aaa/aaaa",
},
{
tokens: "aaaa aaab",
name: "aaaa/aaab",
},
{
tokens: "a001 a002",
name: "a001/a002",
},
{
tokens: "hard ukava",
name: "hard/ukava",
},
{
tokens: "bnb hard",
name: "bnb/hard",
},
{
tokens: "bnb xrpb",
name: "bnb/xrpb",
},
}
for _, tc := range testCases {
t.Run(tc.tokens, func(t *testing.T) {
tokens := strings.Split(tc.tokens, " ")
require.Equal(t, 2, len(tokens))
allowedPool := types.NewAllowedPool(tokens[0], tokens[1])
require.NoError(t, allowedPool.Validate())
assert.Equal(t, tc.name, allowedPool.Name())
})
}
}
func TestAllowedPools_Validate(t *testing.T) {
testCases := []struct {
name string
allowedPools types.AllowedPools
expectedErr string
}{
{
name: "invalid pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("HARD", "UKAVA"),
),
expectedErr: "invalid denom: HARD",
},
{
name: "duplicate pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("hard", "ukava"),
),
expectedErr: "duplicate pool: hard/ukava",
},
{
name: "duplicate pools",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("bnb", "usdx"),
types.NewAllowedPool("btcb", "xrpb"),
types.NewAllowedPool("bnb", "usdx"),
),
expectedErr: "duplicate pool: bnb/usdx",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPools.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}

View File

@ -1,90 +0,0 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// AllowedPool defines a tradable pool
type AllowedPool struct {
TokenA string `json:"token_a" yaml:"token_a"`
TokenB string `json:"token_b" yaml:"token_b"`
}
// NewAllowedPool returns a new AllowedPool object
func NewAllowedPool(tokenA, tokenB string) AllowedPool {
return AllowedPool{
TokenA: tokenA,
TokenB: tokenB,
}
}
// Validate validates allowedPool attributes and returns an error if invalid
func (p AllowedPool) Validate() error {
err := sdk.ValidateDenom(p.TokenA)
if err != nil {
return err
}
err = sdk.ValidateDenom(p.TokenB)
if err != nil {
return err
}
if p.TokenA == p.TokenB {
return fmt.Errorf(
"pool cannot have two tokens of the same type, received '%s' and '%s'",
p.TokenA, p.TokenB,
)
}
if p.TokenA > p.TokenB {
return fmt.Errorf(
"invalid token order: '%s' must come before '%s'",
p.TokenB, p.TokenA,
)
}
return nil
}
// Name returns a unique name for a allowedPool in alphabetical order
func (p AllowedPool) Name() string {
return fmt.Sprintf("%s/%s", p.TokenA, p.TokenB)
}
// String pretty prints the allowedPool
func (p AllowedPool) String() string {
return fmt.Sprintf(`AllowedPool:
Name: %s
Token A: %s
Token B: %s
`, p.Name(), p.TokenA, p.TokenB)
}
// AllowedPools is a slice of AllowedPool
type AllowedPools []AllowedPool
// NewAllowedPools returns AllowedPools from the provided values
func NewAllowedPools(allowedPools ...AllowedPool) AllowedPools {
return AllowedPools(allowedPools)
}
// Validate validates each allowedPool and returns an error if there are any duplicates
func (p AllowedPools) Validate() error {
seenAllowedPools := make(map[string]bool)
for _, allowedPool := range p {
err := allowedPool.Validate()
if err != nil {
return err
}
if seen := seenAllowedPools[allowedPool.Name()]; seen {
return fmt.Errorf("duplicate pool: %s", allowedPool.Name())
}
seenAllowedPools[allowedPool.Name()] = true
}
return nil
}

View File

@ -1,185 +0,0 @@
package types_test
import (
"strings"
"testing"
types "github.com/kava-labs/kava/x/swap/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAllowedPool_Validation(t *testing.T) {
testCases := []struct {
name string
allowedPool types.AllowedPool
expectedErr string
}{
{
name: "blank token a",
allowedPool: types.NewAllowedPool("", "ukava"),
expectedErr: "invalid denom: ",
},
{
name: "blank token b",
allowedPool: types.NewAllowedPool("ukava", ""),
expectedErr: "invalid denom: ",
},
{
name: "invalid token a",
allowedPool: types.NewAllowedPool("1ukava", "ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "invalid token b",
allowedPool: types.NewAllowedPool("ukava", "1ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "no uppercase letters token a",
allowedPool: types.NewAllowedPool("uKava", "ukava"),
expectedErr: "invalid denom: uKava",
},
{
name: "no uppercase letters token b",
allowedPool: types.NewAllowedPool("ukava", "UKAVA"),
expectedErr: "invalid denom: UKAVA",
},
{
name: "matching tokens",
allowedPool: types.NewAllowedPool("ukava", "ukava"),
expectedErr: "pool cannot have two tokens of the same type, received 'ukava' and 'ukava'",
},
{
name: "invalid token order",
allowedPool: types.NewAllowedPool("usdx", "ukava"),
expectedErr: "invalid token order: 'ukava' must come before 'usdx'",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPool.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}
// ensure no regression in case insentive token matching if
// sdk.ValidateDenom ever allows upper case letters
func TestAllowedPool_TokenMatch(t *testing.T) {
allowedPool := types.NewAllowedPool("UKAVA", "ukava")
err := allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("hard", "haRd")
err = allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("Usdx", "uSdX")
err = allowedPool.Validate()
assert.Error(t, err)
}
func TestAllowedPool_String(t *testing.T) {
allowedPool := types.NewAllowedPool("hard", "ukava")
require.NoError(t, allowedPool.Validate())
output := `AllowedPool:
Name: hard/ukava
Token A: hard
Token B: ukava
`
assert.Equal(t, output, allowedPool.String())
}
func TestAllowedPool_Name(t *testing.T) {
testCases := []struct {
tokens string
name string
}{
{
tokens: "atoken btoken",
name: "atoken/btoken",
},
{
tokens: "aaa aaaa",
name: "aaa/aaaa",
},
{
tokens: "aaaa aaab",
name: "aaaa/aaab",
},
{
tokens: "a001 a002",
name: "a001/a002",
},
{
tokens: "hard ukava",
name: "hard/ukava",
},
{
tokens: "bnb hard",
name: "bnb/hard",
},
{
tokens: "bnb xrpb",
name: "bnb/xrpb",
},
}
for _, tc := range testCases {
t.Run(tc.tokens, func(t *testing.T) {
tokens := strings.Split(tc.tokens, " ")
require.Equal(t, 2, len(tokens))
allowedPool := types.NewAllowedPool(tokens[0], tokens[1])
require.NoError(t, allowedPool.Validate())
assert.Equal(t, tc.name, allowedPool.Name())
})
}
}
func TestAllowedPools_Validate(t *testing.T) {
testCases := []struct {
name string
allowedPools types.AllowedPools
expectedErr string
}{
{
name: "invalid pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("HARD", "UKAVA"),
),
expectedErr: "invalid denom: HARD",
},
{
name: "duplicate pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("hard", "ukava"),
),
expectedErr: "duplicate pool: hard/ukava",
},
{
name: "duplicate pools",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("bnb", "usdx"),
types.NewAllowedPool("btcb", "xrpb"),
types.NewAllowedPool("bnb", "usdx"),
),
expectedErr: "duplicate pool: bnb/usdx",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPools.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}

View File

@ -1,6 +1,81 @@
package types package types
import sdk "github.com/cosmos/cosmos-sdk/types"
// Querier routes for the swap module // Querier routes for the swap module
const ( const (
QueryGetParams = "params" QueryGetParams = "params"
QueryGetDeposits = "deposits"
QueryGetPool = "pool"
QueryGetPools = "pools"
) )
// QueryDepositsParams is the params for a filtered deposits query
type QueryDepositsParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Pool string `json:"pool" yaml:"pool"`
}
// NewQueryDepositsParams creates a new QueryDepositsParams
func NewQueryDepositsParams(page, limit int, owner sdk.AccAddress, pool string) QueryDepositsParams {
return QueryDepositsParams{
Page: page,
Limit: limit,
Owner: owner,
Pool: pool,
}
}
// DepositsQueryResult contains the result of a deposits query
type DepositsQueryResult struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
PoolID string `json:"pool_id" yaml:"pool_id"`
SharesOwned sdk.Int `json:"shares_owned" yaml:"shares_owned"`
SharesValue sdk.Coins `json:"shares_value" yaml:"shares_value"`
}
// NewDepositsQueryResult creates a new DepositsQueryResult
func NewDepositsQueryResult(shareRecord ShareRecord, sharesValue sdk.Coins) DepositsQueryResult {
return DepositsQueryResult{
Depositor: shareRecord.Depositor,
PoolID: shareRecord.PoolID,
SharesOwned: shareRecord.SharesOwned,
SharesValue: sharesValue,
}
}
// DepositsQueryResults is a slice of DepositsQueryResult
type DepositsQueryResults []DepositsQueryResult
// QueryPoolParams is the params for a pool query
type QueryPoolParams struct {
Pool string `json:"pool" yaml:"pool"`
}
// NewQueryPoolParams creates a new QueryPoolParams
func NewQueryPoolParams(pool string) QueryPoolParams {
return QueryPoolParams{
Pool: pool,
}
}
// PoolStatsQueryResult contains the result of a pool query
type PoolStatsQueryResult struct {
Name string `json:"name" yaml:"name"`
Coins sdk.Coins `json:"coins" yaml:"coins"`
TotalShares sdk.Int `json:"total_shares" yaml:"total_shares"`
}
// NewPoolStatsQueryResult creates a new PoolStatsQueryResult
func NewPoolStatsQueryResult(name string, coins sdk.Coins, totalShares sdk.Int) PoolStatsQueryResult {
return PoolStatsQueryResult{
Name: name,
Coins: coins,
TotalShares: totalShares,
}
}
// PoolStatsQueryResults is a slice of PoolStatsQueryResult
type PoolStatsQueryResults []PoolStatsQueryResult

76
x/swap/types/state.go Normal file
View File

@ -0,0 +1,76 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// PoolIDFromCoins returns a poolID from a coins object
func PoolIDFromCoins(coins sdk.Coins) string {
return PoolID(coins[0].Denom, coins[1].Denom)
}
// PoolID returns an alphabetically sorted pool name from two denoms.
// The name is commutative for any all pairs A,B: f(A,B) == f(B,A).
func PoolID(denomA string, denomB string) string {
if denomB < denomA {
return fmt.Sprintf("%s/%s", denomB, denomA)
}
return fmt.Sprintf("%s/%s", denomA, denomB)
}
// PoolRecord represents the state of a liquidity pool
// and is used to store the state of a denominated pool
type PoolRecord struct {
// primary key
PoolID string
ReservesA sdk.Coin `json:"reserves_a" yaml:"reserves_a"`
ReservesB sdk.Coin `json:"reserves_b" yaml:"reserves_b"`
TotalShares sdk.Int `json:"total_shares" yaml:"total_shares"`
}
// Reserves returns the total reserves for a pool
func (p PoolRecord) Reserves() sdk.Coins {
return sdk.NewCoins(p.ReservesA, p.ReservesB)
}
// NewPoolRecord takes a pointer to a denominated pool and returns a
// pool record for storage in state.
func NewPoolRecord(pool *DenominatedPool) PoolRecord {
reserves := pool.Reserves()
poolID := PoolIDFromCoins(reserves)
return PoolRecord{
PoolID: poolID,
ReservesA: reserves[0],
ReservesB: reserves[1],
TotalShares: pool.TotalShares(),
}
}
// PoolRecords is a slice of PoolRecord
type PoolRecords []PoolRecord
// ShareRecord stores the shares owned for a depositor and pool
type ShareRecord struct {
// primary key
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
// secondary / sort key
PoolID string `json:"pool_id" yaml:"pool_id"`
SharesOwned sdk.Int `json:"shares_owned" yaml:"shares_owned"`
}
// NewShareRecord takes a depositor, poolID, and shares and returns
// a new share record for storage in state.
func NewShareRecord(depositor sdk.AccAddress, poolID string, sharesOwned sdk.Int) ShareRecord {
return ShareRecord{
Depositor: depositor,
PoolID: poolID,
SharesOwned: sharesOwned,
}
}
// ShareRecords is a slice of ShareRecord
type ShareRecords []ShareRecord

View File

@ -0,0 +1,63 @@
package types_test
import (
"testing"
types "github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestState_PoolID(t *testing.T) {
testCases := []struct {
reserveA string
reserveB string
expectedID string
}{
{"atoken", "btoken", "atoken/btoken"},
{"btoken", "atoken", "atoken/btoken"},
{"aaa", "aaaa", "aaa/aaaa"},
{"aaaa", "aaa", "aaa/aaaa"},
{"aaaa", "aaab", "aaaa/aaab"},
{"aaab", "aaaa", "aaaa/aaab"},
{"a001", "a002", "a001/a002"},
{"a002", "a001", "a001/a002"},
}
for _, tc := range testCases {
assert.Equal(t, tc.expectedID, types.PoolID(tc.reserveA, tc.reserveB))
assert.Equal(t, tc.expectedID, types.PoolID(tc.reserveB, tc.reserveA))
assert.Equal(t, tc.expectedID, types.PoolIDFromCoins(sdk.NewCoins(sdk.NewCoin(tc.reserveA, i(1)), sdk.NewCoin(tc.reserveB, i(1)))))
assert.Equal(t, tc.expectedID, types.PoolIDFromCoins(sdk.NewCoins(sdk.NewCoin(tc.reserveB, i(1)), sdk.NewCoin(tc.reserveA, i(1)))))
}
}
func TestState_NewPoolRecord(t *testing.T) {
reserves := sdk.NewCoins(usdx(50e6), ukava(10e6))
pool, err := types.NewDenominatedPool(reserves)
require.NoError(t, err)
record := types.NewPoolRecord(pool)
assert.Equal(t, types.PoolID("ukava", "usdx"), record.PoolID)
assert.Equal(t, ukava(10e6), record.ReservesA)
assert.Equal(t, record.ReservesB, usdx(50e6))
assert.Equal(t, pool.TotalShares(), record.TotalShares)
assert.Equal(t, sdk.NewCoins(ukava(10e6), usdx(50e6)), record.Reserves())
}
func TestState_NewShareRecord(t *testing.T) {
depositor := sdk.AccAddress("some user")
poolID := types.PoolID("ukava", "usdx")
shares := sdk.NewInt(1e6)
record := types.NewShareRecord(depositor, poolID, shares)
assert.Equal(t, depositor, record.Depositor)
assert.Equal(t, poolID, record.PoolID)
assert.Equal(t, shares, record.SharesOwned)
}