mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 03:07:28 +00:00 
			
		
		
		
	liquid staking (#1273)
* proto types * proto generated types * liquidstaking top level files: module, genesis * liquidstaking types * liquidstaking keeper * liquidstaking client/cli * add liquidstaking to app, simapp * implement mint derivative * set up liquidstaking keeper test suite * test mint derivative * rename module to liquid * rename proto types, app.go to liquid * use sdk.Coin instead of shares * mint liquid tokens to delegator * burn derivative tokens to receive delegation * use conversion method instead of type cast * simplify delegation transfer logic * broaden delegation transfer tests * simplify transfer delegation method This removes a source of rounding errors * move derivative denom to keeper config * check for invalid coins in msg validation * block 0share transfers to avoid handling edge case * refactor MintDerivative to test calculations * simplify burn method so shares and tokens equal * convert TransferShares back to old design this makes handling vesting tokens easier * fix missed merge conflict * remove deprecated constants * tidy up msg.go * add msg tests * remove unused store key * fix msg event sender * remove unused params * tidy up documentation and errors * remove unused mocks * remove unused keepers from AppModule * tidy up msg return values keeper return values to be used in router msgs * reinstate unintentionally removed interface check * catch invalid input for MnitDerivative clear up test TODOs * clear up InitGenesis TODO * Update x/liquid/client/cli/tx.go Co-authored-by: Derrick Lee <derrick@dlee.dev> * Update x/liquid/client/cli/tx.go Co-authored-by: Derrick Lee <derrick@dlee.dev> * show error logs in devnet * unblock mod account so it can receive dist rewards * catch zero amout msgs early * minor cli fixes Co-authored-by: rhuairahrighairigh <ruaridh.odonnell@gmail.com> Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com> Co-authored-by: Derrick Lee <derrick@dlee.dev>
This commit is contained in:
		
							parent
							
								
									ded692d2f4
								
							
						
					
					
						commit
						ceaed3f0e1
					
				
							
								
								
									
										22
									
								
								app/app.go
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								app/app.go
									
									
									
									
									
								
							@ -125,6 +125,9 @@ import (
 | 
				
			|||||||
	kavadistclient "github.com/kava-labs/kava/x/kavadist/client"
 | 
						kavadistclient "github.com/kava-labs/kava/x/kavadist/client"
 | 
				
			||||||
	kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
 | 
						kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
 | 
				
			||||||
	kavadisttypes "github.com/kava-labs/kava/x/kavadist/types"
 | 
						kavadisttypes "github.com/kava-labs/kava/x/kavadist/types"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid"
 | 
				
			||||||
 | 
						liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
 | 
				
			||||||
 | 
						liquidtypes "github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
	pricefeed "github.com/kava-labs/kava/x/pricefeed"
 | 
						pricefeed "github.com/kava-labs/kava/x/pricefeed"
 | 
				
			||||||
	pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
 | 
						pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
 | 
				
			||||||
	pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
 | 
						pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
 | 
				
			||||||
@ -190,6 +193,7 @@ var (
 | 
				
			|||||||
		savings.AppModuleBasic{},
 | 
							savings.AppModuleBasic{},
 | 
				
			||||||
		validatorvesting.AppModuleBasic{},
 | 
							validatorvesting.AppModuleBasic{},
 | 
				
			||||||
		evmutil.AppModuleBasic{},
 | 
							evmutil.AppModuleBasic{},
 | 
				
			||||||
 | 
							liquid.AppModuleBasic{},
 | 
				
			||||||
		earn.AppModuleBasic{},
 | 
							earn.AppModuleBasic{},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -215,12 +219,12 @@ var (
 | 
				
			|||||||
		cdptypes.LiquidatorMacc:         {authtypes.Minter, authtypes.Burner},
 | 
							cdptypes.LiquidatorMacc:         {authtypes.Minter, authtypes.Burner},
 | 
				
			||||||
		hardtypes.ModuleAccountName:     {authtypes.Minter},
 | 
							hardtypes.ModuleAccountName:     {authtypes.Minter},
 | 
				
			||||||
		savingstypes.ModuleAccountName:  nil,
 | 
							savingstypes.ModuleAccountName:  nil,
 | 
				
			||||||
		earntypes.ModuleName:            nil,
 | 
							liquidtypes.ModuleAccountName:   {authtypes.Minter, authtypes.Burner},
 | 
				
			||||||
 | 
							earntypes.ModuleAccountName:     nil,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Verify app interface at compile time
 | 
					// Verify app interface at compile time
 | 
				
			||||||
// var _ simapp.App = (*App)(nil) // TODO
 | 
					 | 
				
			||||||
var _ servertypes.Application = (*App)(nil)
 | 
					var _ servertypes.Application = (*App)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Options bundles several configuration params for an App.
 | 
					// Options bundles several configuration params for an App.
 | 
				
			||||||
@ -285,6 +289,7 @@ type App struct {
 | 
				
			|||||||
	committeeKeeper  committeekeeper.Keeper
 | 
						committeeKeeper  committeekeeper.Keeper
 | 
				
			||||||
	incentiveKeeper  incentivekeeper.Keeper
 | 
						incentiveKeeper  incentivekeeper.Keeper
 | 
				
			||||||
	savingsKeeper    savingskeeper.Keeper
 | 
						savingsKeeper    savingskeeper.Keeper
 | 
				
			||||||
 | 
						liquidKeeper     liquidkeeper.Keeper
 | 
				
			||||||
	earnKeeper       earnkeeper.Keeper
 | 
						earnKeeper       earnkeeper.Keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make scoped keepers public for test purposes
 | 
						// make scoped keepers public for test purposes
 | 
				
			||||||
@ -586,6 +591,12 @@ func NewApp(
 | 
				
			|||||||
		app.accountKeeper,
 | 
							app.accountKeeper,
 | 
				
			||||||
		app.bankKeeper,
 | 
							app.bankKeeper,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						app.liquidKeeper = liquidkeeper.NewDefaultKeeper(
 | 
				
			||||||
 | 
							appCodec,
 | 
				
			||||||
 | 
							app.accountKeeper,
 | 
				
			||||||
 | 
							app.bankKeeper,
 | 
				
			||||||
 | 
							&app.stakingKeeper,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	app.incentiveKeeper = incentivekeeper.NewKeeper(
 | 
						app.incentiveKeeper = incentivekeeper.NewKeeper(
 | 
				
			||||||
		appCodec,
 | 
							appCodec,
 | 
				
			||||||
		keys[incentivetypes.StoreKey],
 | 
							keys[incentivetypes.StoreKey],
 | 
				
			||||||
@ -691,6 +702,7 @@ func NewApp(
 | 
				
			|||||||
		incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.bankKeeper, app.cdpKeeper),
 | 
							incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.bankKeeper, app.cdpKeeper),
 | 
				
			||||||
		evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper),
 | 
							evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper),
 | 
				
			||||||
		savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
 | 
							savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
 | 
				
			||||||
 | 
							liquid.NewAppModule(app.liquidKeeper),
 | 
				
			||||||
		earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
 | 
							earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -738,6 +750,7 @@ func NewApp(
 | 
				
			|||||||
		authz.ModuleName,
 | 
							authz.ModuleName,
 | 
				
			||||||
		evmutiltypes.ModuleName,
 | 
							evmutiltypes.ModuleName,
 | 
				
			||||||
		savingstypes.ModuleName,
 | 
							savingstypes.ModuleName,
 | 
				
			||||||
 | 
							liquidtypes.ModuleName,
 | 
				
			||||||
		earntypes.ModuleName,
 | 
							earntypes.ModuleName,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -777,6 +790,7 @@ func NewApp(
 | 
				
			|||||||
		authz.ModuleName,
 | 
							authz.ModuleName,
 | 
				
			||||||
		evmutiltypes.ModuleName,
 | 
							evmutiltypes.ModuleName,
 | 
				
			||||||
		savingstypes.ModuleName,
 | 
							savingstypes.ModuleName,
 | 
				
			||||||
 | 
							liquidtypes.ModuleName,
 | 
				
			||||||
		earntypes.ModuleName,
 | 
							earntypes.ModuleName,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -816,6 +830,7 @@ func NewApp(
 | 
				
			|||||||
		paramstypes.ModuleName,
 | 
							paramstypes.ModuleName,
 | 
				
			||||||
		upgradetypes.ModuleName,
 | 
							upgradetypes.ModuleName,
 | 
				
			||||||
		validatorvestingtypes.ModuleName,
 | 
							validatorvestingtypes.ModuleName,
 | 
				
			||||||
 | 
							liquidtypes.ModuleName,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app.mm.RegisterInvariants(&app.crisisKeeper)
 | 
						app.mm.RegisterInvariants(&app.crisisKeeper)
 | 
				
			||||||
@ -979,10 +994,11 @@ func (app *App) loadBlockedMaccAddrs() map[string]bool {
 | 
				
			|||||||
	modAccAddrs := app.ModuleAccountAddrs()
 | 
						modAccAddrs := app.ModuleAccountAddrs()
 | 
				
			||||||
	kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName)
 | 
						kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName)
 | 
				
			||||||
	earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName)
 | 
						earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName)
 | 
				
			||||||
 | 
						liquidMaccAddr := app.accountKeeper.GetModuleAddress(liquidtypes.ModuleName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for addr := range modAccAddrs {
 | 
						for addr := range modAccAddrs {
 | 
				
			||||||
		// Set the kavadist and earn module account address as unblocked
 | 
							// Set the kavadist and earn module account address as unblocked
 | 
				
			||||||
		if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() {
 | 
							if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() || addr == liquidMaccAddr.String() {
 | 
				
			||||||
			modAccAddrs[addr] = false
 | 
								modAccAddrs[addr] = false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,7 @@ import (
 | 
				
			|||||||
	incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper"
 | 
						incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper"
 | 
				
			||||||
	issuancekeeper "github.com/kava-labs/kava/x/issuance/keeper"
 | 
						issuancekeeper "github.com/kava-labs/kava/x/issuance/keeper"
 | 
				
			||||||
	kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
 | 
						kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
 | 
				
			||||||
 | 
						liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
 | 
				
			||||||
	pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
 | 
						pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
 | 
				
			||||||
	savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
 | 
						savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
 | 
				
			||||||
	swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
 | 
						swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
 | 
				
			||||||
@ -109,6 +110,7 @@ func (tApp TestApp) GetEvmutilKeeper() evmutilkeeper.Keeper     { return tApp.ev
 | 
				
			|||||||
func (tApp TestApp) GetEvmKeeper() *evmkeeper.Keeper            { return tApp.evmKeeper }
 | 
					func (tApp TestApp) GetEvmKeeper() *evmkeeper.Keeper            { return tApp.evmKeeper }
 | 
				
			||||||
func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper     { return tApp.savingsKeeper }
 | 
					func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper     { return tApp.savingsKeeper }
 | 
				
			||||||
func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
 | 
					func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
 | 
				
			||||||
 | 
					func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper       { return tApp.liquidKeeper }
 | 
				
			||||||
func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper           { return tApp.earnKeeper }
 | 
					func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper           { return tApp.earnKeeper }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LegacyAmino returns the app's amino codec.
 | 
					// LegacyAmino returns the app's amino codec.
 | 
				
			||||||
 | 
				
			|||||||
@ -35,6 +35,10 @@ sed -in-place='' 's/enable = false/enable = true/g' $DATA/config/app.toml
 | 
				
			|||||||
# Set evm tracer to json
 | 
					# Set evm tracer to json
 | 
				
			||||||
sed -in-place='' 's/tracer = ""/tracer = "json"/g' $DATA/config/app.toml
 | 
					sed -in-place='' 's/tracer = ""/tracer = "json"/g' $DATA/config/app.toml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enable full error trace to be returned on tx failure
 | 
				
			||||||
 | 
					sed -in-place='' '/iavl-cache-size/a\
 | 
				
			||||||
 | 
					trace = true' $DATA/config/app.toml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set client chain id
 | 
					# Set client chain id
 | 
				
			||||||
sed -in-place='' 's/chain-id = ""/chain-id = "kavalocalnet_8888-1"/g' $DATA/config/client.toml
 | 
					sed -in-place='' 's/chain-id = ""/chain-id = "kavalocalnet_8888-1"/g' $DATA/config/client.toml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -385,6 +385,14 @@
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
    - [Query](#kava.kavadist.v1beta1.Query)
 | 
					    - [Query](#kava.kavadist.v1beta1.Query)
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					- [kava/liquid/v1beta1/tx.proto](#kava/liquid/v1beta1/tx.proto)
 | 
				
			||||||
 | 
					    - [MsgBurnDerivative](#kava.liquid.v1beta1.MsgBurnDerivative)
 | 
				
			||||||
 | 
					    - [MsgBurnDerivativeResponse](#kava.liquid.v1beta1.MsgBurnDerivativeResponse)
 | 
				
			||||||
 | 
					    - [MsgMintDerivative](#kava.liquid.v1beta1.MsgMintDerivative)
 | 
				
			||||||
 | 
					    - [MsgMintDerivativeResponse](#kava.liquid.v1beta1.MsgMintDerivativeResponse)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    - [Msg](#kava.liquid.v1beta1.Msg)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
- [kava/pricefeed/v1beta1/store.proto](#kava/pricefeed/v1beta1/store.proto)
 | 
					- [kava/pricefeed/v1beta1/store.proto](#kava/pricefeed/v1beta1/store.proto)
 | 
				
			||||||
    - [CurrentPrice](#kava.pricefeed.v1beta1.CurrentPrice)
 | 
					    - [CurrentPrice](#kava.pricefeed.v1beta1.CurrentPrice)
 | 
				
			||||||
    - [Market](#kava.pricefeed.v1beta1.Market)
 | 
					    - [Market](#kava.pricefeed.v1beta1.Market)
 | 
				
			||||||
@ -5384,6 +5392,97 @@ Query defines the gRPC querier service.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava/liquid/v1beta1/tx.proto"></a>
 | 
				
			||||||
 | 
					<p align="right"><a href="#top">Top</a></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## kava/liquid/v1beta1/tx.proto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava.liquid.v1beta1.MsgBurnDerivative"></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### MsgBurnDerivative
 | 
				
			||||||
 | 
					MsgBurnDerivative defines the Msg/BurnDerivative request type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Field | Type | Label | Description |
 | 
				
			||||||
 | 
					| ----- | ---- | ----- | ----------- |
 | 
				
			||||||
 | 
					| `sender` | [string](#string) |  | sender is the owner of the derivatives to be converted |
 | 
				
			||||||
 | 
					| `validator` | [string](#string) |  | validator is the validator of the derivatives to be converted |
 | 
				
			||||||
 | 
					| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) |  | amount is the quantity of derivatives to be converted |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava.liquid.v1beta1.MsgBurnDerivativeResponse"></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### MsgBurnDerivativeResponse
 | 
				
			||||||
 | 
					MsgBurnDerivativeResponse defines the Msg/BurnDerivative response type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Field | Type | Label | Description |
 | 
				
			||||||
 | 
					| ----- | ---- | ----- | ----------- |
 | 
				
			||||||
 | 
					| `received` | [string](#string) |  | received is the number of delegation shares sent to the sender |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava.liquid.v1beta1.MsgMintDerivative"></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### MsgMintDerivative
 | 
				
			||||||
 | 
					MsgMintDerivative defines the Msg/MintDerivative request type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Field | Type | Label | Description |
 | 
				
			||||||
 | 
					| ----- | ---- | ----- | ----------- |
 | 
				
			||||||
 | 
					| `sender` | [string](#string) |  | sender is the owner of the delegation to be converted |
 | 
				
			||||||
 | 
					| `validator` | [string](#string) |  | validator is the validator of the delegation to be converted |
 | 
				
			||||||
 | 
					| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) |  | amount is the quantity of staked assets to be converted |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava.liquid.v1beta1.MsgMintDerivativeResponse"></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### MsgMintDerivativeResponse
 | 
				
			||||||
 | 
					MsgMintDerivativeResponse defines the Msg/MintDerivative response type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Field | Type | Label | Description |
 | 
				
			||||||
 | 
					| ----- | ---- | ----- | ----------- |
 | 
				
			||||||
 | 
					| `received` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) |  | received is the amount of staking derivative minted and sent to the sender |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 <!-- end messages -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 <!-- end enums -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 <!-- end HasExtensions -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a name="kava.liquid.v1beta1.Msg"></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Msg
 | 
				
			||||||
 | 
					Msg defines the liquid Msg service.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint |
 | 
				
			||||||
 | 
					| ----------- | ------------ | ------------- | ------------| ------- | -------- |
 | 
				
			||||||
 | 
					| `MintDerivative` | [MsgMintDerivative](#kava.liquid.v1beta1.MsgMintDerivative) | [MsgMintDerivativeResponse](#kava.liquid.v1beta1.MsgMintDerivativeResponse) | MintDerivative defines a method for converting a delegation into staking deriviatives. | |
 | 
				
			||||||
 | 
					| `BurnDerivative` | [MsgBurnDerivative](#kava.liquid.v1beta1.MsgBurnDerivative) | [MsgBurnDerivativeResponse](#kava.liquid.v1beta1.MsgBurnDerivativeResponse) | BurnDerivative defines a method for converting staking deriviatives into a delegation. | |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 <!-- end services -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<a name="kava/pricefeed/v1beta1/store.proto"></a>
 | 
					<a name="kava/pricefeed/v1beta1/store.proto"></a>
 | 
				
			||||||
<p align="right"><a href="#top">Top</a></p>
 | 
					<p align="right"><a href="#top">Top</a></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								proto/kava/liquid/v1beta1/tx.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								proto/kava/liquid/v1beta1/tx.proto
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					syntax = "proto3";
 | 
				
			||||||
 | 
					package kava.liquid.v1beta1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "gogoproto/gogo.proto";
 | 
				
			||||||
 | 
					import "cosmos/base/v1beta1/coin.proto";
 | 
				
			||||||
 | 
					import "cosmos_proto/cosmos.proto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					option go_package = "github.com/kava-labs/kava/x/liquid/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Msg defines the liquid Msg service.
 | 
				
			||||||
 | 
					service Msg {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // MintDerivative defines a method for converting a delegation into staking deriviatives.
 | 
				
			||||||
 | 
					  rpc MintDerivative(MsgMintDerivative) returns (MsgMintDerivativeResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // BurnDerivative defines a method for converting staking deriviatives into a delegation.
 | 
				
			||||||
 | 
					  rpc BurnDerivative(MsgBurnDerivative) returns (MsgBurnDerivativeResponse);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MsgMintDerivative defines the Msg/MintDerivative request type.
 | 
				
			||||||
 | 
					message MsgMintDerivative {
 | 
				
			||||||
 | 
					  // sender is the owner of the delegation to be converted
 | 
				
			||||||
 | 
					  string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 | 
				
			||||||
 | 
					  // validator is the validator of the delegation to be converted
 | 
				
			||||||
 | 
					  string validator = 2;
 | 
				
			||||||
 | 
					  // amount is the quantity of staked assets to be converted
 | 
				
			||||||
 | 
					  cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MsgMintDerivativeResponse defines the Msg/MintDerivative response type.
 | 
				
			||||||
 | 
					message MsgMintDerivativeResponse {
 | 
				
			||||||
 | 
					  // received is the amount of staking derivative minted and sent to the sender
 | 
				
			||||||
 | 
					  cosmos.base.v1beta1.Coin received = 1 [(gogoproto.nullable) = false];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MsgBurnDerivative defines the Msg/BurnDerivative request type.
 | 
				
			||||||
 | 
					message MsgBurnDerivative {
 | 
				
			||||||
 | 
					  // sender is the owner of the derivatives to be converted
 | 
				
			||||||
 | 
					  string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 | 
				
			||||||
 | 
					  // validator is the validator of the derivatives to be converted
 | 
				
			||||||
 | 
					  string validator = 2;
 | 
				
			||||||
 | 
					  // amount is the quantity of derivatives to be converted
 | 
				
			||||||
 | 
					  cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MsgBurnDerivativeResponse defines the Msg/BurnDerivative response type.
 | 
				
			||||||
 | 
					message MsgBurnDerivativeResponse {
 | 
				
			||||||
 | 
					  // received is the number of delegation shares sent to the sender
 | 
				
			||||||
 | 
					  string received = 1 [
 | 
				
			||||||
 | 
					    (cosmos_proto.scalar)  = "cosmos.Dec",
 | 
				
			||||||
 | 
					    (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
 | 
				
			||||||
 | 
					    (gogoproto.nullable)   = false
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								x/liquid/client/cli/query.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								x/liquid/client/cli/query.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					package cli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client/flags"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetQueryCmd returns the cli query commands for this module
 | 
				
			||||||
 | 
					func GetQueryCmd() *cobra.Command {
 | 
				
			||||||
 | 
						liquidQueryCmd := &cobra.Command{
 | 
				
			||||||
 | 
							Use:                        types.ModuleName,
 | 
				
			||||||
 | 
							Short:                      "Querying commands for the liquid module",
 | 
				
			||||||
 | 
							DisableFlagParsing:         true,
 | 
				
			||||||
 | 
							SuggestionsMinimumDistance: 2,
 | 
				
			||||||
 | 
							RunE:                       client.ValidateCmd,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmds := []*cobra.Command{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, cmd := range cmds {
 | 
				
			||||||
 | 
							flags.AddQueryFlagsToCmd(cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						liquidQueryCmd.AddCommand(cmds...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return liquidQueryCmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										122
									
								
								x/liquid/client/cli/tx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								x/liquid/client/cli/tx.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
				
			|||||||
 | 
					package cli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client/flags"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client/tx"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/version"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTxCmd returns the transaction commands for this module
 | 
				
			||||||
 | 
					func GetTxCmd() *cobra.Command {
 | 
				
			||||||
 | 
						liquidTxCmd := &cobra.Command{
 | 
				
			||||||
 | 
							Use:                        types.ModuleName,
 | 
				
			||||||
 | 
							Short:                      "liquid transactions subcommands",
 | 
				
			||||||
 | 
							DisableFlagParsing:         true,
 | 
				
			||||||
 | 
							SuggestionsMinimumDistance: 2,
 | 
				
			||||||
 | 
							RunE:                       client.ValidateCmd,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmds := []*cobra.Command{
 | 
				
			||||||
 | 
							getCmdMintDerivative(),
 | 
				
			||||||
 | 
							getCmdBurnDerivative(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, cmd := range cmds {
 | 
				
			||||||
 | 
							flags.AddTxFlagsToCmd(cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						liquidTxCmd.AddCommand(cmds...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return liquidTxCmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getCmdMintDerivative() *cobra.Command {
 | 
				
			||||||
 | 
						return &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "mint [validator-addr] [amount]",
 | 
				
			||||||
 | 
							Short: "mints staking derivative from a delegation",
 | 
				
			||||||
 | 
							Long:  "Mint removes a portion of a user's staking delegation and issues them validator specific staking derivative tokens.",
 | 
				
			||||||
 | 
							Args:  cobra.ExactArgs(2),
 | 
				
			||||||
 | 
							Example: fmt.Sprintf(
 | 
				
			||||||
 | 
								`%s tx %s mint kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
								clientCtx, err := client.GetClientTxContext(cmd)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								valAddr, err := sdk.ValAddressFromBech32(args[0])
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								coin, err := sdk.ParseCoinNormalized(args[1])
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								msg := types.NewMsgMintDerivative(clientCtx.GetFromAddress(), valAddr, coin)
 | 
				
			||||||
 | 
								if err := msg.ValidateBasic(); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getCmdBurnDerivative() *cobra.Command {
 | 
				
			||||||
 | 
						return &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "burn [amount]",
 | 
				
			||||||
 | 
							Short: "burns staking derivative to redeem a delegation",
 | 
				
			||||||
 | 
							Long:  "Burn removes some staking derivative from a user's account and converts it back to a staking delegation.",
 | 
				
			||||||
 | 
							Example: fmt.Sprintf(
 | 
				
			||||||
 | 
								`%s tx %s burn 10000000bkava-kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd --from <key>`, version.AppName, types.ModuleName,
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							Args: cobra.ExactArgs(1),
 | 
				
			||||||
 | 
							RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
								clientCtx, err := client.GetClientTxContext(cmd)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								amount, err := sdk.ParseCoinNormalized(args[0])
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								valAddr, err := parseLiquidStakingTokenDenom(amount.Denom)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return sdkerrors.Wrap(types.ErrInvalidDenom, err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								msg := types.NewMsgBurnDerivative(clientCtx.GetFromAddress(), valAddr, amount)
 | 
				
			||||||
 | 
								if err := msg.ValidateBasic(); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseLiquidStakingTokenDenom extracts a validator address from a derivative denom.
 | 
				
			||||||
 | 
					func parseLiquidStakingTokenDenom(denom string) (sdk.ValAddress, error) {
 | 
				
			||||||
 | 
						elements := strings.Split(denom, types.DenomSeparator)
 | 
				
			||||||
 | 
						if len(elements) != 2 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("cannot parse denom %s", denom)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						addr, err := sdk.ValAddressFromBech32(elements[1])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return addr, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										118
									
								
								x/liquid/keeper/derivative.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								x/liquid/keeper/derivative.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					package keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MintDerivative removes a user's staking delegation and mints them equivalent staking derivative coins.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The input staking token amount is used to calculate shares in the user's delegation, which are transferred to a delegation owned by the module.
 | 
				
			||||||
 | 
					// Derivative coins are them minted and transferred to the user.
 | 
				
			||||||
 | 
					func (k Keeper) MintDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error) {
 | 
				
			||||||
 | 
						bondDenom := k.stakingKeeper.BondDenom(ctx)
 | 
				
			||||||
 | 
						if amount.Denom != bondDenom {
 | 
				
			||||||
 | 
							return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidDenom, "expected %s", bondDenom)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						derivativeAmount, shares, err := k.CalculateDerivativeSharesFromTokens(ctx, delegatorAddr, valAddr, amount.Amount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Coin{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fetching the module account will create it if it doesn't exist.
 | 
				
			||||||
 | 
						// This is necessary as otherwise TransferDelegation will create a normal account.
 | 
				
			||||||
 | 
						modAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
 | 
				
			||||||
 | 
						if _, err := k.TransferDelegation(ctx, valAddr, delegatorAddr, modAcc.GetAddress(), shares); err != nil {
 | 
				
			||||||
 | 
							return sdk.Coin{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						liquidTokenDenom := k.GetLiquidStakingTokenDenom(valAddr)
 | 
				
			||||||
 | 
						liquidToken := sdk.NewCoin(liquidTokenDenom, derivativeAmount)
 | 
				
			||||||
 | 
						if err = k.mintCoins(ctx, delegatorAddr, sdk.NewCoins(liquidToken)); err != nil {
 | 
				
			||||||
 | 
							return sdk.Coin{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.EventManager().EmitEvent(
 | 
				
			||||||
 | 
							sdk.NewEvent(
 | 
				
			||||||
 | 
								types.EventTypeMintDerivative,
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeyDelegator, delegatorAddr.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeyAmount, liquidToken.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeySharesTransferred, shares.String()),
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return liquidToken, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CalculateDerivativeSharesFromTokens converts a staking token amount into its equivalent delegation shares, and staking derivative amount.
 | 
				
			||||||
 | 
					// This combines the code for calculating the shares to be transferred, and the derivative coins to be minted.
 | 
				
			||||||
 | 
					func (k Keeper) CalculateDerivativeSharesFromTokens(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress, tokens sdk.Int) (sdk.Int, sdk.Dec, error) {
 | 
				
			||||||
 | 
						if !tokens.IsPositive() {
 | 
				
			||||||
 | 
							return sdk.Int{}, sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "token amount must be positive")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						shares, err := k.stakingKeeper.ValidateUnbondAmount(ctx, delegator, validator, tokens)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Int{}, sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return shares.TruncateInt(), shares, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BurnDerivative burns an user's staking derivative coins and returns them an equivalent staking delegation.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The derivative coins are burned, and an equivalent number of shares in the module's staking delegation are transferred back to the user.
 | 
				
			||||||
 | 
					func (k Keeper) BurnDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Dec, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if amount.Denom != k.GetLiquidStakingTokenDenom(valAddr) {
 | 
				
			||||||
 | 
							return sdk.Dec{}, sdkerrors.Wrap(types.ErrInvalidDenom, "derivative denom does not match validator")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := k.burnCoins(ctx, delegatorAddr, sdk.NewCoins(amount)); err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						modAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
 | 
				
			||||||
 | 
						shares := amount.Amount.ToDec()
 | 
				
			||||||
 | 
						receivedShares, err := k.TransferDelegation(ctx, valAddr, modAcc.GetAddress(), delegatorAddr, shares)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.EventManager().EmitEvent(
 | 
				
			||||||
 | 
							sdk.NewEvent(
 | 
				
			||||||
 | 
								types.EventTypeBurnDerivative,
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeyDelegator, delegatorAddr.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
 | 
				
			||||||
 | 
								sdk.NewAttribute(types.AttributeKeySharesTransferred, shares.String()),
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return receivedShares, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (k Keeper) GetLiquidStakingTokenDenom(valAddr sdk.ValAddress) string {
 | 
				
			||||||
 | 
						return types.GetLiquidStakingTokenDenom(k.derivativeDenom, valAddr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (k Keeper) mintCoins(ctx sdk.Context, receiver sdk.AccAddress, amount sdk.Coins) error {
 | 
				
			||||||
 | 
						if err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, amount); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, receiver, amount); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (k Keeper) burnCoins(ctx sdk.Context, sender sdk.AccAddress, amount sdk.Coins) error {
 | 
				
			||||||
 | 
						if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, amount); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := k.bankKeeper.BurnCoins(ctx, types.ModuleAccountName, amount); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										316
									
								
								x/liquid/keeper/derivative_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								x/liquid/keeper/derivative_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,316 @@
 | 
				
			|||||||
 | 
					package keeper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/x/staking"
 | 
				
			||||||
 | 
						stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestBurnDerivative() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(5)
 | 
				
			||||||
 | 
						valAccAddr, user := addrs[0], addrs[1]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						liquidDenom := suite.Keeper.GetLiquidStakingTokenDenom(valAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name             string
 | 
				
			||||||
 | 
							balance          sdk.Coin
 | 
				
			||||||
 | 
							moduleDelegation sdk.Int
 | 
				
			||||||
 | 
							burnAmount       sdk.Coin
 | 
				
			||||||
 | 
							expectedErr      error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "user can burn their entire balance",
 | 
				
			||||||
 | 
								balance:          c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								moduleDelegation: i(1e9),
 | 
				
			||||||
 | 
								burnAmount:       c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "user can burn minimum derivative unit",
 | 
				
			||||||
 | 
								balance:          c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								moduleDelegation: i(1e9),
 | 
				
			||||||
 | 
								burnAmount:       c(liquidDenom, 1),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "error when denom cannot be parsed",
 | 
				
			||||||
 | 
								balance:          c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								moduleDelegation: i(1e9),
 | 
				
			||||||
 | 
								burnAmount:       c(fmt.Sprintf("ckava-%s", valAddr), 1e6),
 | 
				
			||||||
 | 
								expectedErr:      types.ErrInvalidDenom,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "error when burn amount is 0",
 | 
				
			||||||
 | 
								balance:          c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								moduleDelegation: i(1e9),
 | 
				
			||||||
 | 
								burnAmount:       c(liquidDenom, 0),
 | 
				
			||||||
 | 
								expectedErr:      types.ErrUntransferableShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "error when user doesn't have enough funds",
 | 
				
			||||||
 | 
								balance:          c("ukava", 10),
 | 
				
			||||||
 | 
								moduleDelegation: i(1e9),
 | 
				
			||||||
 | 
								burnAmount:       c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								expectedErr:      sdkerrors.ErrInsufficientFunds,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "error when backing delegation isn't large enough",
 | 
				
			||||||
 | 
								balance:          c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								moduleDelegation: i(999_999_999),
 | 
				
			||||||
 | 
								burnAmount:       c(liquidDenom, 1e9),
 | 
				
			||||||
 | 
								expectedErr:      stakingtypes.ErrNotEnoughDelegationShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e6)))
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(user, sdk.NewCoins(tc.balance))
 | 
				
			||||||
 | 
								suite.AddCoinsToModule(types.ModuleAccountName, suite.NewBondCoins(tc.moduleDelegation))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// create delegation from module account to back the derivatives
 | 
				
			||||||
 | 
								moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName)
 | 
				
			||||||
 | 
								suite.CreateNewUnbondedValidator(valAddr, i(1e6))
 | 
				
			||||||
 | 
								suite.CreateDelegation(valAddr, moduleAccAddress, tc.moduleDelegation)
 | 
				
			||||||
 | 
								staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
								modBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, moduleAccAddress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err := suite.Keeper.BurnDerivative(suite.Ctx, user, valAddr, tc.burnAmount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.Require().ErrorIs(err, tc.expectedErr)
 | 
				
			||||||
 | 
								if tc.expectedErr != nil {
 | 
				
			||||||
 | 
									// if an error is expected, state should be reverted so don't need to test state is unchanged
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.AccountBalanceEqual(user, sdk.NewCoins(tc.balance.Sub(tc.burnAmount)))
 | 
				
			||||||
 | 
								suite.AccountBalanceEqual(moduleAccAddress, modBalance) // ensure derivatives are burned, and not in module account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								sharesTransferred := tc.burnAmount.Amount.ToDec()
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, user, sharesTransferred)
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, moduleAccAddress, tc.moduleDelegation.ToDec().Sub(sharesTransferred))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
 | 
				
			||||||
 | 
									types.EventTypeBurnDerivative,
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeyDelegator, user.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(sdk.AttributeKeyAmount, tc.burnAmount.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeySharesTransferred, sharesTransferred.String()),
 | 
				
			||||||
 | 
								))
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestCalculateShares() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(5)
 | 
				
			||||||
 | 
						valAccAddr, delegator := addrs[0], addrs[1]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type returns struct {
 | 
				
			||||||
 | 
							derivatives sdk.Int
 | 
				
			||||||
 | 
							shares      sdk.Dec
 | 
				
			||||||
 | 
							err         error
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						type validator struct {
 | 
				
			||||||
 | 
							tokens          sdk.Int
 | 
				
			||||||
 | 
							delegatorShares sdk.Dec
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name       string
 | 
				
			||||||
 | 
							validator  *validator
 | 
				
			||||||
 | 
							delegation sdk.Dec
 | 
				
			||||||
 | 
							transfer   sdk.Int
 | 
				
			||||||
 | 
							expected   returns
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error when validator not found",
 | 
				
			||||||
 | 
								validator:  nil,
 | 
				
			||||||
 | 
								delegation: d("1000000000"),
 | 
				
			||||||
 | 
								transfer:   i(500e6),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: stakingtypes.ErrNoValidatorFound,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error when delegation not found",
 | 
				
			||||||
 | 
								validator:  &validator{i(1e9), d("1000000000")},
 | 
				
			||||||
 | 
								delegation: sdk.Dec{},
 | 
				
			||||||
 | 
								transfer:   i(500e6),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: stakingtypes.ErrNoDelegation,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error when transfer < 0",
 | 
				
			||||||
 | 
								validator:  &validator{i(10), d("10")},
 | 
				
			||||||
 | 
								delegation: d("10"),
 | 
				
			||||||
 | 
								transfer:   i(-1),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: types.ErrUntransferableShares,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{ // disallow zero transfers
 | 
				
			||||||
 | 
								name:       "error when transfer = 0",
 | 
				
			||||||
 | 
								validator:  &validator{i(10), d("10")},
 | 
				
			||||||
 | 
								delegation: d("10"),
 | 
				
			||||||
 | 
								transfer:   i(0),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: types.ErrUntransferableShares,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error when transfer > delegated shares",
 | 
				
			||||||
 | 
								validator:  &validator{i(10), d("10")},
 | 
				
			||||||
 | 
								delegation: d("10"),
 | 
				
			||||||
 | 
								transfer:   i(11),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: sdkerrors.ErrInvalidRequest,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error when validator has no tokens",
 | 
				
			||||||
 | 
								validator:  &validator{i(0), d("10")},
 | 
				
			||||||
 | 
								delegation: d("10"),
 | 
				
			||||||
 | 
								transfer:   i(5),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: stakingtypes.ErrInsufficientShares,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "shares and derivatives are truncated",
 | 
				
			||||||
 | 
								validator:  &validator{i(3), d("4")},
 | 
				
			||||||
 | 
								delegation: d("4"),
 | 
				
			||||||
 | 
								transfer:   i(2),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									derivatives: i(2),                      // truncated down
 | 
				
			||||||
 | 
									shares:      d("2.666666666666666666"), // 2/3 * 4 not rounded to ...667
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "error if calculated shares > shares in delegation",
 | 
				
			||||||
 | 
								validator:  &validator{i(3), d("4")},
 | 
				
			||||||
 | 
								delegation: d("2.666666666666666665"), // one less than 2/3 * 4
 | 
				
			||||||
 | 
								transfer:   i(2),
 | 
				
			||||||
 | 
								expected: returns{
 | 
				
			||||||
 | 
									err: sdkerrors.ErrInvalidRequest,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if tc.validator != nil {
 | 
				
			||||||
 | 
									suite.StakingKeeper.SetValidator(suite.Ctx, stakingtypes.Validator{
 | 
				
			||||||
 | 
										OperatorAddress: valAddr.String(),
 | 
				
			||||||
 | 
										Tokens:          tc.validator.tokens,
 | 
				
			||||||
 | 
										DelegatorShares: tc.validator.delegatorShares,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !tc.delegation.IsNil() {
 | 
				
			||||||
 | 
									suite.StakingKeeper.SetDelegation(suite.Ctx, stakingtypes.Delegation{
 | 
				
			||||||
 | 
										DelegatorAddress: delegator.String(),
 | 
				
			||||||
 | 
										ValidatorAddress: valAddr.String(),
 | 
				
			||||||
 | 
										Shares:           tc.delegation,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								derivatives, shares, err := suite.Keeper.CalculateDerivativeSharesFromTokens(suite.Ctx, delegator, valAddr, tc.transfer)
 | 
				
			||||||
 | 
								if tc.expected.err != nil {
 | 
				
			||||||
 | 
									suite.ErrorIs(err, tc.expected.err)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.NoError(err)
 | 
				
			||||||
 | 
									suite.Equal(tc.expected.derivatives, derivatives, "expected '%s' got '%s'", tc.expected.derivatives, derivatives)
 | 
				
			||||||
 | 
									suite.Equal(tc.expected.shares, shares)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestMintDerivative() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(5)
 | 
				
			||||||
 | 
						valAccAddr, delegator := addrs[0], addrs[1]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
						moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initialBalance := i(1e9)
 | 
				
			||||||
 | 
						vestedBalance := i(500e6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name                    string
 | 
				
			||||||
 | 
							amount                  sdk.Coin
 | 
				
			||||||
 | 
							expectedDerivatives     sdk.Int
 | 
				
			||||||
 | 
							expectedSharesRemaining sdk.Dec
 | 
				
			||||||
 | 
							expectedSharesAdded     sdk.Dec
 | 
				
			||||||
 | 
							expectedErr             error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:                    "derivative is minted",
 | 
				
			||||||
 | 
								amount:                  suite.NewBondCoin(vestedBalance),
 | 
				
			||||||
 | 
								expectedDerivatives:     i(500e6),
 | 
				
			||||||
 | 
								expectedSharesRemaining: d("500000000.0"),
 | 
				
			||||||
 | 
								expectedSharesAdded:     d("500000000.0"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:        "error when the input denom isn't correct",
 | 
				
			||||||
 | 
								amount:      sdk.NewCoin("invalid", i(1000)),
 | 
				
			||||||
 | 
								expectedErr: types.ErrInvalidDenom,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:        "error when shares cannot be calculated",
 | 
				
			||||||
 | 
								amount:      suite.NewBondCoin(initialBalance.Mul(i(100))),
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidRequest,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:        "error when shares cannot be transferred",
 | 
				
			||||||
 | 
								amount:      suite.NewBondCoin(initialBalance), // trying to move vesting coins will fail in `TransferShares`
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInsufficientFunds,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
								suite.CreateVestingAccountWithAddress(delegator, suite.NewBondCoins(initialBalance), suite.NewBondCoins(vestedBalance))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.CreateNewUnbondedValidator(valAddr, initialBalance)
 | 
				
			||||||
 | 
								suite.CreateDelegation(valAddr, delegator, initialBalance)
 | 
				
			||||||
 | 
								staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err := suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr, tc.amount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.Require().ErrorIs(err, tc.expectedErr)
 | 
				
			||||||
 | 
								if tc.expectedErr != nil {
 | 
				
			||||||
 | 
									// if an error is expected, state should be reverted so don't need to test state is unchanged
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								derivative := sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("bkava-%s", valAddr), tc.expectedDerivatives))
 | 
				
			||||||
 | 
								suite.AccountBalanceEqual(delegator, derivative)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, delegator, tc.expectedSharesRemaining)
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, moduleAccAddress, tc.expectedSharesAdded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								sharesTransferred := initialBalance.ToDec().Sub(tc.expectedSharesRemaining)
 | 
				
			||||||
 | 
								suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent(
 | 
				
			||||||
 | 
									types.EventTypeMintDerivative,
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeyDelegator, delegator.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(sdk.AttributeKeyAmount, derivative.String()),
 | 
				
			||||||
 | 
									sdk.NewAttribute(types.AttributeKeySharesTransferred, sharesTransferred.String()),
 | 
				
			||||||
 | 
								))
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								x/liquid/keeper/keeper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								x/liquid/keeper/keeper.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					package keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/codec"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						"github.com/tendermint/tendermint/libs/log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Keeper struct for the liquid module.
 | 
				
			||||||
 | 
					type Keeper struct {
 | 
				
			||||||
 | 
						cdc codec.Codec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						accountKeeper types.AccountKeeper
 | 
				
			||||||
 | 
						bankKeeper    types.BankKeeper
 | 
				
			||||||
 | 
						stakingKeeper types.StakingKeeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						derivativeDenom string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewKeeper returns a new keeper for the liquid module.
 | 
				
			||||||
 | 
					func NewKeeper(
 | 
				
			||||||
 | 
						cdc codec.Codec,
 | 
				
			||||||
 | 
						ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper,
 | 
				
			||||||
 | 
						derivativeDenom string,
 | 
				
			||||||
 | 
					) Keeper {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return Keeper{
 | 
				
			||||||
 | 
							cdc:             cdc,
 | 
				
			||||||
 | 
							accountKeeper:   ak,
 | 
				
			||||||
 | 
							bankKeeper:      bk,
 | 
				
			||||||
 | 
							stakingKeeper:   sk,
 | 
				
			||||||
 | 
							derivativeDenom: derivativeDenom,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDefaultKeeper returns a new keeper for the liquid module with default values.
 | 
				
			||||||
 | 
					func NewDefaultKeeper(
 | 
				
			||||||
 | 
						cdc codec.Codec,
 | 
				
			||||||
 | 
						ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper,
 | 
				
			||||||
 | 
					) Keeper {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return NewKeeper(cdc, ak, bk, sk, types.DefaultDerivativeDenom)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Logger returns a module-specific logger.
 | 
				
			||||||
 | 
					func (k Keeper) Logger(ctx sdk.Context) log.Logger {
 | 
				
			||||||
 | 
						return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										238
									
								
								x/liquid/keeper/keeper_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								x/liquid/keeper/keeper_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					package keeper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
 | 
				
			||||||
 | 
						"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"
 | 
				
			||||||
 | 
						bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
 | 
				
			||||||
 | 
						stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
 | 
				
			||||||
 | 
						stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/suite"
 | 
				
			||||||
 | 
						abci "github.com/tendermint/tendermint/abci/types"
 | 
				
			||||||
 | 
						tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
 | 
				
			||||||
 | 
						tmtime "github.com/tendermint/tendermint/types/time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/keeper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test suite used for all keeper tests
 | 
				
			||||||
 | 
					type KeeperTestSuite struct {
 | 
				
			||||||
 | 
						suite.Suite
 | 
				
			||||||
 | 
						App           app.TestApp
 | 
				
			||||||
 | 
						Ctx           sdk.Context
 | 
				
			||||||
 | 
						Keeper        keeper.Keeper
 | 
				
			||||||
 | 
						BankKeeper    bankkeeper.Keeper
 | 
				
			||||||
 | 
						StakingKeeper stakingkeeper.Keeper
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The default state used by each test
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) SetupTest() {
 | 
				
			||||||
 | 
						tApp := app.NewTestApp()
 | 
				
			||||||
 | 
						ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tApp.InitializeFromGenesisStates()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.App = tApp
 | 
				
			||||||
 | 
						suite.Ctx = ctx
 | 
				
			||||||
 | 
						suite.Keeper = tApp.GetLiquidKeeper()
 | 
				
			||||||
 | 
						suite.StakingKeeper = tApp.GetStakingKeeper()
 | 
				
			||||||
 | 
						suite.BankKeeper = tApp.GetBankKeeper()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateAccount creates a new account (with a fixed address) from the provided balance.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) CreateAccount(initialBalance sdk.Coins) authtypes.AccountI {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return suite.CreateAccountWithAddress(addrs[0], initialBalance)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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.BankKeeper, 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddCoinsToModule adds coins to the a module account, creating it if it doesn't exist.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) AddCoinsToModule(module string, amount sdk.Coins) {
 | 
				
			||||||
 | 
						err := simapp.FundModuleAccount(suite.BankKeeper, suite.Ctx, module, amount)
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AccountBalanceEqual checks if an account has the specified coins.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
 | 
				
			||||||
 | 
						balance := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
 | 
				
			||||||
 | 
						suite.Equalf(coins, balance, "expected account balance to equal coins %s, but got %s", coins, balance)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.StakingKeeper)
 | 
				
			||||||
 | 
						_, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewBondCoin creates a Coin with the current staking denom.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) NewBondCoin(amount sdk.Int) sdk.Coin {
 | 
				
			||||||
 | 
						stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
 | 
				
			||||||
 | 
						return sdk.NewCoin(stakingDenom, amount)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewBondCoins creates Coins with the current staking denom.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) NewBondCoins(amount sdk.Int) sdk.Coins {
 | 
				
			||||||
 | 
						return sdk.NewCoins(suite.NewBondCoin(amount))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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, suite.NewBondCoin(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.StakingKeeper.GetValidator(suite.Ctx, addr)
 | 
				
			||||||
 | 
						suite.Require().True(found)
 | 
				
			||||||
 | 
						return validator
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SlashValidator burns tokens staked in a validator. new_tokens = old_tokens * (1-slashFraction)
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) SlashValidator(addr sdk.ValAddress, slashFraction sdk.Dec) {
 | 
				
			||||||
 | 
						validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, addr)
 | 
				
			||||||
 | 
						suite.Require().True(found)
 | 
				
			||||||
 | 
						consAddr, err := validator.GetConsAddr()
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Assume infraction was at current height. Note unbonding delegations and redelegations are only slashed if created after
 | 
				
			||||||
 | 
						// the infraction height so none will be slashed.
 | 
				
			||||||
 | 
						infractionHeight := suite.Ctx.BlockHeight()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						power := suite.StakingKeeper.TokensToConsensusPower(suite.Ctx, validator.GetTokens())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.StakingKeeper.Slash(suite.Ctx, consAddr, infractionHeight, power, slashFraction)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateDelegation delegates tokens to a validator.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) CreateDelegation(valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) sdk.Dec {
 | 
				
			||||||
 | 
						stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
 | 
				
			||||||
 | 
						msg := stakingtypes.NewMsgDelegate(
 | 
				
			||||||
 | 
							delegator,
 | 
				
			||||||
 | 
							valAddr,
 | 
				
			||||||
 | 
							sdk.NewCoin(stakingDenom, amount),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
 | 
				
			||||||
 | 
						_, err := msgServer.Delegate(sdk.WrapSDKContext(suite.Ctx), msg)
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
 | 
				
			||||||
 | 
						suite.Require().True(found)
 | 
				
			||||||
 | 
						return del.Shares
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateRedelegation undelegates tokens from one validator and delegates to another.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) CreateRedelegation(delegator sdk.AccAddress, fromValidator, toValidator sdk.ValAddress, amount sdk.Int) {
 | 
				
			||||||
 | 
						stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
 | 
				
			||||||
 | 
						msg := stakingtypes.NewMsgBeginRedelegate(
 | 
				
			||||||
 | 
							delegator,
 | 
				
			||||||
 | 
							fromValidator,
 | 
				
			||||||
 | 
							toValidator,
 | 
				
			||||||
 | 
							sdk.NewCoin(stakingDenom, amount),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
 | 
				
			||||||
 | 
						_, err := msgServer.BeginRedelegate(sdk.WrapSDKContext(suite.Ctx), msg)
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DelegationSharesEqual checks if a delegation has the specified shares.
 | 
				
			||||||
 | 
					// It expects delegations with zero shares to not be stored in state.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) DelegationSharesEqual(valAddr sdk.ValAddress, delegator sdk.AccAddress, shares sdk.Dec) bool {
 | 
				
			||||||
 | 
						del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if shares.IsZero() {
 | 
				
			||||||
 | 
							return suite.Falsef(found, "expected delegator to not be found, got %s shares", del.Shares)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							res := suite.True(found, "expected delegator to be found")
 | 
				
			||||||
 | 
							return res && suite.Truef(shares.Equal(del.Shares), "expected %s delegator shares but got %s", shares, del.Shares)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EventsContains asserts that the expected event is in the provided events
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) 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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EventsDoNotContainType asserts that the provided events do contain an event of a certain type.
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) EventsDoNotContainType(events sdk.Events, eventType string) {
 | 
				
			||||||
 | 
						for _, event := range events {
 | 
				
			||||||
 | 
							suite.Falsef(event.Type == eventType, "found unexpected event %s", eventType)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func attrsToMap(attrs []abci.EventAttribute) []sdk.Attribute {
 | 
				
			||||||
 | 
						out := []sdk.Attribute{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, attr := range attrs {
 | 
				
			||||||
 | 
							out = append(out, sdk.NewAttribute(string(attr.Key), string(attr.Value)))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return out
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestKeeperTestSuite(t *testing.T) {
 | 
				
			||||||
 | 
						suite.Run(t, new(KeeperTestSuite))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								x/liquid/keeper/msg_server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								x/liquid/keeper/msg_server.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					package keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type msgServer struct {
 | 
				
			||||||
 | 
						keeper Keeper
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewMsgServerImpl returns an implementation of the liquid MsgServer interface
 | 
				
			||||||
 | 
					// for the provided Keeper.
 | 
				
			||||||
 | 
					func NewMsgServerImpl(keeper Keeper) types.MsgServer {
 | 
				
			||||||
 | 
						return &msgServer{keeper: keeper}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ types.MsgServer = msgServer{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MintDerivative handles MintDerivative msgs.
 | 
				
			||||||
 | 
					func (k msgServer) MintDerivative(goCtx context.Context, msg *types.MsgMintDerivative) (*types.MsgMintDerivativeResponse, error) {
 | 
				
			||||||
 | 
						ctx := sdk.UnwrapSDKContext(goCtx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						validator, err := sdk.ValAddressFromBech32(msg.Validator)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sender, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mintedDerivative, err := k.keeper.MintDerivative(ctx, sender, validator, msg.Amount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.EventManager().EmitEvent(
 | 
				
			||||||
 | 
							sdk.NewEvent(
 | 
				
			||||||
 | 
								sdk.EventTypeMessage,
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &types.MsgMintDerivativeResponse{
 | 
				
			||||||
 | 
							Received: mintedDerivative,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BurnDerivative handles BurnDerivative msgs.
 | 
				
			||||||
 | 
					func (k msgServer) BurnDerivative(goCtx context.Context, msg *types.MsgBurnDerivative) (*types.MsgBurnDerivativeResponse, error) {
 | 
				
			||||||
 | 
						ctx := sdk.UnwrapSDKContext(goCtx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sender, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						validator, err := sdk.ValAddressFromBech32(msg.Validator)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sharesReceived, err := k.keeper.BurnDerivative(ctx, sender, validator, msg.Amount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.EventManager().EmitEvent(
 | 
				
			||||||
 | 
							sdk.NewEvent(
 | 
				
			||||||
 | 
								sdk.EventTypeMessage,
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
 | 
				
			||||||
 | 
								sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return &types.MsgBurnDerivativeResponse{
 | 
				
			||||||
 | 
							Received: sharesReceived,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										109
									
								
								x/liquid/keeper/staking.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								x/liquid/keeper/staking.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					package keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TransferDelegation moves some delegation shares between addresses, while keeping the same validator.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Internally shares are unbonded, tokens moved then bonded again. This limits only vested tokens from being transferred.
 | 
				
			||||||
 | 
					// The sending delegation must not have any active redelegations.
 | 
				
			||||||
 | 
					// A validator cannot reduce self delegated shares below its min self delegation.
 | 
				
			||||||
 | 
					// Attempting to transfer zero shares will error.
 | 
				
			||||||
 | 
					func (k Keeper) TransferDelegation(ctx sdk.Context, valAddr sdk.ValAddress, fromDelegator, toDelegator sdk.AccAddress, shares sdk.Dec) (sdk.Dec, error) {
 | 
				
			||||||
 | 
						// Redelegations link a delegation to it's previous validator so slashes are propagated to the new validator.
 | 
				
			||||||
 | 
						// If the delegation is transferred to a new owner, the redelegation object must be updated.
 | 
				
			||||||
 | 
						// For expediency all transfers with redelegations are blocked.
 | 
				
			||||||
 | 
						if k.stakingKeeper.HasReceivingRedelegation(ctx, fromDelegator, valAddr) {
 | 
				
			||||||
 | 
							return sdk.Dec{}, types.ErrRedelegationsNotCompleted
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if shares.IsNil() || shares.LT(sdk.ZeroDec()) {
 | 
				
			||||||
 | 
							return sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "nil or negative shares")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if shares.Equal(sdk.ZeroDec()) {
 | 
				
			||||||
 | 
							// Block 0 transfers to reduce edge cases.
 | 
				
			||||||
 | 
							return sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "zero shares")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fromDelegation, found := k.stakingKeeper.GetDelegation(ctx, fromDelegator, valAddr)
 | 
				
			||||||
 | 
						if !found {
 | 
				
			||||||
 | 
							return sdk.Dec{}, types.ErrNoDelegatorForAddress
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						validator, found := k.stakingKeeper.GetValidator(ctx, valAddr)
 | 
				
			||||||
 | 
						if !found {
 | 
				
			||||||
 | 
							return sdk.Dec{}, types.ErrNoValidatorFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Prevent validators from reducing their self delegation below the min.
 | 
				
			||||||
 | 
						isValidatorOperator := fromDelegator.Equals(valAddr)
 | 
				
			||||||
 | 
						if isValidatorOperator {
 | 
				
			||||||
 | 
							if isBelowMinSelfDelegation(validator, fromDelegation.Shares.Sub(shares)) {
 | 
				
			||||||
 | 
								return sdk.Dec{}, types.ErrSelfDelegationBelowMinimum
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						returnAmount, err := k.fastUndelegate(ctx, valAddr, fromDelegator, shares)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						returnCoins := sdk.NewCoins(sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), returnAmount))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := k.bankKeeper.SendCoins(ctx, fromDelegator, toDelegator, returnCoins); err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						receivedShares, err := k.delegateFromAccount(ctx, valAddr, toDelegator, returnAmount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return receivedShares, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// isBelowMinSelfDelegation check if the supplied shares, converted to tokens, are under the validator's min_self_delegation.
 | 
				
			||||||
 | 
					func isBelowMinSelfDelegation(validator stakingtypes.ValidatorI, shares sdk.Dec) bool {
 | 
				
			||||||
 | 
						return validator.TokensFromShares(shares).TruncateInt().LT(validator.GetMinSelfDelegation())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fastUndelegate undelegates shares from a validator skipping the unbonding period and not creating any unbonding delegations.
 | 
				
			||||||
 | 
					func (k Keeper) fastUndelegate(ctx sdk.Context, valAddr sdk.ValAddress, delegator sdk.AccAddress, shares sdk.Dec) (sdk.Int, error) {
 | 
				
			||||||
 | 
						validator, found := k.stakingKeeper.GetValidator(ctx, valAddr)
 | 
				
			||||||
 | 
						if !found {
 | 
				
			||||||
 | 
							return sdk.Int{}, types.ErrNoDelegatorForAddress
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						returnAmount, err := k.stakingKeeper.Unbond(ctx, delegator, valAddr, shares)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Int{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						returnCoins := sdk.NewCoins(sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), returnAmount))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// transfer the validator tokens to the not bonded pool
 | 
				
			||||||
 | 
						if validator.IsBonded() {
 | 
				
			||||||
 | 
							if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, returnCoins); err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(ctx, stakingtypes.NotBondedPoolName, delegator, returnCoins); err != nil {
 | 
				
			||||||
 | 
							return sdk.Int{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return returnAmount, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// delegateFromAccount delegates to a validator from an account (vs redelegating from an existing delegation)
 | 
				
			||||||
 | 
					func (k Keeper) delegateFromAccount(ctx sdk.Context, valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) (sdk.Dec, error) {
 | 
				
			||||||
 | 
						validator, found := k.stakingKeeper.GetValidator(ctx, valAddr)
 | 
				
			||||||
 | 
						if !found {
 | 
				
			||||||
 | 
							return sdk.Dec{}, types.ErrNoValidatorFound
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// source tokens are from an account, so subtractAccount true and tokenSrc unbonded
 | 
				
			||||||
 | 
						newShares, err := k.stakingKeeper.Delegate(ctx, delegator, amount, stakingtypes.Unbonded, validator, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdk.Dec{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return newShares, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										378
									
								
								x/liquid/keeper/staking_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								x/liquid/keeper/staking_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,378 @@
 | 
				
			|||||||
 | 
					package keeper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/x/staking"
 | 
				
			||||||
 | 
						stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
 | 
				
			||||||
 | 
						stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// d is an alias for sdk.MustNewDecFromStr
 | 
				
			||||||
 | 
						d = sdk.MustNewDecFromStr
 | 
				
			||||||
 | 
						// i is an alias for sdk.NewInt
 | 
				
			||||||
 | 
						i = sdk.NewInt
 | 
				
			||||||
 | 
						// c is an alias for sdk.NewInt64Coin
 | 
				
			||||||
 | 
						c = sdk.NewInt64Coin
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_ValidatorStates() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(3)
 | 
				
			||||||
 | 
						valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initialBalance := i(1e9)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						notBondedModAddr := authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName)
 | 
				
			||||||
 | 
						bondedModAddr := authtypes.NewModuleAddress(stakingtypes.BondedPoolName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name            string
 | 
				
			||||||
 | 
							createValidator func() (delegatorShares sdk.Dec, err error)
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "bonded validator",
 | 
				
			||||||
 | 
								createValidator: func() (sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, initialBalance)
 | 
				
			||||||
 | 
									delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Run end blocker to update validator state to bonded.
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return delegatorShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "unbonded validator",
 | 
				
			||||||
 | 
								createValidator: func() (sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, initialBalance)
 | 
				
			||||||
 | 
									delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Don't run end blocker, new validators are by default unbonded.
 | 
				
			||||||
 | 
									return delegatorShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "ubonding (jailed) validator",
 | 
				
			||||||
 | 
								createValidator: func() (sdk.Dec, error) {
 | 
				
			||||||
 | 
									val := suite.CreateNewUnbondedValidator(valAddr, initialBalance)
 | 
				
			||||||
 | 
									delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Run end blocker to update validator state to bonded.
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Jail and run end blocker to transition validator to unbonding.
 | 
				
			||||||
 | 
									consAddr, err := val.GetConsAddr()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return sdk.Dec{}, err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									suite.StakingKeeper.Jail(suite.Ctx, consAddr)
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return delegatorShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								fromDelegationShares, err := tc.createValidator()
 | 
				
			||||||
 | 
								suite.Require().NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
 | 
				
			||||||
 | 
								suite.Require().True(found)
 | 
				
			||||||
 | 
								notBondedBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, notBondedModAddr)
 | 
				
			||||||
 | 
								bondedBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, bondedModAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								shares := d("1000")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, shares)
 | 
				
			||||||
 | 
								suite.Require().NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Transferring a delegation should move shares, and leave the validator and pool balances the same.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(shares))
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, toDelegator, shares) // also creates new delegation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								validatorAfter, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
 | 
				
			||||||
 | 
								suite.Require().True(found)
 | 
				
			||||||
 | 
								suite.Equal(validator.GetTokens(), validatorAfter.GetTokens())
 | 
				
			||||||
 | 
								suite.Equal(validator.GetDelegatorShares(), validatorAfter.GetDelegatorShares())
 | 
				
			||||||
 | 
								suite.Equal(validator.GetStatus(), validatorAfter.GetStatus())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.AccountBalanceEqual(notBondedModAddr, notBondedBalance)
 | 
				
			||||||
 | 
								suite.AccountBalanceEqual(bondedModAddr, bondedBalance)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_Shares() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(5)
 | 
				
			||||||
 | 
						valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initialBalance := i(1e12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name              string
 | 
				
			||||||
 | 
							createDelegations func() (fromDelegatorShares, toDelegatorShares sdk.Dec, err error)
 | 
				
			||||||
 | 
							shares            sdk.Dec
 | 
				
			||||||
 | 
							expectReceived    sdk.Dec
 | 
				
			||||||
 | 
							expectedErr       error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "negative shares cannot be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									// Run end blocker to update validator state to bonded.
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:      d("-1.0"),
 | 
				
			||||||
 | 
								expectedErr: types.ErrUntransferableShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "nil shares cannot be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:      sdk.Dec{},
 | 
				
			||||||
 | 
								expectedErr: types.ErrUntransferableShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "0 shares cannot be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(2e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, toDelegationShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:      sdk.ZeroDec(),
 | 
				
			||||||
 | 
								expectedErr: types.ErrUntransferableShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "all shares can be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(2e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, toDelegationShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:         d("1000000000.0"),
 | 
				
			||||||
 | 
								expectReceived: d("1000000000.0"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "excess shares cannot be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:      d("1000000000.000000000000000001"),
 | 
				
			||||||
 | 
								expectedErr: stakingtypes.ErrNotEnoughDelegationShares,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "shares can be transferred to a non existent delegation",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:         d("500000000.0"),
 | 
				
			||||||
 | 
								expectReceived: d("500000000.0"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "shares cannot be transferred from a non existent delegation",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return sdk.ZeroDec(), sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:      d("500000000.0"),
 | 
				
			||||||
 | 
								expectedErr: types.ErrNoDelegatorForAddress,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "slashed validator shares can be transferred",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									suite.SlashValidator(valAddr, d("0.05"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, sdk.ZeroDec(), nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:         d("500000000.0"),
 | 
				
			||||||
 | 
								expectReceived: d("500000000.0"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "zero shares received when transfer < 1 token",
 | 
				
			||||||
 | 
								createDelegations: func() (sdk.Dec, sdk.Dec, error) {
 | 
				
			||||||
 | 
									suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
									fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
									toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(1e9))
 | 
				
			||||||
 | 
									staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
									// make 1 share worth more than 1 token
 | 
				
			||||||
 | 
									suite.SlashValidator(valAddr, d("0.05"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return fromDelegationShares, toDelegationShares, nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								shares:         d("1.0"), // send 1 share (truncates to zero tokens)
 | 
				
			||||||
 | 
								expectReceived: d("0.0"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
								suite.CreateAccountWithAddress(toDelegator, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								fromDelegationShares, toDelegationShares, err := tc.createDelegations()
 | 
				
			||||||
 | 
								suite.Require().NoError(err)
 | 
				
			||||||
 | 
								validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
 | 
				
			||||||
 | 
								suite.Require().True(found)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, tc.shares)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if tc.expectedErr != nil {
 | 
				
			||||||
 | 
									suite.ErrorIs(err, tc.expectedErr)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.NoError(err)
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(tc.shares))
 | 
				
			||||||
 | 
								suite.DelegationSharesEqual(valAddr, toDelegator, toDelegationShares.Add(tc.expectReceived))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								validatorAfter, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
 | 
				
			||||||
 | 
								suite.Require().True(found)
 | 
				
			||||||
 | 
								// total tokens should not change
 | 
				
			||||||
 | 
								suite.Equal(validator.GetTokens(), validatorAfter.GetTokens())
 | 
				
			||||||
 | 
								// but total shares can differ
 | 
				
			||||||
 | 
								suite.Equal(
 | 
				
			||||||
 | 
									validator.GetDelegatorShares().Sub(tc.shares).Add(tc.expectReceived),
 | 
				
			||||||
 | 
									validatorAfter.GetDelegatorShares(),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_RedelegationsForbidden() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(4)
 | 
				
			||||||
 | 
						val1AccAddr, val2AccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2], addrs[3]
 | 
				
			||||||
 | 
						val1Addr := sdk.ValAddress(val1AccAddr)
 | 
				
			||||||
 | 
						val2Addr := sdk.ValAddress(val2AccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initialBalance := i(1e12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(val1AccAddr, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(val2AccAddr, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(initialBalance))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create bonded validator 1 with a delegation
 | 
				
			||||||
 | 
						suite.CreateNewUnbondedValidator(val1Addr, i(1e9))
 | 
				
			||||||
 | 
						fromDelegationShares := suite.CreateDelegation(val1Addr, fromDelegator, i(1e9))
 | 
				
			||||||
 | 
						staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create validator 2 and redelegate to it
 | 
				
			||||||
 | 
						suite.CreateNewUnbondedValidator(val2Addr, i(1e9))
 | 
				
			||||||
 | 
						suite.CreateRedelegation(fromDelegator, val1Addr, val2Addr, i(1e9))
 | 
				
			||||||
 | 
						staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := suite.Keeper.TransferDelegation(suite.Ctx, val2Addr, fromDelegator, toDelegator, fromDelegationShares)
 | 
				
			||||||
 | 
						suite.ErrorIs(err, types.ErrRedelegationsNotCompleted)
 | 
				
			||||||
 | 
						suite.DelegationSharesEqual(val2Addr, fromDelegator, fromDelegationShares)
 | 
				
			||||||
 | 
						suite.DelegationSharesEqual(val2Addr, toDelegator, sdk.ZeroDec())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_CompliesWithMinSelfDelegation() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(4)
 | 
				
			||||||
 | 
						valAccAddr, toDelegator := addrs[0], addrs[1]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e12)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create bonded validator with minimum delegated
 | 
				
			||||||
 | 
						minSelfDelegation := i(1e9)
 | 
				
			||||||
 | 
						delegation := suite.NewBondCoin(i(1e9))
 | 
				
			||||||
 | 
						msg, err := stakingtypes.NewMsgCreateValidator(
 | 
				
			||||||
 | 
							valAddr,
 | 
				
			||||||
 | 
							ed25519.GenPrivKey().PubKey(),
 | 
				
			||||||
 | 
							delegation,
 | 
				
			||||||
 | 
							stakingtypes.Description{},
 | 
				
			||||||
 | 
							stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
 | 
				
			||||||
 | 
							minSelfDelegation,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
 | 
				
			||||||
 | 
						_, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg)
 | 
				
			||||||
 | 
						suite.Require().NoError(err)
 | 
				
			||||||
 | 
						staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, valAccAddr, toDelegator, d("0.000000000000000001"))
 | 
				
			||||||
 | 
						suite.ErrorIs(err, types.ErrSelfDelegationBelowMinimum)
 | 
				
			||||||
 | 
						suite.DelegationSharesEqual(valAddr, valAccAddr, delegation.Amount.ToDec())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_CanTransferVested() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(4)
 | 
				
			||||||
 | 
						valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
						suite.CreateVestingAccountWithAddress(fromDelegator, suite.NewBondCoins(i(2e9)), suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
						fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(2e9))
 | 
				
			||||||
 | 
						staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shares := d("1000000000.0")
 | 
				
			||||||
 | 
						_, err := suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, shares)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(shares))
 | 
				
			||||||
 | 
						suite.DelegationSharesEqual(valAddr, toDelegator, shares)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *KeeperTestSuite) TestTransferDelegation_CannotTransferVesting() {
 | 
				
			||||||
 | 
						_, addrs := app.GeneratePrivKeyAddressPairs(4)
 | 
				
			||||||
 | 
						valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2]
 | 
				
			||||||
 | 
						valAddr := sdk.ValAddress(valAccAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
						suite.CreateVestingAccountWithAddress(fromDelegator, suite.NewBondCoins(i(2e9)), suite.NewBondCoins(i(1e9)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.CreateNewUnbondedValidator(valAddr, i(1e9))
 | 
				
			||||||
 | 
						suite.CreateDelegation(valAddr, fromDelegator, i(2e9))
 | 
				
			||||||
 | 
						staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, d("1000000001.0"))
 | 
				
			||||||
 | 
						suite.ErrorIs(err, sdkerrors.ErrInsufficientFunds)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										138
									
								
								x/liquid/module.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								x/liquid/module.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					package liquid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
 | 
						"github.com/grpc-ecosystem/grpc-gateway/runtime"
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/client"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/codec"
 | 
				
			||||||
 | 
						codectypes "github.com/cosmos/cosmos-sdk/codec/types"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/types/module"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						abci "github.com/tendermint/tendermint/abci/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/client/cli"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/keeper"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						_ module.AppModule      = AppModule{}
 | 
				
			||||||
 | 
						_ module.AppModuleBasic = AppModuleBasic{}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AppModuleBasic app module basics object
 | 
				
			||||||
 | 
					type AppModuleBasic struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Name get module name
 | 
				
			||||||
 | 
					func (AppModuleBasic) Name() string {
 | 
				
			||||||
 | 
						return types.ModuleName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterLegacyAminoCodec register module codec
 | 
				
			||||||
 | 
					func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
 | 
				
			||||||
 | 
						types.RegisterLegacyAminoCodec(cdc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DefaultGenesis default genesis state
 | 
				
			||||||
 | 
					func (AppModuleBasic) DefaultGenesis(_ codec.JSONCodec) json.RawMessage {
 | 
				
			||||||
 | 
						return []byte("{}")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ValidateGenesis module validate genesis
 | 
				
			||||||
 | 
					func (AppModuleBasic) ValidateGenesis(_ codec.JSONCodec, _ client.TxEncodingConfig, _ json.RawMessage) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterInterfaces implements InterfaceModule.RegisterInterfaces
 | 
				
			||||||
 | 
					func (a AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) {
 | 
				
			||||||
 | 
						types.RegisterInterfaces(registry)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterRESTRoutes registers REST routes for the module.
 | 
				
			||||||
 | 
					func (a AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module.
 | 
				
			||||||
 | 
					func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTxCmd returns the root tx command for the module.
 | 
				
			||||||
 | 
					func (AppModuleBasic) GetTxCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return cli.GetTxCmd()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetQueryCmd returns no root query command for the module.
 | 
				
			||||||
 | 
					func (AppModuleBasic) GetQueryCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return cli.GetQueryCmd()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//____________________________________________________________________________
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AppModule app module type
 | 
				
			||||||
 | 
					type AppModule struct {
 | 
				
			||||||
 | 
						AppModuleBasic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keeper keeper.Keeper
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewAppModule creates a new AppModule object
 | 
				
			||||||
 | 
					func NewAppModule(keeper keeper.Keeper) AppModule {
 | 
				
			||||||
 | 
						return AppModule{
 | 
				
			||||||
 | 
							AppModuleBasic: AppModuleBasic{},
 | 
				
			||||||
 | 
							keeper:         keeper,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Name module name
 | 
				
			||||||
 | 
					func (am AppModule) Name() string {
 | 
				
			||||||
 | 
						return am.AppModuleBasic.Name()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterInvariants register module invariants
 | 
				
			||||||
 | 
					func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Route module message route name
 | 
				
			||||||
 | 
					func (am AppModule) Route() sdk.Route {
 | 
				
			||||||
 | 
						return sdk.Route{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// QuerierRoute module querier route name
 | 
				
			||||||
 | 
					func (AppModule) QuerierRoute() string {
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LegacyQuerierHandler returns no sdk.Querier.
 | 
				
			||||||
 | 
					func (am AppModule) LegacyQuerierHandler(_ *codec.LegacyAmino) sdk.Querier {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConsensusVersion implements AppModule/ConsensusVersion.
 | 
				
			||||||
 | 
					func (AppModule) ConsensusVersion() uint64 {
 | 
				
			||||||
 | 
						return 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterServices registers module services.
 | 
				
			||||||
 | 
					func (am AppModule) RegisterServices(cfg module.Configurator) {
 | 
				
			||||||
 | 
						types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InitGenesis module init-genesis
 | 
				
			||||||
 | 
					func (am AppModule) InitGenesis(_ sdk.Context, _ codec.JSONCodec, _ json.RawMessage) []abci.ValidatorUpdate {
 | 
				
			||||||
 | 
						return []abci.ValidatorUpdate{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ExportGenesis module export genesis
 | 
				
			||||||
 | 
					func (am AppModule) ExportGenesis(_ sdk.Context, cdc codec.JSONCodec) json.RawMessage {
 | 
				
			||||||
 | 
						return am.DefaultGenesis(cdc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BeginBlock module begin-block
 | 
				
			||||||
 | 
					func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EndBlock module end-block
 | 
				
			||||||
 | 
					func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
 | 
				
			||||||
 | 
						return []abci.ValidatorUpdate{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								x/liquid/types/codec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								x/liquid/types/codec.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/codec"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/codec/types"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/types/msgservice"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the module.
 | 
				
			||||||
 | 
					func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
 | 
				
			||||||
 | 
						cdc.RegisterConcrete(&MsgMintDerivative{}, "liquid/MsgMintDerivative", nil)
 | 
				
			||||||
 | 
						cdc.RegisterConcrete(&MsgBurnDerivative{}, "liquid/MsgBurnDerivative", nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterInterfaces registers proto messages under their interfaces for unmarshalling,
 | 
				
			||||||
 | 
					// in addition to registering the msg service for handling tx msgs.
 | 
				
			||||||
 | 
					func RegisterInterfaces(registry types.InterfaceRegistry) {
 | 
				
			||||||
 | 
						registry.RegisterImplementations((*sdk.Msg)(nil),
 | 
				
			||||||
 | 
							&MsgMintDerivative{},
 | 
				
			||||||
 | 
							&MsgBurnDerivative{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						amino     = codec.NewLegacyAmino()
 | 
				
			||||||
 | 
						ModuleCdc = codec.NewAminoCodec(amino)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						RegisterLegacyAminoCodec(amino)
 | 
				
			||||||
 | 
						cryptocodec.RegisterCrypto(amino)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								x/liquid/types/common_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								x/liquid/types/common_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					package types_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMain(m *testing.M) {
 | 
				
			||||||
 | 
						app.SetSDKConfig()
 | 
				
			||||||
 | 
						os.Exit(m.Run())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								x/liquid/types/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								x/liquid/types/errors.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ErrNoValidatorFound           = sdkerrors.New(ModuleName, 2, "validator does not exist")
 | 
				
			||||||
 | 
						ErrNoDelegatorForAddress      = sdkerrors.New(ModuleName, 3, "delegator does not contain delegation")
 | 
				
			||||||
 | 
						ErrInvalidDenom               = sdkerrors.New(ModuleName, 4, "invalid denom")
 | 
				
			||||||
 | 
						ErrNotEnoughDelegationShares  = sdkerrors.New(ModuleName, 5, "not enough delegation shares")
 | 
				
			||||||
 | 
						ErrRedelegationsNotCompleted  = sdkerrors.New(ModuleName, 6, "active redelegations cannot be transferred")
 | 
				
			||||||
 | 
						ErrUntransferableShares       = sdkerrors.New(ModuleName, 7, "shares cannot be transferred")
 | 
				
			||||||
 | 
						ErrSelfDelegationBelowMinimum = sdkerrors.Register(ModuleName, 8, "validator's self delegation must be greater than their minimum self delegation")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										11
									
								
								x/liquid/types/events.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								x/liquid/types/events.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						EventTypeMintDerivative = "mint_derivative"
 | 
				
			||||||
 | 
						EventTypeBurnDerivative = "burn_derivative"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						AttributeValueCategory        = ModuleName
 | 
				
			||||||
 | 
						AttributeKeyDelegator         = "delegator"
 | 
				
			||||||
 | 
						AttributeKeyValidator         = "validator"
 | 
				
			||||||
 | 
						AttributeKeySharesTransferred = "shares_transferred"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										46
									
								
								x/liquid/types/expected_keepers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								x/liquid/types/expected_keepers.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
						stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BankKeeper defines the expected bank keeper
 | 
				
			||||||
 | 
					type BankKeeper interface {
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
						SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
 | 
				
			||||||
 | 
						BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
 | 
				
			||||||
 | 
						UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AccountKeeper defines the expected keeper interface for interacting with account
 | 
				
			||||||
 | 
					type AccountKeeper interface {
 | 
				
			||||||
 | 
						GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StakingKeeper defines the expected keeper interface for interacting with staking
 | 
				
			||||||
 | 
					type StakingKeeper interface {
 | 
				
			||||||
 | 
						BondDenom(ctx sdk.Context) (res string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool)
 | 
				
			||||||
 | 
						GetDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (delegation types.Delegation, found bool)
 | 
				
			||||||
 | 
						HasReceivingRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, valDstAddr sdk.ValAddress) bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ValidateUnbondAmount(
 | 
				
			||||||
 | 
							ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt sdk.Int,
 | 
				
			||||||
 | 
						) (shares sdk.Dec, err error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Delegate(
 | 
				
			||||||
 | 
							ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Int, tokenSrc stakingtypes.BondStatus,
 | 
				
			||||||
 | 
							validator stakingtypes.Validator, subtractAccount bool,
 | 
				
			||||||
 | 
						) (newShares sdk.Dec, err error)
 | 
				
			||||||
 | 
						Unbond(
 | 
				
			||||||
 | 
							ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec,
 | 
				
			||||||
 | 
						) (amount sdk.Int, err error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								x/liquid/types/key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								x/liquid/types/key.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// ModuleName The name that will be used throughout the module
 | 
				
			||||||
 | 
						ModuleName = "liquid"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// RouterKey Top level router key
 | 
				
			||||||
 | 
						RouterKey = ModuleName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ModuleAccountName is the module account's name
 | 
				
			||||||
 | 
						ModuleAccountName = ModuleName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DefaultDerivativeDenom = "bkava"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DenomSeparator = "-"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetLiquidStakingTokenDenom(bondDenom string, valAddr sdk.ValAddress) string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s%s%s", bondDenom, DenomSeparator, valAddr.String())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										120
									
								
								x/liquid/types/msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								x/liquid/types/msg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// TypeMsgMintDerivative represents the type string for MsgMintDerivative
 | 
				
			||||||
 | 
						TypeMsgMintDerivative = "mint_derivative"
 | 
				
			||||||
 | 
						// TypeMsgBurnDerivative represents the type string for MsgBurnDerivative
 | 
				
			||||||
 | 
						TypeMsgBurnDerivative = "burn_derivative"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ensure Msg interface compliance at compile time
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						_ sdk.Msg            = &MsgMintDerivative{}
 | 
				
			||||||
 | 
						_ legacytx.LegacyMsg = &MsgMintDerivative{}
 | 
				
			||||||
 | 
						_ sdk.Msg            = &MsgBurnDerivative{}
 | 
				
			||||||
 | 
						_ legacytx.LegacyMsg = &MsgBurnDerivative{}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewMsgMintDerivative returns a new MsgMintDerivative
 | 
				
			||||||
 | 
					func NewMsgMintDerivative(sender sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) MsgMintDerivative {
 | 
				
			||||||
 | 
						return MsgMintDerivative{
 | 
				
			||||||
 | 
							Sender:    sender.String(),
 | 
				
			||||||
 | 
							Validator: validator.String(),
 | 
				
			||||||
 | 
							Amount:    amount,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Route return the message type used for routing the message.
 | 
				
			||||||
 | 
					func (msg MsgMintDerivative) Route() string { return RouterKey }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Type returns a human-readable string for the message, intended for utilization within tags.
 | 
				
			||||||
 | 
					func (msg MsgMintDerivative) Type() string { return TypeMsgMintDerivative }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ValidateBasic does a simple validation check that doesn't require access to any other information.
 | 
				
			||||||
 | 
					func (msg MsgMintDerivative) ValidateBasic() error {
 | 
				
			||||||
 | 
						_, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = sdk.ValAddressFromBech32(msg.Validator)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
 | 
				
			||||||
 | 
							return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetSignBytes gets the canonical byte representation of the Msg.
 | 
				
			||||||
 | 
					func (msg MsgMintDerivative) GetSignBytes() []byte {
 | 
				
			||||||
 | 
						bz := ModuleCdc.MustMarshalJSON(&msg)
 | 
				
			||||||
 | 
						return sdk.MustSortJSON(bz)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetSigners returns the addresses of signers that must sign.
 | 
				
			||||||
 | 
					func (msg MsgMintDerivative) GetSigners() []sdk.AccAddress {
 | 
				
			||||||
 | 
						sender, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return []sdk.AccAddress{sender}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewMsgBurnDerivative returns a new MsgBurnDerivative
 | 
				
			||||||
 | 
					func NewMsgBurnDerivative(sender sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) MsgBurnDerivative {
 | 
				
			||||||
 | 
						return MsgBurnDerivative{
 | 
				
			||||||
 | 
							Sender:    sender.String(),
 | 
				
			||||||
 | 
							Validator: validator.String(),
 | 
				
			||||||
 | 
							Amount:    amount,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Route return the message type used for routing the message.
 | 
				
			||||||
 | 
					func (msg MsgBurnDerivative) Route() string { return RouterKey }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Type returns a human-readable string for the message, intended for utilization within tags.
 | 
				
			||||||
 | 
					func (msg MsgBurnDerivative) Type() string { return TypeMsgBurnDerivative }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ValidateBasic does a simple validation check that doesn't require access to any other information.
 | 
				
			||||||
 | 
					func (msg MsgBurnDerivative) ValidateBasic() error {
 | 
				
			||||||
 | 
						_, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = sdk.ValAddressFromBech32(msg.Validator)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
 | 
				
			||||||
 | 
							return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetSignBytes gets the canonical byte representation of the Msg.
 | 
				
			||||||
 | 
					func (msg MsgBurnDerivative) GetSignBytes() []byte {
 | 
				
			||||||
 | 
						bz := ModuleCdc.MustMarshalJSON(&msg)
 | 
				
			||||||
 | 
						return sdk.MustSortJSON(bz)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetSigners returns the addresses of signers that must sign.
 | 
				
			||||||
 | 
					func (msg MsgBurnDerivative) GetSigners() []sdk.AccAddress {
 | 
				
			||||||
 | 
						sender, err := sdk.AccAddressFromBech32(msg.Sender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return []sdk.AccAddress{sender}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										163
									
								
								x/liquid/types/msg_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								x/liquid/types/msg_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,163 @@
 | 
				
			|||||||
 | 
					package types_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						fmt "fmt"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
						sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/liquid/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMsgMintDerivative_Signing(t *testing.T) {
 | 
				
			||||||
 | 
						address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
 | 
				
			||||||
 | 
						validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg := types.NewMsgMintDerivative(
 | 
				
			||||||
 | 
							address,
 | 
				
			||||||
 | 
							validatorAddress,
 | 
				
			||||||
 | 
							sdk.NewCoin("ukava", sdk.NewInt(1e9)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// checking for the "type" field ensures the msg is registered on the amino codec
 | 
				
			||||||
 | 
						signBytes := []byte(
 | 
				
			||||||
 | 
							`{"type":"liquid/MsgMintDerivative","value":{"amount":{"amount":"1000000000","denom":"ukava"},"sender":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
 | 
				
			||||||
 | 
						assert.Equal(t, signBytes, msg.GetSignBytes())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMsgBurnDerivative_Signing(t *testing.T) {
 | 
				
			||||||
 | 
						address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
 | 
				
			||||||
 | 
						validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg := types.NewMsgBurnDerivative(
 | 
				
			||||||
 | 
							address,
 | 
				
			||||||
 | 
							validatorAddress,
 | 
				
			||||||
 | 
							sdk.NewCoin("bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42", sdk.NewInt(1e9)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// checking for the "type" field ensures the msg is registered on the amino codec
 | 
				
			||||||
 | 
						signBytes := []byte(
 | 
				
			||||||
 | 
							`{"type":"liquid/MsgBurnDerivative","value":{"amount":{"amount":"1000000000","denom":"bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"},"sender":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
 | 
				
			||||||
 | 
						assert.Equal(t, signBytes, msg.GetSignBytes())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMsg_Validate(t *testing.T) {
 | 
				
			||||||
 | 
						validAddress := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
 | 
				
			||||||
 | 
						validValidatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
 | 
				
			||||||
 | 
						validCoin := sdk.NewInt64Coin("ukava", 1e9)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type msgArgs struct {
 | 
				
			||||||
 | 
							sender    string
 | 
				
			||||||
 | 
							validator string
 | 
				
			||||||
 | 
							amount    sdk.Coin
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name        string
 | 
				
			||||||
 | 
							msgArgs     msgArgs
 | 
				
			||||||
 | 
							expectedErr error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "normal is valid",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    validAddress.String(),
 | 
				
			||||||
 | 
									validator: validValidatorAddress.String(),
 | 
				
			||||||
 | 
									amount:    validCoin,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid sender",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    "invalid",
 | 
				
			||||||
 | 
									validator: validValidatorAddress.String(),
 | 
				
			||||||
 | 
									amount:    validCoin,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidAddress,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid short sender",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    "kava1uexte6", // encoded zero length address
 | 
				
			||||||
 | 
									validator: validValidatorAddress.String(),
 | 
				
			||||||
 | 
									amount:    validCoin,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidAddress,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid validator",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    validAddress.String(),
 | 
				
			||||||
 | 
									validator: "invalid",
 | 
				
			||||||
 | 
									amount:    validCoin,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidAddress,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid nil coin",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    validAddress.String(),
 | 
				
			||||||
 | 
									validator: validValidatorAddress.String(),
 | 
				
			||||||
 | 
									amount:    sdk.Coin{},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidCoins,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid zero coin",
 | 
				
			||||||
 | 
								msgArgs: msgArgs{
 | 
				
			||||||
 | 
									sender:    validAddress.String(),
 | 
				
			||||||
 | 
									validator: validValidatorAddress.String(),
 | 
				
			||||||
 | 
									amount:    sdk.NewInt64Coin("ukava", 0),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedErr: sdkerrors.ErrInvalidCoins,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range tests {
 | 
				
			||||||
 | 
							msgs := []sdk.Msg{
 | 
				
			||||||
 | 
								&types.MsgMintDerivative{
 | 
				
			||||||
 | 
									Sender:    tc.msgArgs.sender,
 | 
				
			||||||
 | 
									Validator: tc.msgArgs.validator,
 | 
				
			||||||
 | 
									Amount:    tc.msgArgs.amount,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								&types.MsgBurnDerivative{
 | 
				
			||||||
 | 
									Sender:    tc.msgArgs.sender,
 | 
				
			||||||
 | 
									Validator: tc.msgArgs.validator,
 | 
				
			||||||
 | 
									Amount:    tc.msgArgs.amount,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, msg := range msgs {
 | 
				
			||||||
 | 
								t.Run(fmt.Sprintf("%s/%T", tc.name, msg), func(t *testing.T) {
 | 
				
			||||||
 | 
									err := msg.ValidateBasic()
 | 
				
			||||||
 | 
									if tc.expectedErr == nil {
 | 
				
			||||||
 | 
										require.NoError(t, err)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										require.ErrorIs(t, err, tc.expectedErr, "expected error '%s' not found in actual '%s'", tc.expectedErr, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mustAccAddressFromBech32(address string) sdk.AccAddress {
 | 
				
			||||||
 | 
						addr, err := sdk.AccAddressFromBech32(address)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return addr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mustValAddressFromBech32(address string) sdk.ValAddress {
 | 
				
			||||||
 | 
						addr, err := sdk.ValAddressFromBech32(address)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return addr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1188
									
								
								x/liquid/types/tx.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1188
									
								
								x/liquid/types/tx.pb.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user