mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05:18 +00:00
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:
parent
3fc2a63556
commit
65052ce31a
@ -90,6 +90,8 @@ var (
|
||||
)
|
||||
|
||||
// module account permissions
|
||||
// if these are changed, then the permissions
|
||||
// must also be migrated during a chain upgrade
|
||||
mAccPerms = map[string][]string{
|
||||
auth.FeeCollectorName: nil,
|
||||
distr.ModuleName: nil,
|
||||
@ -105,6 +107,7 @@ var (
|
||||
kavadist.ModuleName: {supply.Minter},
|
||||
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
|
||||
hard.ModuleAccountName: {supply.Minter},
|
||||
swap.ModuleAccountName: nil,
|
||||
}
|
||||
|
||||
// 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,
|
||||
keys[swap.StoreKey],
|
||||
swapSubspace,
|
||||
app.accountKeeper,
|
||||
app.supplyKeeper,
|
||||
)
|
||||
app.incentiveKeeper = incentive.NewKeeper(
|
||||
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),
|
||||
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper),
|
||||
swap.NewAppModule(app.swapKeeper),
|
||||
)
|
||||
|
||||
app.sm.RegisterStoreDecoders()
|
||||
|
@ -39,6 +39,7 @@ import (
|
||||
"github.com/kava-labs/kava/x/incentive"
|
||||
"github.com/kava-labs/kava/x/kavadist"
|
||||
"github.com/kava-labs/kava/x/pricefeed"
|
||||
"github.com/kava-labs/kava/x/swap"
|
||||
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[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
|
||||
{app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}},
|
||||
{app.keys[swap.StoreKey], newApp.keys[swap.StoreKey], [][]byte{}},
|
||||
}
|
||||
|
||||
for _, skp := range storeKeysPrefixes {
|
||||
|
@ -2,28 +2,34 @@
|
||||
parent:
|
||||
order: false
|
||||
--->
|
||||
|
||||
# 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.
|
||||
|
||||
## User Interfaces
|
||||
|
||||
- [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)
|
||||
- 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
|
||||
|
||||
- [Mintscan](https://kava.mintscan.io/) by Cosmostation
|
||||
- [Big Dipper](https://kava.bigdipper.live/) by Forbole
|
||||
- [PING.pub](https://kava.ping.pub/#/parameter)
|
||||
- [KAVAScan](https://kavascan.com)
|
||||
|
||||
## For Validators
|
||||
|
||||
- [QuickSync](https://kava.quicksync.io/) chain snapshots
|
||||
|
||||
## Staking
|
||||
|
||||
- [Staking rewards calculator](https://www.stakingrewards.com/earn/kava/calculate)
|
||||
|
||||
### Staking Providers
|
||||
|
||||
Listed in alphabetical order.
|
||||
|
||||
- [ATEAM](https://nodeateam.com/)
|
||||
@ -46,5 +52,3 @@ Listed in alphabetical order.
|
||||
- [StakeWith.Us](https://www.stakewith.us/)
|
||||
- [Staked](https://staked.us/)
|
||||
- [stake.fish](https://stake.fish/en/)
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
v0_15committee "github.com/kava-labs/kava/x/committee/types"
|
||||
v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14"
|
||||
v0_15incentive "github.com/kava-labs/kava/x/incentive/types"
|
||||
v0_15swap "github.com/kava-labs/kava/x/swap/types"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -74,6 +75,8 @@ func MigrateAppState(v0_14AppState genutil.AppMap) {
|
||||
// Marshal v15 committee genesis state
|
||||
v0_14AppState[v0_15committee.ModuleName] = v0_15Codec.MustMarshalJSON(Committee(committeeGS))
|
||||
}
|
||||
|
||||
v0_14AppState[v0_15swap.ModuleName] = v0_15Codec.MustMarshalJSON(Swap())
|
||||
}
|
||||
|
||||
func makeV014Codec() *codec.Codec {
|
||||
@ -92,7 +95,8 @@ func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisS
|
||||
proposals := []v0_15committee.Proposal{}
|
||||
|
||||
for _, com := range genesisState.Committees {
|
||||
if com.ID == 1 {
|
||||
switch com.ID {
|
||||
case 1:
|
||||
// Initialize member committee without permissions
|
||||
stabilityCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members,
|
||||
[]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{})
|
||||
|
||||
// Set stability committee permissions
|
||||
baseStabilityCom := stabilityCom.SetPermissions(newStabilityCommitteePermissions)
|
||||
newStabilityCom := v0_15committee.MemberCommittee{BaseCommittee: baseStabilityCom}
|
||||
newStabilityCom := stabilityCom.SetPermissions(newStabilityCommitteePermissions)
|
||||
committees = append(committees, newStabilityCom)
|
||||
} else {
|
||||
case 2:
|
||||
safetyCom := v0_15committee.NewMemberCommittee(com.ID, com.Description, com.Members,
|
||||
[]v0_15committee.Permission{v0_15committee.SoftwareUpgradePermission{}},
|
||||
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 {
|
||||
newVote := v0_15committee.NewVote(v.ProposalID, v.Voter, v0_15committee.Yes)
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisState {
|
||||
// Migrate params
|
||||
|
@ -43,7 +43,7 @@ func TestCommittee(t *testing.T) {
|
||||
err = newGenState.Validate()
|
||||
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++ {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-7-incentive-state.json"))
|
||||
require.NoError(t, err)
|
||||
|
@ -106,7 +106,6 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
|
||||
VoteThreshold: suite.testGenesis.Committees[0].GetVoteThreshold(),
|
||||
ProposalDuration: suite.testGenesis.Committees[0].GetProposalDuration(),
|
||||
TallyOption: types.FirstPastThePost,
|
||||
Type: types.MemberCommitteeType,
|
||||
},
|
||||
},
|
||||
),
|
||||
|
@ -36,6 +36,7 @@ func RegisterCodec(cdc *codec.Codec) {
|
||||
|
||||
// Committees
|
||||
cdc.RegisterInterface((*Committee)(nil), nil)
|
||||
cdc.RegisterConcrete(BaseCommittee{}, "kava/BaseCommittee", nil)
|
||||
cdc.RegisterConcrete(MemberCommittee{}, "kava/MemberCommittee", nil)
|
||||
cdc.RegisterConcrete(TokenCommittee{}, "kava/TokenCommittee", nil)
|
||||
|
||||
|
@ -24,6 +24,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
BaseCommitteeType = "kava/BaseCommittee"
|
||||
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
|
||||
BondDenom = "ukava"
|
||||
@ -33,6 +34,7 @@ func init() {
|
||||
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
|
||||
// But since these proposals contain Committees, these types also need registering:
|
||||
govtypes.ModuleCdc.RegisterInterface((*Committee)(nil), nil)
|
||||
govtypes.RegisterProposalTypeCodec(BaseCommittee{}, "kava/BaseCommittee")
|
||||
govtypes.RegisterProposalTypeCodec(MemberCommittee{}, "kava/MemberCommittee")
|
||||
govtypes.RegisterProposalTypeCodec(TokenCommittee{}, "kava/TokenCommittee")
|
||||
}
|
||||
@ -130,7 +132,7 @@ type Committee interface {
|
||||
HasMember(addr sdk.AccAddress) bool
|
||||
|
||||
GetPermissions() []Permission
|
||||
SetPermissions([]Permission) BaseCommittee
|
||||
SetPermissions([]Permission) Committee
|
||||
HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool
|
||||
|
||||
GetProposalDuration() time.Duration
|
||||
@ -148,7 +150,6 @@ type Committees []Committee
|
||||
|
||||
// BaseCommittee is a common type shared by all Committees
|
||||
type BaseCommittee struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
ID uint64 `json:"id" yaml:"id"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Members []sdk.AccAddress `json:"members" yaml:"members"`
|
||||
@ -159,7 +160,7 @@ type BaseCommittee struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c BaseCommittee) GetID() uint64 { return c.ID }
|
||||
@ -275,14 +276,13 @@ func (c BaseCommittee) Validate() error {
|
||||
// String implements fmt.Stringer
|
||||
func (c BaseCommittee) String() string {
|
||||
return fmt.Sprintf(`Committee %d:
|
||||
Type: %s
|
||||
Description: %s
|
||||
Members: %s
|
||||
Permissions: %s
|
||||
VoteThreshold: %s
|
||||
ProposalDuration: %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.TallyOption.String(),
|
||||
)
|
||||
@ -298,7 +298,6 @@ func NewMemberCommittee(id uint64, description string, members []sdk.AccAddress,
|
||||
threshold sdk.Dec, duration time.Duration, tallyOption TallyOption) MemberCommittee {
|
||||
return MemberCommittee{
|
||||
BaseCommittee: BaseCommittee{
|
||||
Type: MemberCommitteeType,
|
||||
ID: id,
|
||||
Description: description,
|
||||
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
|
||||
func (c MemberCommittee) Validate() error {
|
||||
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 {
|
||||
return TokenCommittee{
|
||||
BaseCommittee: BaseCommittee{
|
||||
Type: TokenCommitteeType,
|
||||
ID: id,
|
||||
Description: description,
|
||||
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
|
||||
func (c TokenCommittee) GetQuorum() sdk.Dec { return c.Quorum }
|
||||
|
||||
// GetTallyDenom returns the tally denom of the committee
|
||||
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
|
||||
func (c TokenCommittee) Validate() error {
|
||||
if c.TallyDenom == BondDenom {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
@ -199,7 +200,6 @@ func TestMemberCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: MemberCommitteeType,
|
||||
},
|
||||
},
|
||||
expectPass: true,
|
||||
@ -245,7 +245,6 @@ func TestTokenCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: TokenCommitteeType,
|
||||
},
|
||||
Quorum: d("0.4"),
|
||||
TallyDenom: "hard",
|
||||
@ -263,7 +262,6 @@ func TestTokenCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: TokenCommitteeType,
|
||||
},
|
||||
Quorum: sdk.Dec{Int: nil},
|
||||
TallyDenom: "hard",
|
||||
@ -281,7 +279,6 @@ func TestTokenCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: TokenCommitteeType,
|
||||
},
|
||||
Quorum: d("-0.1"),
|
||||
TallyDenom: "hard",
|
||||
@ -299,7 +296,6 @@ func TestTokenCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: TokenCommitteeType,
|
||||
},
|
||||
Quorum: d("1.001"),
|
||||
TallyDenom: "hard",
|
||||
@ -317,7 +313,6 @@ func TestTokenCommittee(t *testing.T) {
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
TallyOption: FirstPastThePost,
|
||||
Type: TokenCommitteeType,
|
||||
},
|
||||
Quorum: d("0.4"),
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ func TestGenesisState_Validate(t *testing.T) {
|
||||
Permissions: []Permission{GodPermission{}},
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
Type: MemberCommitteeType,
|
||||
TallyOption: FirstPastThePost,
|
||||
},
|
||||
},
|
||||
@ -45,7 +44,6 @@ func TestGenesisState_Validate(t *testing.T) {
|
||||
Permissions: nil,
|
||||
VoteThreshold: d("0.8"),
|
||||
ProposalDuration: time.Hour * 24 * 21,
|
||||
Type: MemberCommitteeType,
|
||||
TallyOption: FirstPastThePost,
|
||||
},
|
||||
},
|
||||
@ -57,7 +55,6 @@ func TestGenesisState_Validate(t *testing.T) {
|
||||
Permissions: nil,
|
||||
VoteThreshold: d("0.8"),
|
||||
ProposalDuration: time.Hour * 24 * 21,
|
||||
Type: TokenCommitteeType,
|
||||
TallyOption: Deadline,
|
||||
},
|
||||
Quorum: sdk.MustNewDecFromStr("0.4"),
|
||||
|
@ -1,3 +1,5 @@
|
||||
// Code generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) DO NOT EDIT.
|
||||
|
||||
package swap
|
||||
|
||||
import (
|
||||
@ -6,23 +8,82 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = types.ModuleName
|
||||
QuerierRoute = types.QuerierRoute
|
||||
RouterKey = types.RouterKey
|
||||
StoreKey = types.StoreKey
|
||||
DefaultParamspace = types.DefaultParamspace
|
||||
)
|
||||
|
||||
type (
|
||||
GenesisState = types.GenesisState
|
||||
Keeper = keeper.Keeper
|
||||
AttributeKeyDepositor = types.AttributeKeyDepositor
|
||||
AttributeKeyOwner = types.AttributeKeyOwner
|
||||
AttributeKeyPoolID = types.AttributeKeyPoolID
|
||||
AttributeKeyShares = types.AttributeKeyShares
|
||||
AttributeValueCategory = types.AttributeValueCategory
|
||||
DefaultParamspace = types.DefaultParamspace
|
||||
EventTypeSwapDeposit = types.EventTypeSwapDeposit
|
||||
EventTypeSwapWithdraw = types.EventTypeSwapWithdraw
|
||||
ModuleAccountName = types.ModuleAccountName
|
||||
ModuleName = types.ModuleName
|
||||
QuerierRoute = types.QuerierRoute
|
||||
QueryGetParams = types.QueryGetParams
|
||||
RouterKey = types.RouterKey
|
||||
StoreKey = types.StoreKey
|
||||
)
|
||||
|
||||
var (
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
ModuleCdc = types.ModuleCdc
|
||||
ParamKeyTable = types.ParamKeyTable
|
||||
RegisterCodec = types.RegisterCodec
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
// function aliases
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
DefaultParams = types.DefaultParams
|
||||
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
|
||||
)
|
||||
|
@ -2,17 +2,26 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"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/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/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
|
||||
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
swapQueryCmd := &cobra.Command{
|
||||
@ -25,6 +34,9 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
|
||||
swapQueryCmd.AddCommand(flags.GetCommands(
|
||||
queryParamsCmd(queryRoute, cdc),
|
||||
queryDepositsCmd(queryRoute, cdc),
|
||||
queryPoolCmd(queryRoute, cdc),
|
||||
queryPoolsCmd(queryRoute, cdc),
|
||||
)...)
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,13 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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/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"
|
||||
)
|
||||
@ -22,7 +29,97 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||
RunE: client.ValidateCmd,
|
||||
}
|
||||
|
||||
swapTxCmd.AddCommand(flags.PostCommands()...)
|
||||
swapTxCmd.AddCommand(flags.PostCommands(
|
||||
getCmdDeposit(cdc),
|
||||
getCmdWithdraw(cdc),
|
||||
)...)
|
||||
|
||||
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})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@ package rest
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
@ -14,6 +16,9 @@ import (
|
||||
|
||||
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/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 {
|
||||
@ -35,3 +40,110 @@ func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,39 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
)
|
||||
|
||||
// REST variable names
|
||||
// nolint
|
||||
const ()
|
||||
const (
|
||||
RestPool = "pool"
|
||||
RestOwner = "owner"
|
||||
)
|
||||
|
||||
// RegisterRoutes registers swap-related REST handlers to a router
|
||||
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||
registerQueryRoutes(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"`
|
||||
}
|
||||
|
@ -1,9 +1,62 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||
|
||||
"github.com/kava-labs/kava/x/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})
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,65 @@ package swap
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
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
|
||||
func NewHandler(k Keeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
|
||||
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) {
|
||||
case types.MsgDeposit:
|
||||
return handleMsgDeposit(ctx, k, msg)
|
||||
case types.MsgWithdraw:
|
||||
return handleMsgWithdraw(ctx, k, msg)
|
||||
default:
|
||||
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
360
x/swap/handler_test.go
Normal 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
145
x/swap/keeper/deposit.go
Normal 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
|
||||
}
|
343
x/swap/keeper/deposit_test.go
Normal file
343
x/swap/keeper/deposit_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
@ -9,9 +9,14 @@ import (
|
||||
"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 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 {
|
||||
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
|
||||
|
@ -1,11 +1,12 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
)
|
||||
|
||||
// Keeper keeper for the swap module
|
||||
@ -13,10 +14,18 @@ type Keeper struct {
|
||||
key sdk.StoreKey
|
||||
cdc *codec.Codec
|
||||
paramSubspace subspace.Subspace
|
||||
accountKeeper types.AccountKeeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
}
|
||||
|
||||
// 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() {
|
||||
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
|
||||
}
|
||||
@ -25,5 +34,141 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace)
|
||||
key: key,
|
||||
cdc: cdc,
|
||||
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, ¶ms)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
110
x/swap/keeper/keeper_test.go
Normal file
110
x/swap/keeper/keeper_test.go
Normal 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{})
|
||||
}
|
@ -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, ¶ms)
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
@ -16,6 +19,12 @@ func NewQuerier(k Keeper) sdk.Querier {
|
||||
switch path[0] {
|
||||
case types.QueryGetParams:
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
var params types.QueryDepositsParams
|
||||
err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
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, ¶ms)
|
||||
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
|
||||
}
|
||||
|
@ -1,51 +1,53 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"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"
|
||||
"github.com/stretchr/testify/suite"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/swap/keeper"
|
||||
"github.com/kava-labs/kava/x/swap/testutil"
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
)
|
||||
|
||||
type QuerierTestSuite struct {
|
||||
suite.Suite
|
||||
keeper keeper.Keeper
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
querier sdk.Querier
|
||||
type querierTestSuite struct {
|
||||
testutil.Suite
|
||||
querier sdk.Querier
|
||||
addresses []sdk.AccAddress
|
||||
}
|
||||
|
||||
func (suite *QuerierTestSuite) SetupTest() {
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
func (suite *querierTestSuite) SetupTest() {
|
||||
suite.Suite.SetupTest()
|
||||
|
||||
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(),
|
||||
)
|
||||
|
||||
suite.ctx = ctx
|
||||
suite.app = tApp
|
||||
suite.keeper = tApp.GetSwapKeeper()
|
||||
suite.querier = keeper.NewQuerier(suite.keeper)
|
||||
suite.querier = keeper.NewQuerier(suite.Keeper)
|
||||
}
|
||||
|
||||
func (suite *QuerierTestSuite) TestUnkownRequest() {
|
||||
ctx := suite.ctx.WithIsCheckTx(false)
|
||||
func (suite *querierTestSuite) TestUnkownRequest() {
|
||||
ctx := suite.Ctx.WithIsCheckTx(false)
|
||||
bz, err := suite.querier(ctx, []string{"invalid-path"}, abci.RequestQuery{})
|
||||
suite.Nil(bz)
|
||||
suite.EqualError(err, "unknown request: unknown swap query endpoint")
|
||||
}
|
||||
|
||||
func (suite *QuerierTestSuite) TestQueryParams() {
|
||||
ctx := suite.ctx.WithIsCheckTx(false)
|
||||
func (suite *querierTestSuite) TestQueryParams() {
|
||||
ctx := suite.Ctx.WithIsCheckTx(false)
|
||||
bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
|
||||
suite.Nil(err)
|
||||
suite.NotNil(bz)
|
||||
@ -55,11 +57,117 @@ func (suite *QuerierTestSuite) TestQueryParams() {
|
||||
|
||||
swapGenesisState := NewSwapGenStateMulti()
|
||||
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)
|
||||
}
|
||||
|
||||
func TestQuerierTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(QuerierTestSuite))
|
||||
func (suite *querierTestSuite) TestQueryPool() {
|
||||
// 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
90
x/swap/keeper/withdraw.go
Normal 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)
|
||||
}
|
||||
}
|
223
x/swap/keeper/withdraw_test.go
Normal file
223
x/swap/keeper/withdraw_test.go
Normal 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")
|
||||
}
|
206
x/swap/legacy/v0_15/types.go
Normal file
206
x/swap/legacy/v0_15/types.go
Normal 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(),
|
||||
)
|
||||
}
|
@ -2,6 +2,7 @@ package swap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
@ -10,20 +11,21 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
sim "github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
|
||||
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/rest"
|
||||
"github.com/kava-labs/kava/x/swap/keeper"
|
||||
"github.com/kava-labs/kava/x/swap/simulation"
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ module.AppModule = AppModule{}
|
||||
_ module.AppModuleBasic = AppModuleBasic{}
|
||||
// _ module.AppModuleSimulation = AppModule{}
|
||||
_ module.AppModule = AppModule{}
|
||||
_ module.AppModuleBasic = AppModuleBasic{}
|
||||
_ module.AppModuleSimulation = AppModule{}
|
||||
)
|
||||
|
||||
// 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
|
||||
// func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
|
||||
// simulation.RandomizedGenState(simState)
|
||||
// }
|
||||
// GenerateGenesisState creates a randomized GenState of the swap module
|
||||
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
|
||||
simulation.RandomizedGenState(simState)
|
||||
}
|
||||
|
||||
// // ProposalContents doesn't return any content functions for governance proposals.
|
||||
// func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
|
||||
// return nil
|
||||
// }
|
||||
// ProposalContents doesn't return any content functions for governance proposals.
|
||||
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// // RandomizedParams returns nil because swap has no params.
|
||||
// func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||
// return simulation.ParamChanges(r)
|
||||
// }
|
||||
// RandomizedParams returns nil because swap has no params.
|
||||
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||
return simulation.ParamChanges(r)
|
||||
}
|
||||
|
||||
// // RegisterStoreDecoder registers a decoder for swap module's types
|
||||
// func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
||||
// sdr[StoreKey] = simulation.DecodeStore
|
||||
// }
|
||||
// RegisterStoreDecoder registers a decoder for swap module's types
|
||||
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
||||
sdr[StoreKey] = simulation.DecodeStore
|
||||
}
|
||||
|
||||
// // WeightedOperations returns the all the swap module operations with their respective weights.
|
||||
// func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
|
||||
// return nil
|
||||
// }
|
||||
// WeightedOperations returns the all the swap module operations with their respective weights.
|
||||
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
|
||||
return nil
|
||||
}
|
||||
|
13
x/swap/simulation/decoder.go
Normal file
13
x/swap/simulation/decoder.go
Normal 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 ""
|
||||
}
|
100
x/swap/simulation/genesis.go
Normal file
100
x/swap/simulation/genesis.go
Normal 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
|
||||
}
|
12
x/swap/simulation/operations.go
Normal file
12
x/swap/simulation/operations.go
Normal 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)
|
||||
)
|
32
x/swap/simulation/params.go
Normal file
32
x/swap/simulation/params.go
Normal 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
281
x/swap/testutil/suite.go
Normal 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
437
x/swap/types/base_pool.go
Normal 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()))
|
||||
}
|
||||
}
|
589
x/swap/types/base_pool_test.go
Normal file
589
x/swap/types/base_pool_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
@ -14,5 +14,6 @@ func init() {
|
||||
|
||||
// RegisterCodec registers the necessary types for swap module
|
||||
func RegisterCodec(cdc *codec.Codec) {
|
||||
cdc.RegisterConcrete(AllowedPool{}, "swap/AllowedPool", nil)
|
||||
cdc.RegisterConcrete(MsgDeposit{}, "swap/MsgDeposit", nil)
|
||||
cdc.RegisterConcrete(MsgWithdraw{}, "swap/MsgWithdraw", nil)
|
||||
}
|
||||
|
13
x/swap/types/common_test.go
Normal file
13
x/swap/types/common_test.go
Normal 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()
|
||||
}
|
159
x/swap/types/denominated_pool.go
Normal file
159
x/swap/types/denominated_pool.go
Normal 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)
|
||||
}
|
182
x/swap/types/denominated_pool_test.go
Normal file
182
x/swap/types/denominated_pool_test.go
Normal 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")
|
||||
}
|
@ -4,8 +4,17 @@ import (
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
)
|
||||
|
||||
// DONTCOVER
|
||||
|
||||
// swap module errors
|
||||
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")
|
||||
)
|
||||
|
@ -2,5 +2,11 @@ package types
|
||||
|
||||
// Event types for swap module
|
||||
const (
|
||||
// EventTypeCustom = ""
|
||||
AttributeValueCategory = ModuleName
|
||||
EventTypeSwapDeposit = "swap_deposit"
|
||||
EventTypeSwapWithdraw = "swap_withdraw"
|
||||
AttributeKeyPoolID = "pool_id"
|
||||
AttributeKeyDepositor = "depositor"
|
||||
AttributeKeyShares = "shares"
|
||||
AttributeKeyOwner = "owner"
|
||||
)
|
||||
|
22
x/swap/types/expected_keepers.go
Normal file
22
x/swap/types/expected_keepers.go
Normal 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)
|
||||
}
|
@ -1,9 +1,16 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// ModuleName name that will be used throughout the module
|
||||
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 = ModuleName
|
||||
|
||||
@ -17,6 +24,27 @@ const (
|
||||
DefaultParamspace = ModuleName
|
||||
)
|
||||
|
||||
// key prefixes for store
|
||||
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
18
x/swap/types/keys_test.go
Normal 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))
|
||||
}
|
@ -1 +1,181 @@
|
||||
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
408
x/swap/types/msg_test.go
Normal 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())
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ func ParamKeyTable() params.KeyTable {
|
||||
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 {
|
||||
return params.ParamSetPairs{
|
||||
params.NewParamSetPair(KeyAllowedPools, &p.AllowedPools, validateAllowedPoolsParams),
|
||||
@ -83,9 +83,92 @@ func validateSwapFee(i interface{}) error {
|
||||
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 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
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kava-labs/kava/x/swap/types"
|
||||
@ -86,7 +87,7 @@ func TestParams_ParamSetPairs_AllowedPools(t *testing.T) {
|
||||
|
||||
var paramSetPair *paramstypes.ParamSetPair
|
||||
for _, pair := range defaultParams.ParamSetPairs() {
|
||||
if bytes.Compare(pair.Key, types.KeyAllowedPools) == 0 {
|
||||
if bytes.Equal(pair.Key, types.KeyAllowedPools) {
|
||||
paramSetPair = &pair
|
||||
break
|
||||
}
|
||||
@ -107,7 +108,7 @@ func TestParams_ParamSetPairs_SwapFee(t *testing.T) {
|
||||
|
||||
var paramSetPair *paramstypes.ParamSetPair
|
||||
for _, pair := range defaultParams.ParamSetPairs() {
|
||||
if bytes.Compare(pair.Key, types.KeySwapFee) == 0 {
|
||||
if bytes.Equal(pair.Key, types.KeySwapFee) {
|
||||
paramSetPair = &pair
|
||||
break
|
||||
}
|
||||
@ -183,7 +184,7 @@ func TestParams_Validation(t *testing.T) {
|
||||
testFn: func(params *types.Params) {
|
||||
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
|
||||
for _, pair := range params.ParamSetPairs() {
|
||||
if bytes.Compare(pair.Key, tc.key) == 0 {
|
||||
if bytes.Equal(pair.Key, tc.key) {
|
||||
paramSetPair = &pair
|
||||
break
|
||||
}
|
||||
@ -231,3 +232,177 @@ func TestParams_String(t *testing.T) {
|
||||
assert.Contains(t, output, "ukava/usdx")
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,81 @@
|
||||
package types
|
||||
|
||||
import sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
// Querier routes for the swap module
|
||||
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
76
x/swap/types/state.go
Normal 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
|
63
x/swap/types/state_test.go
Normal file
63
x/swap/types/state_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user