diff --git a/app/app.go b/app/app.go index df12fd43..4f89a711 100644 --- a/app/app.go +++ b/app/app.go @@ -584,18 +584,19 @@ func NewApp( app.pricefeedKeeper, app.auctionKeeper, ) + app.liquidKeeper = liquidkeeper.NewDefaultKeeper( + appCodec, + app.accountKeeper, + app.bankKeeper, + &app.stakingKeeper, + ) savingsKeeper := savingskeeper.NewKeeper( appCodec, keys[savingstypes.StoreKey], savingsSubspace, app.accountKeeper, app.bankKeeper, - ) - app.liquidKeeper = liquidkeeper.NewDefaultKeeper( - appCodec, - app.accountKeeper, - app.bankKeeper, - &app.stakingKeeper, + app.liquidKeeper, ) earnKeeper := earnkeeper.NewKeeper( appCodec, diff --git a/x/savings/keeper/deposit_test.go b/x/savings/keeper/deposit_test.go index 15fbe17f..584e6c66 100644 --- a/x/savings/keeper/deposit_test.go +++ b/x/savings/keeper/deposit_test.go @@ -1,9 +1,12 @@ package keeper_test import ( + "fmt" "strings" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/tendermint/tendermint/crypto" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" @@ -13,6 +16,15 @@ import ( ) func (suite *KeeperTestSuite) TestDeposit() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, delegator := addrs[0], addrs[1] + + valAddr := sdk.ValAddress(valAccAddr) + initialBalance := sdk.NewInt(1e9) + + bkavaDenom := fmt.Sprintf("bkava-%s", valAddr.String()) + invalidBkavaDenom := fmt.Sprintf("bkava-%s", sdk.ValAddress(addrs[2]).String()) + type args struct { allowedDenoms []string depositor sdk.AccAddress @@ -67,6 +79,23 @@ func (suite *KeeperTestSuite) TestDeposit() { contains: "", }, }, + { + "valid bkava", + args{ + allowedDenoms: []string{"bnb", "btcb", "ukava", "bkava"}, + depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialDepositorBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + depositAmount: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + numberDeposits: 1, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + expectedDepositCoins: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, { "invalid deposit denom", args{ @@ -84,6 +113,23 @@ func (suite *KeeperTestSuite) TestDeposit() { contains: "invalid deposit denom", }, }, + { + "invalid bkava", + args{ + allowedDenoms: []string{"bnb", "btcb", "ukava", "bkava"}, + depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialDepositorBalance: sdk.NewCoins(sdk.NewCoin(invalidBkavaDenom, sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + depositAmount: sdk.NewCoins(sdk.NewCoin(invalidBkavaDenom, sdk.NewInt(100))), + numberDeposits: 1, + expectedAccountBalance: sdk.Coins{}, + expectedModAccountBalance: sdk.Coins{}, + expectedDepositCoins: sdk.Coins{}, + }, + errArgs{ + expectPass: false, + contains: "invalid deposit denom", + }, + }, { "insufficient funds", args{ @@ -119,14 +165,26 @@ func (suite *KeeperTestSuite) TestDeposit() { types.Deposits{}, ) + stakingParams := stakingtypes.DefaultParams() + stakingParams.BondDenom = "ukava" + tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: tApp.AppCodec().MustMarshalJSON(&savingsGS)}, + app.GenesisState{stakingtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(stakingtypes.NewGenesisState(stakingParams, nil, nil))}, ) keeper := tApp.GetSavingsKeeper() suite.app = tApp suite.ctx = ctx suite.keeper = keeper + // Create validator and delegate for bkava + suite.CreateAccountWithAddress(valAccAddr, cs(c("ukava", 100e10))) + suite.CreateAccountWithAddress(delegator, cs(c("ukava", 100e10))) + + suite.CreateNewUnbondedValidator(valAddr, initialBalance) + suite.CreateDelegation(valAddr, delegator, initialBalance) + staking.EndBlocker(suite.ctx, suite.app.GetStakingKeeper()) + // run the test var err error for i := 0; i < tc.args.numberDeposits; i++ { diff --git a/x/savings/keeper/keeper.go b/x/savings/keeper/keeper.go index 86b3a2b7..828ef398 100644 --- a/x/savings/keeper/keeper.go +++ b/x/savings/keeper/keeper.go @@ -19,13 +19,14 @@ type Keeper struct { paramSubspace paramtypes.Subspace accountKeeper types.AccountKeeper bankKeeper types.BankKeeper + liquidKeeper types.LiquidKeeper hooks types.SavingsHooks } // NewKeeper returns a new keeper for the savings module. func NewKeeper( cdc codec.Codec, key sdk.StoreKey, paramstore paramtypes.Subspace, - ak types.AccountKeeper, bk types.BankKeeper, + ak types.AccountKeeper, bk types.BankKeeper, lk types.LiquidKeeper, ) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) @@ -37,6 +38,7 @@ func NewKeeper( paramSubspace: paramstore, accountKeeper: ak, bankKeeper: bk, + liquidKeeper: lk, hooks: nil, } } diff --git a/x/savings/keeper/keeper_test.go b/x/savings/keeper/keeper_test.go index 299aaa86..591f300a 100644 --- a/x/savings/keeper/keeper_test.go +++ b/x/savings/keeper/keeper_test.go @@ -8,9 +8,14 @@ import ( tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/savings/keeper" "github.com/kava-labs/kava/x/savings/types" @@ -39,6 +44,10 @@ func (suite *KeeperTestSuite) SetupTest() { suite.ctx = ctx suite.keeper = keeper suite.addrs = addrs + + stakingParams := stakingtypes.DefaultParams() + stakingParams.BondDenom = "ukava" + suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) } func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() { @@ -99,3 +108,87 @@ func (suite *KeeperTestSuite) getModuleAccountAtCtx(name string, ctx sdk.Context func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } + +// CreateAccount creates a new account from the provided balance and address +func (suite *KeeperTestSuite) CreateAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins) authtypes.AccountI { + ak := suite.app.GetAccountKeeper() + + acc := ak.NewAccountWithAddress(suite.ctx, addr) + ak.SetAccount(suite.ctx, acc) + + err := simapp.FundAccount(suite.app.GetBankKeeper(), suite.ctx, acc.GetAddress(), initialBalance) + suite.Require().NoError(err) + + return acc +} + +// CreateVestingAccount creates a new vesting account. `vestingBalance` should be a fraction of `initialBalance`. +func (suite *KeeperTestSuite) CreateVestingAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins, vestingBalance sdk.Coins) authtypes.AccountI { + if vestingBalance.IsAnyGT(initialBalance) { + panic("vesting balance must be less than initial balance") + } + acc := suite.CreateAccountWithAddress(addr, initialBalance) + bacc := acc.(*authtypes.BaseAccount) + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 31556952, + Amount: vestingBalance, + }, + } + vacc := vestingtypes.NewPeriodicVestingAccount(bacc, vestingBalance, suite.ctx.BlockTime().Unix(), periods) + suite.app.GetAccountKeeper().SetAccount(suite.ctx, vacc) + return vacc +} + +func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error { + msg, err := stakingtypes.NewMsgCreateValidator( + address, + ed25519.GenPrivKey().PubKey(), + selfDelegation, + stakingtypes.Description{}, + stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.NewInt(1e6), + ) + if err != nil { + return err + } + + msgServer := stakingkeeper.NewMsgServerImpl(suite.app.GetStakingKeeper()) + _, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.ctx), msg) + return err +} + +// CreateNewUnbondedValidator creates a new validator in the staking module. +// New validators are unbonded until the end blocker is run. +func (suite *KeeperTestSuite) CreateNewUnbondedValidator(addr sdk.ValAddress, selfDelegation sdk.Int) stakingtypes.Validator { + // Create a validator + err := suite.deliverMsgCreateValidator(suite.ctx, addr, sdk.NewCoin("ukava", selfDelegation)) + suite.Require().NoError(err) + + // New validators are created in an unbonded state. Note if the end blocker is run later this validator could become bonded. + + validator, found := suite.app.GetStakingKeeper().GetValidator(suite.ctx, addr) + suite.Require().True(found) + return validator +} + +// CreateDelegation delegates tokens to a validator. +func (suite *KeeperTestSuite) CreateDelegation(valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) sdk.Dec { + sk := suite.app.GetStakingKeeper() + + stakingDenom := sk.BondDenom(suite.ctx) + msg := stakingtypes.NewMsgDelegate( + delegator, + valAddr, + sdk.NewCoin(stakingDenom, amount), + ) + + msgServer := stakingkeeper.NewMsgServerImpl(sk) + _, err := msgServer.Delegate(sdk.WrapSDKContext(suite.ctx), msg) + suite.Require().NoError(err) + + del, found := sk.GetDelegation(suite.ctx, delegator, valAddr) + suite.Require().True(found) + return del.Shares +} diff --git a/x/savings/keeper/params.go b/x/savings/keeper/params.go index 1d67366b..25928204 100644 --- a/x/savings/keeper/params.go +++ b/x/savings/keeper/params.go @@ -3,6 +3,7 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" + liquidtypes "github.com/kava-labs/kava/x/liquid/types" "github.com/kava-labs/kava/x/savings/types" ) @@ -25,6 +26,13 @@ func (k Keeper) IsDenomSupported(ctx sdk.Context, denom string) bool { if supportedDenom == denom { return true } + + if supportedDenom == liquidtypes.DefaultDerivativeDenom { + if k.liquidKeeper.IsDerivativeDenom(ctx, denom) { + return true + } + } } + return false } diff --git a/x/savings/keeper/withdraw_test.go b/x/savings/keeper/withdraw_test.go index 6880e08f..0ef79dd6 100644 --- a/x/savings/keeper/withdraw_test.go +++ b/x/savings/keeper/withdraw_test.go @@ -1,9 +1,12 @@ package keeper_test import ( + "fmt" "strings" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/tendermint/tendermint/crypto" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" @@ -13,6 +16,14 @@ import ( ) func (suite *KeeperTestSuite) TestWithdraw() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, delegator := addrs[0], addrs[1] + + valAddr := sdk.ValAddress(valAccAddr) + initialBalance := sdk.NewInt(1e9) + + bkavaDenom := fmt.Sprintf("bkava-%s", valAddr.String()) + type args struct { allowedDenoms []string depositor sdk.AccAddress @@ -52,6 +63,24 @@ func (suite *KeeperTestSuite) TestWithdraw() { contains: "", }, }, + { + "valid: partial bkava", + args{ + allowedDenoms: []string{"bnb", "btcb", "ukava", "bkava"}, + depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialDepositorBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + depositAmount: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(200))), + withdrawAmount: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + expectedDepositCoins: sdk.NewCoins(sdk.NewCoin(bkavaDenom, sdk.NewInt(100))), + }, + errArgs{ + expectPass: true, + expectDelete: false, + contains: "", + }, + }, { "valid: full withdraw", args{ @@ -122,8 +151,12 @@ func (suite *KeeperTestSuite) TestWithdraw() { types.Deposits{}, ) + stakingParams := stakingtypes.DefaultParams() + stakingParams.BondDenom = "ukava" + tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: tApp.AppCodec().MustMarshalJSON(&savingsGS)}, + app.GenesisState{stakingtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(stakingtypes.NewGenesisState(stakingParams, nil, nil))}, ) keeper := tApp.GetSavingsKeeper() suite.app = tApp @@ -131,6 +164,14 @@ func (suite *KeeperTestSuite) TestWithdraw() { suite.keeper = keeper bankKeeper := tApp.GetBankKeeper() + // Create validator and delegate for bkava + suite.CreateAccountWithAddress(valAccAddr, cs(c("ukava", 100e10))) + suite.CreateAccountWithAddress(delegator, cs(c("ukava", 100e10))) + + suite.CreateNewUnbondedValidator(valAddr, initialBalance) + suite.CreateDelegation(valAddr, delegator, initialBalance) + staking.EndBlocker(suite.ctx, suite.app.GetStakingKeeper()) + err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount) suite.Require().NoError(err) diff --git a/x/savings/types/expected_keepers.go b/x/savings/types/expected_keepers.go index f0f218d2..cc0a1959 100644 --- a/x/savings/types/expected_keepers.go +++ b/x/savings/types/expected_keepers.go @@ -31,3 +31,7 @@ type SavingsHooks interface { AfterSavingsDepositCreated(ctx sdk.Context, deposit Deposit) BeforeSavingsDepositModified(ctx sdk.Context, deposit Deposit, incomingDenoms []string) } + +type LiquidKeeper interface { + IsDerivativeDenom(ctx sdk.Context, denom string) bool +}