Unverified Commit b0095882 authored by Dev Ojha's avatar Dev Ojha Committed by GitHub
Browse files

comment out superfluid redelegation logic (#870)

* comment out superfluid redelegation logic

* Fix comment formatting

* Comment out redelegation in simulation

* Comment out handler

* Comment out the test in staking
parent 0f8e4ac7
Showing with 380 additions and 349 deletions
+380 -349
......@@ -30,7 +30,6 @@ message ClaimRecord {
// true if action is completed
// index of bool in array refers to action enum #
repeated bool action_completed = 3 [
(gogoproto.moretags) = "yaml:\"action_completed\""
];
repeated bool action_completed = 3
[ (gogoproto.moretags) = "yaml:\"action_completed\"" ];
}
\ No newline at end of file
......@@ -39,8 +39,10 @@ enum LockQueryType {
}
message QueryCondition {
LockQueryType lock_query_type = 1; // type of lock query, ByLockDuration | ByLockTime
string denom = 2; // What token denomination are we looking for lockups of
// type of lock query, ByLockDuration | ByLockTime
LockQueryType lock_query_type = 1;
// What token denomination are we looking for lockups of
string denom = 2;
// valid when query condition is ByDuration
google.protobuf.Duration duration = 3 [
(gogoproto.stdduration) = true,
......@@ -57,9 +59,12 @@ message QueryCondition {
// SyntheticLock is a single unit of synthetic lockup
message SyntheticLock {
uint64 underlying_lock_id = 1; // underlying native lockup id for this synthetic lockup
string suffix = 2; // synthetic suffix comes after native denom - used for querying
// used for unbonding synthetic lockups, for active synthetic lockups, this value is set to uninitialized value
// underlying native lockup id for this synthetic lockup
uint64 underlying_lock_id = 1;
// synthetic suffix comes after native denom - used for querying
string suffix = 2;
// used for unbonding synthetic lockups, for active synthetic lockups, this
// value is set to uninitialized value
google.protobuf.Timestamp end_time = 3 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
......
......@@ -9,10 +9,11 @@ option go_package = "github.com/osmosis-labs/osmosis/x/superfluid/types";
// GenesisState defines the module's genesis state.
message GenesisState {
Params params = 1 [ (gogoproto.nullable) = false ];
repeated SuperfluidAsset superfluid_assets = 2 [ (gogoproto.nullable) = false ];
repeated EpochOsmoEquivalentTWAP twap_price_records = 3
[ (gogoproto.nullable) = false ];
repeated SuperfluidIntermediaryAccount intermediary_accounts = 4
[ (gogoproto.nullable) = false ];
Params params = 1 [ (gogoproto.nullable) = false ];
repeated SuperfluidAsset superfluid_assets = 2
[ (gogoproto.nullable) = false ];
repeated EpochOsmoEquivalentTWAP twap_price_records = 3
[ (gogoproto.nullable) = false ];
repeated SuperfluidIntermediaryAccount intermediary_accounts = 4
[ (gogoproto.nullable) = false ];
}
......@@ -6,7 +6,8 @@ import "osmosis/superfluid/superfluid.proto";
option go_package = "github.com/osmosis-labs/osmosis/x/superfluid/types";
// SetSuperfluidAssetsProposal is a gov Content type to update the superfluid assets
// SetSuperfluidAssetsProposal is a gov Content type to update the superfluid
// assets
message SetSuperfluidAssetsProposal {
option (gogoproto.equal) = true;
option (gogoproto.goproto_getters) = false;
......@@ -17,7 +18,8 @@ message SetSuperfluidAssetsProposal {
repeated SuperfluidAsset assets = 3 [ (gogoproto.nullable) = false ];
}
// RemoveSuperfluidAssetsProposal is a gov Content type to remove the superfluid assets by denom
// RemoveSuperfluidAssetsProposal is a gov Content type to remove the superfluid
// assets by denom
message RemoveSuperfluidAssetsProposal {
option (gogoproto.equal) = true;
option (gogoproto.goproto_getters) = false;
......
......@@ -11,12 +11,13 @@ message Params {
// refresh epoch identifier for staked amount
string refresh_epoch_identifier = 1
[ (gogoproto.moretags) = "yaml:\"refresh_epoch_identifier\"" ];
// the risk_factor is to be cut on OSMO equivalent value of lp tokens for superfluid staking, default: 5%
// the risk_factor is to be cut on OSMO equivalent value of lp tokens for
// superfluid staking, default: 5%
string minimum_risk_factor = 2 [
(gogoproto.moretags) = "yaml:\"minimum_risk_factor\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
(gogoproto.moretags) = "yaml:\"minimum_risk_factor\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// unbonding duration of superfluid delegation
google.protobuf.Duration unbonding_duration = 3 [
(gogoproto.stdduration) = true,
......
......@@ -15,7 +15,8 @@ option go_package = "github.com/osmosis-labs/osmosis/x/superfluid/types";
service Query {
// Returns superfluid asset type
rpc AssetType(AssetTypeRequest) returns (AssetTypeResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/asset_type/{denom}";
option (google.api.http).get =
"/osmosis/superfluid/v1beta1/asset_type/{denom}";
}
// Returns all superfluid asset types
rpc AllAssets(AllAssetsRequest) returns (AllAssetsResponse) {
......@@ -23,36 +24,33 @@ service Query {
}
// Returns superfluid asset TWAP
rpc AssetTwap(AssetTwapRequest) returns (AssetTwapResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/asset_twap/{denom}";
option (google.api.http).get =
"/osmosis/superfluid/v1beta1/asset_twap/{denom}";
}
// Returns all superfluid intermediary account
rpc AllIntermediaryAccounts(AllIntermediaryAccountsRequest) returns (AllIntermediaryAccountsResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/all_intermediary_accounts";
rpc AllIntermediaryAccounts(AllIntermediaryAccountsRequest)
returns (AllIntermediaryAccountsResponse) {
option (google.api.http).get =
"/osmosis/superfluid/v1beta1/all_intermediary_accounts";
}
// Returns intermediary account connected to a superfluid staked lock by id
rpc ConnectedIntermediaryAccount(ConnectedIntermediaryAccountRequest) returns (ConnectedIntermediaryAccountResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/connected_intermediary_account/{lock_id}";
rpc ConnectedIntermediaryAccount(ConnectedIntermediaryAccountRequest)
returns (ConnectedIntermediaryAccountResponse) {
option (google.api.http).get =
"/osmosis/superfluid/v1beta1/connected_intermediary_account/{lock_id}";
}
}
message AssetTypeRequest {
string denom = 1;
};
message AssetTypeResponse {
SuperfluidAssetType asset_type = 1;
};
message AssetTypeRequest { string denom = 1; };
message AssetTypeResponse { SuperfluidAssetType asset_type = 1; };
message AllAssetsRequest {};
message AllAssetsResponse {
repeated SuperfluidAsset assets = 1 [(gogoproto.nullable) = false];
repeated SuperfluidAsset assets = 1 [ (gogoproto.nullable) = false ];
};
message AssetTwapRequest {
string denom = 1;
};
message AssetTwapResponse {
EpochOsmoEquivalentTWAP twap = 1;
};
message AssetTwapRequest { string denom = 1; };
message AssetTwapResponse { EpochOsmoEquivalentTWAP twap = 1; };
message SuperfluidIntermediaryAccountInfo {
string denom = 1;
......@@ -64,13 +62,12 @@ message AllIntermediaryAccountsRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
};
message AllIntermediaryAccountsResponse {
repeated SuperfluidIntermediaryAccountInfo accounts = 1 [ (gogoproto.nullable) = false ];
repeated SuperfluidIntermediaryAccountInfo accounts = 1
[ (gogoproto.nullable) = false ];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
};
message ConnectedIntermediaryAccountRequest {
uint64 lock_id = 1;
}
message ConnectedIntermediaryAccountRequest { uint64 lock_id = 1; }
message ConnectedIntermediaryAccountResponse {
SuperfluidIntermediaryAccountInfo account = 1;
}
......@@ -25,13 +25,20 @@ message SuperfluidAsset {
SuperfluidAssetType asset_type = 2;
}
// SuperfluidIntermediaryAccount takes the role of intermediary between LP token and OSMO tokens for superfluid staking
// SuperfluidIntermediaryAccount takes the role of intermediary between LP token
// and OSMO tokens for superfluid staking
message SuperfluidIntermediaryAccount {
string denom = 1;
string val_addr = 2;
uint64 gauge_id = 3; // perpetual gauge for rewards distribution
}
// The Osmo-Equivalent-TWAP for epoch N refers to the osmo worth we treat an LP
// share as having, for all of epoch N. This is intended to be set as the
// Time-weighted-average-osmo-backing for the entire duration of epoch N-1.
// (Thereby locking whats in use for epoch N as based on the prior epochs
// rewards) However for now, this is not the TWAP but instead the spot price at
// the boundary.
message EpochOsmoEquivalentTWAP {
int64 epoch_number = 1;
string denom = 2; // superfluid asset denom, can be LP token or native token
......
......@@ -11,11 +11,14 @@ option go_package = "github.com/osmosis-labs/osmosis/x/superfluid/types";
// Msg defines the Msg service.
service Msg {
// Execute superfluid delegation for a lockup
rpc SuperfluidDelegate(MsgSuperfluidDelegate) returns (MsgSuperfluidDelegateResponse);
rpc SuperfluidDelegate(MsgSuperfluidDelegate)
returns (MsgSuperfluidDelegateResponse);
// Execute superfluid undelegation for a lockup
rpc SuperfluidUndelegate(MsgSuperfluidUndelegate) returns (MsgSuperfluidUndelegateResponse);
rpc SuperfluidUndelegate(MsgSuperfluidUndelegate)
returns (MsgSuperfluidUndelegateResponse);
// Execute superfluid redelegation for a lockup
rpc SuperfluidRedelegate(MsgSuperfluidRedelegate) returns (MsgSuperfluidRedelegateResponse);
// rpc SuperfluidRedelegate(MsgSuperfluidRedelegate) returns
// (MsgSuperfluidRedelegateResponse);
}
message MsgSuperfluidDelegate {
......@@ -23,17 +26,17 @@ message MsgSuperfluidDelegate {
uint64 lock_id = 2;
string val_addr = 3;
}
message MsgSuperfluidDelegateResponse { }
message MsgSuperfluidDelegateResponse {}
message MsgSuperfluidUndelegate {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
uint64 lock_id = 2;
}
message MsgSuperfluidUndelegateResponse { }
message MsgSuperfluidUndelegateResponse {}
message MsgSuperfluidRedelegate {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
uint64 lock_id = 2;
string new_val_addr = 3;
}
message MsgSuperfluidRedelegateResponse {}
// message MsgSuperfluidRedelegate {
// string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
// uint64 lock_id = 2;
// string new_val_addr = 3;
// }
// message MsgSuperfluidRedelegateResponse {}
......@@ -135,8 +135,10 @@ func (m *PeriodLock) GetCoins() github_com_cosmos_cosmos_sdk_types.Coins {
}
type QueryCondition struct {
// type of lock query, ByLockDuration | ByLockTime
LockQueryType LockQueryType `protobuf:"varint,1,opt,name=lock_query_type,json=lockQueryType,proto3,enum=osmosis.lockup.LockQueryType" json:"lock_query_type,omitempty"`
Denom string `protobuf:"bytes,2,opt,name=denom,proto3" json:"denom,omitempty"`
// What token denomination are we looking for lockups of
Denom string `protobuf:"bytes,2,opt,name=denom,proto3" json:"denom,omitempty"`
// valid when query condition is ByDuration
Duration time.Duration `protobuf:"bytes,3,opt,name=duration,proto3,stdduration" json:"duration" yaml:"duration"`
// valid when query condition is ByTime
......@@ -206,13 +208,17 @@ func (m *QueryCondition) GetTimestamp() time.Time {
// SyntheticLock is a single unit of synthetic lockup
type SyntheticLock struct {
// underlying native lockup id for this synthetic lockup
UnderlyingLockId uint64 `protobuf:"varint,1,opt,name=underlying_lock_id,json=underlyingLockId,proto3" json:"underlying_lock_id,omitempty"`
Suffix string `protobuf:"bytes,2,opt,name=suffix,proto3" json:"suffix,omitempty"`
// used for unbonding synthetic lockups, for active synthetic lockups, this value is set to uninitialized value
EndTime time.Time `protobuf:"bytes,3,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time" yaml:"end_time"`
Duration time.Duration `protobuf:"bytes,4,opt,name=duration,proto3,stdduration" json:"duration,omitempty" yaml:"duration"`
Coins github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,5,rep,name=coins,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"coins"`
Owner string `protobuf:"bytes,6,opt,name=owner,proto3" json:"owner,omitempty" yaml:"owner"`
// synthetic suffix comes after native denom - used for querying
Suffix string `protobuf:"bytes,2,opt,name=suffix,proto3" json:"suffix,omitempty"`
// used for unbonding synthetic lockups, for active synthetic lockups, this
// value is set to uninitialized value
EndTime time.Time `protobuf:"bytes,3,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time" yaml:"end_time"`
Duration time.Duration `protobuf:"bytes,4,opt,name=duration,proto3,stdduration" json:"duration,omitempty" yaml:"duration"`
// The coins from the underlying lock ID that are synthetically locked
Coins github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,5,rep,name=coins,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"coins"`
Owner string `protobuf:"bytes,6,opt,name=owner,proto3" json:"owner,omitempty" yaml:"owner"`
}
func (m *SyntheticLock) Reset() { *m = SyntheticLock{} }
......
......@@ -28,7 +28,7 @@ func GetTxCmd() *cobra.Command {
cmd.AddCommand(
NewSuperfluidDelegateCmd(),
NewSuperfluidUndelegateCmd(),
NewSuperfluidRedelegateCmd(),
// NewSuperfluidRedelegateCmd(),
)
return cmd
......@@ -105,42 +105,42 @@ func NewSuperfluidUndelegateCmd() *cobra.Command {
}
// NewSuperfluidRedelegateCmd broadcast MsgSuperfluidRedelegate
func NewSuperfluidRedelegateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "redelegate [lock_id] [val_addr] [flags]",
Short: "superfluid redelegate a lock to a new validator",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
txf := tx.NewFactoryCLI(clientCtx, cmd.Flags()).WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever)
lockId, err := strconv.Atoi(args[0])
if err != nil {
return err
}
valAddr, err := sdk.ValAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgSuperfluidRedelegate(
clientCtx.GetFromAddress(),
uint64(lockId),
valAddr,
)
return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
// func NewSuperfluidRedelegateCmd() *cobra.Command {
// cmd := &cobra.Command{
// Use: "redelegate [lock_id] [val_addr] [flags]",
// Short: "superfluid redelegate a lock to a new validator",
// Args: cobra.ExactArgs(2),
// RunE: func(cmd *cobra.Command, args []string) error {
// clientCtx, err := client.GetClientTxContext(cmd)
// if err != nil {
// return err
// }
// txf := tx.NewFactoryCLI(clientCtx, cmd.Flags()).WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever)
// lockId, err := strconv.Atoi(args[0])
// if err != nil {
// return err
// }
// valAddr, err := sdk.ValAddressFromBech32(args[1])
// if err != nil {
// return err
// }
// msg := types.NewMsgSuperfluidRedelegate(
// clientCtx.GetFromAddress(),
// uint64(lockId),
// valAddr,
// )
// return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg)
// },
// }
// flags.AddTxFlagsToCmd(cmd)
// return cmd
// }
// NewCmdSubmitSetSuperfluidAssetsProposal implements a command handler for submitting a superfluid asset set proposal transaction.
func NewCmdSubmitSetSuperfluidAssetsProposal() *cobra.Command {
......
......@@ -22,9 +22,9 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgSuperfluidUndelegate:
res, err := msgServer.SuperfluidUndelegate(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgSuperfluidRedelegate:
res, err := msgServer.SuperfluidRedelegate(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
// case *types.MsgSuperfluidRedelegate:
// res, err := msgServer.SuperfluidRedelegate(sdk.WrapSDKContext(ctx), msg)
// return sdk.WrapServiceResult(ctx, res, err)
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
......
......@@ -34,9 +34,9 @@ func (server msgServer) SuperfluidUndelegate(goCtx context.Context, msg *types.M
return &types.MsgSuperfluidUndelegateResponse{}, err
}
func (server msgServer) SuperfluidRedelegate(goCtx context.Context, msg *types.MsgSuperfluidRedelegate) (*types.MsgSuperfluidRedelegateResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// func (server msgServer) SuperfluidRedelegate(goCtx context.Context, msg *types.MsgSuperfluidRedelegate) (*types.MsgSuperfluidRedelegateResponse, error) {
// ctx := sdk.UnwrapSDKContext(goCtx)
err := server.keeper.SuperfluidRedelegate(ctx, msg.Sender, msg.LockId, msg.NewValAddr)
return &types.MsgSuperfluidRedelegateResponse{}, err
}
// err := server.keeper.SuperfluidRedelegate(ctx, msg.Sender, msg.LockId, msg.NewValAddr)
// return &types.MsgSuperfluidRedelegateResponse{}, err
// }
......@@ -332,21 +332,21 @@ func (k Keeper) SuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint
return valAddr, nil
}
func (k Keeper) SuperfluidRedelegate(ctx sdk.Context, sender string, lockID uint64, newValAddr string) error {
// Note: we prevent circular redelegations since when unbonding lockup is available from a specific validator,
// not able to redelegate or undelegate again, especially the case for automatic undelegation when native lockup unlock
// func (k Keeper) SuperfluidRedelegate(ctx sdk.Context, sender string, lockID uint64, newValAddr string) error {
// // Note: we prevent circular redelegations since when unbonding lockup is available from a specific validator,
// // not able to redelegate or undelegate again, especially the case for automatic undelegation when native lockup unlock
valAddr, err := k.SuperfluidUndelegate(ctx, sender, lockID)
if err != nil {
return err
}
// valAddr, err := k.SuperfluidUndelegate(ctx, sender, lockID)
// if err != nil {
// return err
// }
if valAddr.String() == newValAddr {
return types.ErrSameValidatorRedelegation
}
// if valAddr.String() == newValAddr {
// return types.ErrSameValidatorRedelegation
// }
return k.SuperfluidDelegate(ctx, sender, lockID, newValAddr)
}
// return k.SuperfluidDelegate(ctx, sender, lockID, newValAddr)
// }
// TODO: Need to (eventually) override the existing staking messages and queries, for undelegating, delegating, rewards, and redelegating, to all be going through all superfluid module.
// Want integrators to be able to use the same staking queries and messages
......
......@@ -426,155 +426,155 @@ func (suite *KeeperTestSuite) TestSuperfluidUndelegate() {
}
}
func (suite *KeeperTestSuite) TestSuperfluidRedelegate() {
testCases := []struct {
name string
validatorStats []stakingtypes.BondStatus
superDelegations []superfluidDelegation
superRedelegations []superfluidRedelegation
expSuperRedelegationErr []bool
}{
{
"with single validator and single superfluid delegation with single redelegation",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}},
[]superfluidRedelegation{{1, 0, 1}}, // lock1 => val0 -> val1
[]bool{false},
},
{
"with multiple superfluid delegations with single redelegation",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
[]superfluidRedelegation{{1, 0, 1}}, // lock1 => val0 -> val1
[]bool{false},
},
{
"with multiple superfluid delegations with multiple redelegations",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
[]superfluidRedelegation{{1, 0, 1}, {2, 0, 1}}, // lock1 => val0 -> val1, lock2 => val0 -> val1
[]bool{false, false},
},
{
"try redelegating back from new validator to original validator",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
[]superfluidRedelegation{{1, 0, 1}, {1, 1, 0}}, // lock1 => val0 -> val1, lock1 => val1 -> val0
[]bool{false, true},
},
{
"not available lock id redelegation",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}},
[]superfluidRedelegation{{2, 0, 1}}, // lock1 => val0 -> val1
[]bool{true},
},
{
"redelegation for same validator",
[]stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
[]superfluidDelegation{{0, "gamm/pool/1"}},
[]superfluidRedelegation{{1, 0, 0}}, // lock1 => val0 -> val0
[]bool{true},
},
}
for _, tc := range testCases {
tc := tc
suite.Run(tc.name, func() {
suite.SetupTest()
poolId := suite.createGammPool([]string{appparams.BaseCoinUnit, "foo"})
suite.Require().Equal(poolId, uint64(1))
// setup validators
valAddrs := suite.SetupValidators(tc.validatorStats)
// setup superfluid delegations
intermediaryAccs, _ := suite.SetupSuperfluidDelegations(valAddrs, tc.superDelegations)
suite.checkIntermediaryAccountDelegations(intermediaryAccs)
// execute redelegation and check changes on store
for index, srd := range tc.superRedelegations {
lock, err := suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
if err != nil {
lock = &lockuptypes.PeriodLock{}
}
// superfluid redelegate
err = suite.app.SuperfluidKeeper.SuperfluidRedelegate(suite.ctx, lock.Owner, srd.lockId, valAddrs[srd.newValIndex].String())
if tc.expSuperRedelegationErr[index] {
suite.Require().Error(err)
continue
}
suite.Require().NoError(err)
// check previous validator bonding synthetic lockup deletion
_, err = suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.StakingSuffix(valAddrs[srd.oldValIndex].String()))
suite.Require().Error(err)
// check unbonding synthetic lockup creation
params := suite.app.SuperfluidKeeper.GetParams(suite.ctx)
synthLock, err := suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.UnstakingSuffix(valAddrs[srd.oldValIndex].String()))
suite.Require().NoError(err)
suite.Require().Equal(synthLock.UnderlyingLockId, srd.lockId)
suite.Require().Equal(synthLock.Suffix, keeper.UnstakingSuffix(valAddrs[srd.oldValIndex].String()))
suite.Require().Equal(synthLock.EndTime, suite.ctx.BlockTime().Add(params.UnbondingDuration))
// check synthetic lockup creation
synthLock2, err := suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.StakingSuffix(valAddrs[srd.newValIndex].String()))
suite.Require().NoError(err)
suite.Require().Equal(synthLock2.UnderlyingLockId, srd.lockId)
suite.Require().Equal(synthLock2.Suffix, keeper.StakingSuffix(valAddrs[srd.newValIndex].String()))
suite.Require().Equal(synthLock2.EndTime, time.Time{})
// check intermediary account creation
lock, err = suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
suite.Require().NoError(err)
expAcc := types.NewSuperfluidIntermediaryAccount(lock.Coins[0].Denom, valAddrs[srd.newValIndex].String(), 1)
gotAcc := suite.app.SuperfluidKeeper.GetIntermediaryAccount(suite.ctx, expAcc.GetAccAddress())
suite.Require().Equal(gotAcc.Denom, expAcc.Denom)
suite.Require().Equal(gotAcc.ValAddr, expAcc.ValAddr)
// check gauge creation
gauge, err := suite.app.IncentivesKeeper.GetGaugeByID(suite.ctx, gotAcc.GaugeId)
suite.Require().NoError(err)
suite.Require().Equal(gauge.Id, gotAcc.GaugeId)
suite.Require().Equal(gauge.IsPerpetual, true)
suite.Require().Equal(gauge.DistributeTo, lockuptypes.QueryCondition{
LockQueryType: lockuptypes.ByDuration,
Denom: expAcc.Denom + keeper.StakingSuffix(valAddrs[srd.newValIndex].String()),
Duration: params.UnbondingDuration,
})
suite.Require().Equal(gauge.Coins, sdk.Coins(nil))
suite.Require().Equal(gauge.StartTime, suite.ctx.BlockTime())
suite.Require().Equal(gauge.NumEpochsPaidOver, uint64(1))
suite.Require().Equal(gauge.FilledEpochs, uint64(0))
suite.Require().Equal(gauge.DistributedCoins, sdk.Coins(nil))
// Check lockID connection with intermediary account
intAcc := suite.app.SuperfluidKeeper.GetLockIdIntermediaryAccountConnection(suite.ctx, srd.lockId)
suite.Require().Equal(intAcc.String(), expAcc.GetAccAddress().String())
// check delegation from intermediary account to validator
_, found := suite.app.StakingKeeper.GetDelegation(suite.ctx, expAcc.GetAccAddress(), valAddrs[srd.newValIndex])
suite.Require().True(found)
}
// try redelegating twice
for index, srd := range tc.superRedelegations {
if tc.expSuperRedelegationErr[index] {
continue
}
cacheCtx, _ := suite.ctx.CacheContext()
lock, err := suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
suite.Require().NoError(err)
err = suite.app.SuperfluidKeeper.SuperfluidRedelegate(cacheCtx, lock.Owner, srd.lockId, valAddrs[srd.newValIndex].String())
suite.Require().Error(err)
}
})
}
}
// func (suite *KeeperTestSuite) TestSuperfluidRedelegate() {
// testCases := []struct {
// name string
// validatorStats []stakingtypes.BondStatus
// superDelegations []superfluidDelegation
// superRedelegations []superfluidRedelegation
// expSuperRedelegationErr []bool
// }{
// {
// "with single validator and single superfluid delegation with single redelegation",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}},
// []superfluidRedelegation{{1, 0, 1}}, // lock1 => val0 -> val1
// []bool{false},
// },
// {
// "with multiple superfluid delegations with single redelegation",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
// []superfluidRedelegation{{1, 0, 1}}, // lock1 => val0 -> val1
// []bool{false},
// },
// {
// "with multiple superfluid delegations with multiple redelegations",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
// []superfluidRedelegation{{1, 0, 1}, {2, 0, 1}}, // lock1 => val0 -> val1, lock2 => val0 -> val1
// []bool{false, false},
// },
// {
// "try redelegating back from new validator to original validator",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}, {0, "gamm/pool/1"}},
// []superfluidRedelegation{{1, 0, 1}, {1, 1, 0}}, // lock1 => val0 -> val1, lock1 => val1 -> val0
// []bool{false, true},
// },
// {
// "not available lock id redelegation",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}},
// []superfluidRedelegation{{2, 0, 1}}, // lock1 => val0 -> val1
// []bool{true},
// },
// {
// "redelegation for same validator",
// []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded},
// []superfluidDelegation{{0, "gamm/pool/1"}},
// []superfluidRedelegation{{1, 0, 0}}, // lock1 => val0 -> val0
// []bool{true},
// },
// }
// for _, tc := range testCases {
// tc := tc
// suite.Run(tc.name, func() {
// suite.SetupTest()
// poolId := suite.createGammPool([]string{appparams.BaseCoinUnit, "foo"})
// suite.Require().Equal(poolId, uint64(1))
// // setup validators
// valAddrs := suite.SetupValidators(tc.validatorStats)
// // setup superfluid delegations
// intermediaryAccs, _ := suite.SetupSuperfluidDelegations(valAddrs, tc.superDelegations)
// suite.checkIntermediaryAccountDelegations(intermediaryAccs)
// // execute redelegation and check changes on store
// for index, srd := range tc.superRedelegations {
// lock, err := suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
// if err != nil {
// lock = &lockuptypes.PeriodLock{}
// }
// // superfluid redelegate
// err = suite.app.SuperfluidKeeper.SuperfluidRedelegate(suite.ctx, lock.Owner, srd.lockId, valAddrs[srd.newValIndex].String())
// if tc.expSuperRedelegationErr[index] {
// suite.Require().Error(err)
// continue
// }
// suite.Require().NoError(err)
// // check previous validator bonding synthetic lockup deletion
// _, err = suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.StakingSuffix(valAddrs[srd.oldValIndex].String()))
// suite.Require().Error(err)
// // check unbonding synthetic lockup creation
// params := suite.app.SuperfluidKeeper.GetParams(suite.ctx)
// synthLock, err := suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.UnstakingSuffix(valAddrs[srd.oldValIndex].String()))
// suite.Require().NoError(err)
// suite.Require().Equal(synthLock.UnderlyingLockId, srd.lockId)
// suite.Require().Equal(synthLock.Suffix, keeper.UnstakingSuffix(valAddrs[srd.oldValIndex].String()))
// suite.Require().Equal(synthLock.EndTime, suite.ctx.BlockTime().Add(params.UnbondingDuration))
// // check synthetic lockup creation
// synthLock2, err := suite.app.LockupKeeper.GetSyntheticLockup(suite.ctx, srd.lockId, keeper.StakingSuffix(valAddrs[srd.newValIndex].String()))
// suite.Require().NoError(err)
// suite.Require().Equal(synthLock2.UnderlyingLockId, srd.lockId)
// suite.Require().Equal(synthLock2.Suffix, keeper.StakingSuffix(valAddrs[srd.newValIndex].String()))
// suite.Require().Equal(synthLock2.EndTime, time.Time{})
// // check intermediary account creation
// lock, err = suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
// suite.Require().NoError(err)
// expAcc := types.NewSuperfluidIntermediaryAccount(lock.Coins[0].Denom, valAddrs[srd.newValIndex].String(), 1)
// gotAcc := suite.app.SuperfluidKeeper.GetIntermediaryAccount(suite.ctx, expAcc.GetAccAddress())
// suite.Require().Equal(gotAcc.Denom, expAcc.Denom)
// suite.Require().Equal(gotAcc.ValAddr, expAcc.ValAddr)
// // check gauge creation
// gauge, err := suite.app.IncentivesKeeper.GetGaugeByID(suite.ctx, gotAcc.GaugeId)
// suite.Require().NoError(err)
// suite.Require().Equal(gauge.Id, gotAcc.GaugeId)
// suite.Require().Equal(gauge.IsPerpetual, true)
// suite.Require().Equal(gauge.DistributeTo, lockuptypes.QueryCondition{
// LockQueryType: lockuptypes.ByDuration,
// Denom: expAcc.Denom + keeper.StakingSuffix(valAddrs[srd.newValIndex].String()),
// Duration: params.UnbondingDuration,
// })
// suite.Require().Equal(gauge.Coins, sdk.Coins(nil))
// suite.Require().Equal(gauge.StartTime, suite.ctx.BlockTime())
// suite.Require().Equal(gauge.NumEpochsPaidOver, uint64(1))
// suite.Require().Equal(gauge.FilledEpochs, uint64(0))
// suite.Require().Equal(gauge.DistributedCoins, sdk.Coins(nil))
// // Check lockID connection with intermediary account
// intAcc := suite.app.SuperfluidKeeper.GetLockIdIntermediaryAccountConnection(suite.ctx, srd.lockId)
// suite.Require().Equal(intAcc.String(), expAcc.GetAccAddress().String())
// // check delegation from intermediary account to validator
// _, found := suite.app.StakingKeeper.GetDelegation(suite.ctx, expAcc.GetAccAddress(), valAddrs[srd.newValIndex])
// suite.Require().True(found)
// }
// // try redelegating twice
// for index, srd := range tc.superRedelegations {
// if tc.expSuperRedelegationErr[index] {
// continue
// }
// cacheCtx, _ := suite.ctx.CacheContext()
// lock, err := suite.app.LockupKeeper.GetLockByID(suite.ctx, srd.lockId)
// suite.Require().NoError(err)
// err = suite.app.SuperfluidKeeper.SuperfluidRedelegate(cacheCtx, lock.Owner, srd.lockId, valAddrs[srd.newValIndex].String())
// suite.Require().Error(err)
// }
// })
// }
// }
func (suite *KeeperTestSuite) TestRefreshIntermediaryDelegationAmounts() {
testCases := []struct {
......
......@@ -38,7 +38,7 @@ func WeightedOperations(
var (
weightMsgSuperfluidDelegate int
weightMsgSuperfluidUndelegate int
weightMsgSuperfluidRedelegate int
// weightMsgSuperfluidRedelegate int
)
appParams.GetOrGenerate(cdc, OpWeightMsgSuperfluidDelegate, &weightMsgSuperfluidDelegate, nil,
......@@ -53,11 +53,11 @@ func WeightedOperations(
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgSuperfluidRedelegate, &weightMsgSuperfluidRedelegate, nil,
func(_ *rand.Rand) {
weightMsgSuperfluidRedelegate = DefaultWeightMsgSuperfluidRedelegate
},
)
// appParams.GetOrGenerate(cdc, OpWeightMsgSuperfluidRedelegate, &weightMsgSuperfluidRedelegate, nil,
// func(_ *rand.Rand) {
// weightMsgSuperfluidRedelegate = DefaultWeightMsgSuperfluidRedelegate
// },
// )
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
......@@ -68,10 +68,10 @@ func WeightedOperations(
weightMsgSuperfluidUndelegate,
SimulateMsgSuperfluidUndelegate(ak, bk, lk, k),
),
simulation.NewWeightedOperation(
weightMsgSuperfluidRedelegate,
SimulateMsgSuperfluidRedelegate(ak, bk, sk, lk, k),
),
// simulation.NewWeightedOperation(
// weightMsgSuperfluidRedelegate,
// SimulateMsgSuperfluidRedelegate(ak, bk, sk, lk, k),
// ),
}
}
......@@ -146,41 +146,41 @@ func SimulateMsgSuperfluidUndelegate(ak stakingtypes.AccountKeeper, bk stakingty
}
}
func SimulateMsgSuperfluidRedelegate(ak stakingtypes.AccountKeeper, bk stakingtypes.BankKeeper, sk types.StakingKeeper, lk types.LockupKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
// select random validator
validator := RandomValidator(ctx, r, sk)
if validator == nil {
return simtypes.NoOpMsg(
types.ModuleName, types.TypeMsgSuperfluidRedelegate, "No validator"), nil, nil
}
lock, simAccount := RandomLockAndAccount(ctx, r, lk, accs)
if lock == nil {
return simtypes.NoOpMsg(
types.ModuleName, types.TypeMsgSuperfluidRedelegate, "Account have no period lock"), nil, nil
}
if k.GetLockIdIntermediaryAccountConnection(ctx, lock.ID).Empty() {
return simtypes.NoOpMsg(
types.ModuleName, types.TypeMsgSuperfluidRedelegate, "Lock is not used for superfluid staking"), nil, nil
}
msg := types.MsgSuperfluidRedelegate{
Sender: lock.Owner,
LockId: lock.ID,
NewValAddr: validator.OperatorAddress,
}
txGen := simappparams.MakeTestEncodingConfig().TxConfig
return osmo_simulation.GenAndDeliverTxWithRandFees(
r, app, txGen, &msg, nil, ctx, simAccount, ak, bk, types.ModuleName)
}
}
// func SimulateMsgSuperfluidRedelegate(ak stakingtypes.AccountKeeper, bk stakingtypes.BankKeeper, sk types.StakingKeeper, lk types.LockupKeeper, k keeper.Keeper) simtypes.Operation {
// return func(
// r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
// ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
// simAccount, _ := simtypes.RandomAcc(r, accs)
// // select random validator
// validator := RandomValidator(ctx, r, sk)
// if validator == nil {
// return simtypes.NoOpMsg(
// types.ModuleName, types.TypeMsgSuperfluidRedelegate, "No validator"), nil, nil
// }
// lock, simAccount := RandomLockAndAccount(ctx, r, lk, accs)
// if lock == nil {
// return simtypes.NoOpMsg(
// types.ModuleName, types.TypeMsgSuperfluidRedelegate, "Account have no period lock"), nil, nil
// }
// if k.GetLockIdIntermediaryAccountConnection(ctx, lock.ID).Empty() {
// return simtypes.NoOpMsg(
// types.ModuleName, types.TypeMsgSuperfluidRedelegate, "Lock is not used for superfluid staking"), nil, nil
// }
// msg := types.MsgSuperfluidRedelegate{
// Sender: lock.Owner,
// LockId: lock.ID,
// NewValAddr: validator.OperatorAddress,
// }
// txGen := simappparams.MakeTestEncodingConfig().TxConfig
// return osmo_simulation.GenAndDeliverTxWithRandFees(
// r, app, txGen, &msg, nil, ctx, simAccount, ak, bk, types.ModuleName)
// }
// }
func RandomLockAndAccount(ctx sdk.Context, r *rand.Rand, lk types.LockupKeeper, accs []simtypes.Account) (*lockuptypes.PeriodLock, simtypes.Account) {
simAccount, _ := simtypes.RandomAcc(r, accs)
......
......@@ -16,7 +16,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
(*sdk.Msg)(nil),
&MsgSuperfluidDelegate{},
&MsgSuperfluidUndelegate{},
&MsgSuperfluidRedelegate{},
// &MsgSuperfluidRedelegate{},
)
registry.RegisterImplementations(
......
......@@ -23,7 +23,8 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// SetSuperfluidAssetsProposal is a gov Content type to update the superfluid assets
// SetSuperfluidAssetsProposal is a gov Content type to update the superfluid
// assets
type SetSuperfluidAssetsProposal struct {
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
......@@ -62,7 +63,8 @@ func (m *SetSuperfluidAssetsProposal) XXX_DiscardUnknown() {
var xxx_messageInfo_SetSuperfluidAssetsProposal proto.InternalMessageInfo
// RemoveSuperfluidAssetsProposal is a gov Content type to remove the superfluid assets by denom
// RemoveSuperfluidAssetsProposal is a gov Content type to remove the superfluid
// assets by denom
type RemoveSuperfluidAssetsProposal struct {
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
......
......@@ -75,35 +75,35 @@ func (m MsgSuperfluidUndelegate) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{sender}
}
var _ sdk.Msg = &MsgSuperfluidRedelegate{}
// var _ sdk.Msg = &MsgSuperfluidRedelegate{}
// NewMsgSuperfluidRedelegate creates a message to do superfluid redelegation
func NewMsgSuperfluidRedelegate(sender sdk.AccAddress, lockId uint64, newValAddr sdk.ValAddress) *MsgSuperfluidRedelegate {
return &MsgSuperfluidRedelegate{
Sender: sender.String(),
LockId: lockId,
NewValAddr: newValAddr.String(),
}
}
// // NewMsgSuperfluidRedelegate creates a message to do superfluid redelegation
// func NewMsgSuperfluidRedelegate(sender sdk.AccAddress, lockId uint64, newValAddr sdk.ValAddress) *MsgSuperfluidRedelegate {
// return &MsgSuperfluidRedelegate{
// Sender: sender.String(),
// LockId: lockId,
// NewValAddr: newValAddr.String(),
// }
// }
func (m MsgSuperfluidRedelegate) Route() string { return RouterKey }
func (m MsgSuperfluidRedelegate) Type() string { return TypeMsgSuperfluidRedelegate }
func (m MsgSuperfluidRedelegate) ValidateBasic() error {
if m.Sender == "" {
return fmt.Errorf("sender should not be an empty address")
}
if m.LockId == 0 {
return fmt.Errorf("lock id should be positive: %d < 0", m.LockId)
}
if m.NewValAddr == "" {
return fmt.Errorf("NewValAddr should not be empty")
}
return nil
}
func (m MsgSuperfluidRedelegate) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m))
}
func (m MsgSuperfluidRedelegate) GetSigners() []sdk.AccAddress {
sender, _ := sdk.AccAddressFromBech32(m.Sender)
return []sdk.AccAddress{sender}
}
// func (m MsgSuperfluidRedelegate) Route() string { return RouterKey }
// func (m MsgSuperfluidRedelegate) Type() string { return TypeMsgSuperfluidRedelegate }
// func (m MsgSuperfluidRedelegate) ValidateBasic() error {
// if m.Sender == "" {
// return fmt.Errorf("sender should not be an empty address")
// }
// if m.LockId == 0 {
// return fmt.Errorf("lock id should be positive: %d < 0", m.LockId)
// }
// if m.NewValAddr == "" {
// return fmt.Errorf("NewValAddr should not be empty")
// }
// return nil
// }
// func (m MsgSuperfluidRedelegate) GetSignBytes() []byte {
// return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m))
// }
// func (m MsgSuperfluidRedelegate) GetSigners() []sdk.AccAddress {
// sender, _ := sdk.AccAddressFromBech32(m.Sender)
// return []sdk.AccAddress{sender}
// }
......@@ -32,7 +32,8 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type Params struct {
// refresh epoch identifier for staked amount
RefreshEpochIdentifier string `protobuf:"bytes,1,opt,name=refresh_epoch_identifier,json=refreshEpochIdentifier,proto3" json:"refresh_epoch_identifier,omitempty" yaml:"refresh_epoch_identifier"`
// the risk_factor is to be cut on OSMO equivalent value of lp tokens for superfluid staking, default: 5%
// the risk_factor is to be cut on OSMO equivalent value of lp tokens for
// superfluid staking, default: 5%
MinimumRiskFactor github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,2,opt,name=minimum_risk_factor,json=minimumRiskFactor,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"minimum_risk_factor" yaml:"minimum_risk_factor"`
// unbonding duration of superfluid delegation
UnbondingDuration time.Duration `protobuf:"bytes,3,opt,name=unbonding_duration,json=unbondingDuration,proto3,stdduration" json:"unbonding_duration" yaml:"unbonding_duration"`
......
......@@ -91,7 +91,8 @@ func (m *SuperfluidAsset) XXX_DiscardUnknown() {
var xxx_messageInfo_SuperfluidAsset proto.InternalMessageInfo
// SuperfluidIntermediaryAccount takes the role of intermediary between LP token and OSMO tokens for superfluid staking
// SuperfluidIntermediaryAccount takes the role of intermediary between LP token
// and OSMO tokens for superfluid staking
type SuperfluidIntermediaryAccount struct {
Denom string `protobuf:"bytes,1,opt,name=denom,proto3" json:"denom,omitempty"`
ValAddr string `protobuf:"bytes,2,opt,name=val_addr,json=valAddr,proto3" json:"val_addr,omitempty"`
......@@ -152,6 +153,12 @@ func (m *SuperfluidIntermediaryAccount) GetGaugeId() uint64 {
return 0
}
// The Osmo-Equivalent-TWAP for epoch N refers to the osmo worth we treat an LP
// share as having, for all of epoch N. This is intended to be set as the
// Time-weighted-average-osmo-backing for the entire duration of epoch N-1.
// (Thereby locking whats in use for epoch N as based on the prior epochs
// rewards) However for now, this is not the TWAP but instead the spot price at
// the boundary.
type EpochOsmoEquivalentTWAP struct {
EpochNumber int64 `protobuf:"varint,1,opt,name=epoch_number,json=epochNumber,proto3" json:"epoch_number,omitempty"`
Denom string `protobuf:"bytes,2,opt,name=denom,proto3" json:"denom,omitempty"`
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment