Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(perp): MsgWithdrawFromPerpFund sudo call as part of #1642 #1734

Merged
merged 14 commits into from
Jan 1, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#1573](https://github.com/NibiruChain/nibiru/pull/1573) - feat(perp): Close markets and compute settlement price
* [#1705](https://github.com/NibiruChain/nibiru/pull/1705) - feat(perp): Add oracle pair to market object
* [#1718](https://github.com/NibiruChain/nibiru/pull/1718) - fix: fees does not require additional funds
* [#1734](https://github.com/NibiruChain/nibiru/pull/1734) - feat(perp): MsgDonateToPerpFund
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit of a more descriptive message like the one in the title would be nice. You will appreciate it when you search for this in the changelog in four years!


### Non-breaking/Compatible Improvements

Expand Down
2 changes: 1 addition & 1 deletion app/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ func ModuleAccPerms() map[string][]string {

perptypes.ModuleName: {},
perptypes.VaultModuleAccount: {},
perptypes.PerpEFModuleAccount: {},
perptypes.PerpFundModuleAccount: {},
perptypes.FeePoolModuleAccount: {},
perptypes.DNRAllocationModuleAccount: {},
perptypes.DNREscrowModuleAccount: {},
Expand Down
21 changes: 21 additions & 0 deletions proto/nibiru/perp/v2/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ service Msg {
// [Admin] Only callable by sudoers.
rpc ShiftSwapInvariant(MsgShiftSwapInvariant)
returns (MsgShiftSwapInvariantResponse) {}

// WithdrawFromPerpFund: gRPC tx msg to withdraw from the perp fund module
// account. [Admin] Only callable by sudoers.
rpc WithdrawFromPerpFund(MsgWithdrawFromPerpFund)
returns (MsgWithdrawFromPerpFundResponse) {}
}

// -------------------------- Settle Position --------------------------
Expand Down Expand Up @@ -429,3 +434,19 @@ message MsgShiftSwapInvariant {
}

message MsgShiftSwapInvariantResponse {}

// -------------------------- WithdrawFromPerpFund --------------------------

// WithdrawFromPerpFund: gRPC tx msg for changing the swap invariant.
// Admin-only.
message MsgWithdrawFromPerpFund {
string sender = 1;
string amount = 2 [
k-yang marked this conversation as resolved.
Show resolved Hide resolved
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string denom = 3;
string to_addr = 4;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) it might be safer to just withdraw to whoever sends the message, i.e. the message sender.

I can foresee possible human error in the future where somebody sends the message but accidentally sets the to_addr different from the message signer/sender.

Copy link
Member Author

@Unique-Divine Unique-Divine Dec 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too. The problem we might encounter here is when the claim goes to a smart contract that doesn't expose a function to withdraw its balance, or if there's routing happening between multiple contracts like the ones used in Mars Protocol or in future Nibiru-developed contracts.

sudo_contract_calls_msg -> withdraw_to_contract_addr -> No fn to withdraw -> funds are lost

I'm trying to avoid this ^ deadlock.

Adding an explicit to_addr makes it less error prone in a different way, by making the caller pass in the location they want the funds to end up.

Another option is to make that field nullable, so that empty means, to_addr := sender. Would you prefer that?

Copy link
Member

@k-yang k-yang Jan 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I didn't even consider that scenario. In that case, then I think it's better to be explicit.

Thinking about it more, in the general case, we'll likely be sending these WithdrawFromPerpFund messages from the CW3 multisig. Having the ability to target a recipient with the message would be more efficient, instead of withdrawing to the CW3 multisig and then doing a bank transfer.

A more sophisticated design could be to only execute the message if the to_addr is in the sudo sudoers list. It would prevent the funds from going to a CosmWasm smart contract that doesn't have the withdraw functionality, because we would be the ones whitelisting the possible recipients. But that could be addressed in a separate PR. What's implemented looks good to me.

}

message MsgWithdrawFromPerpFundResponse {}
16 changes: 2 additions & 14 deletions wasmbinding/bindings/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,15 @@ import (
type NibiruMsg struct {
// bindings-perp ExecuteMsg enum types
// MultiLiquidate *MultiLiquidate `json:"multi_liquidate,omitempty"` // TODO
DonateToInsuranceFund *DonateToInsuranceFund `json:"donate_to_insurance_fund,omitempty"` // TODO
InsuranceFundWithdraw *InsuranceFundWithdraw `json:"insurance_fund_withdraw,omitempty"`
SetMarketEnabled *SetMarketEnabled `json:"set_market_enabled,omitempty"`
CreateMarket *CreateMarket `json:"create_market,omitempty"`
SetMarketEnabled *SetMarketEnabled `json:"set_market_enabled,omitempty"`
CreateMarket *CreateMarket `json:"create_market,omitempty"`

EditOracleParams *EditOracleParams `json:"edit_oracle_params,omitempty"`

// Short for "no operation". A wasm binding payload that does nothing.
NoOp *NoOp `json:"no_op,omitempty"`
}

type DonateToInsuranceFund struct {
Sender string `json:"sender"`
Donation sdk.Coin `json:"donation"`
}

type EditOracleParams struct {
VotePeriod *sdkmath.Int `json:"vote_period,omitempty"`
VoteThreshold *sdk.Dec `json:"vote_threshold,omitempty"`
Expand All @@ -43,11 +36,6 @@ type EditOracleParams struct {
ValidatorFeeRatio *sdk.Dec `json:"validator_fee_ratio,omitempty"`
}

type InsuranceFundWithdraw struct {
Amount sdkmath.Int `json:"amount"`
To string `json:"to"`
}

type SetMarketEnabled struct {
Pair string `json:"pair"`
Enabled bool `json:"enabled"`
Expand Down
19 changes: 0 additions & 19 deletions wasmbinding/exec_perp.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,6 @@ func (exec *ExecutorPerp) MsgServer() perpv2types.MsgServer {
return perpv2keeper.NewMsgServerImpl(exec.PerpV2)
}

func (exec *ExecutorPerp) InsuranceFundWithdraw(
cwMsg *bindings.InsuranceFundWithdraw, ctx sdk.Context,
) (err error) {
if cwMsg == nil {
return wasmvmtypes.InvalidRequest{Err: "null msg"}
}

to, err := sdk.AccAddressFromBech32(cwMsg.To)
if err != nil {
return err
}

return exec.PerpV2.Admin.WithdrawFromInsuranceFund(
ctx,
cwMsg.Amount,
to,
)
}

// TODO: rename to CloseMarket
func (exec *ExecutorPerp) SetMarketEnabled(
cwMsg *bindings.SetMarketEnabled, ctx sdk.Context,
Expand Down
32 changes: 0 additions & 32 deletions wasmbinding/exec_perp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,33 +134,13 @@ func (s *TestSuitePerpExecutor) OnSetupEnd() {
// Happy path coverage
func (s *TestSuitePerpExecutor) TestPerpExecutorHappy() {
for _, err := range []error{
s.DoInsuranceFundWithdrawTest(sdk.NewInt(69), s.contractDeployer),
s.DoCreateMarketTest(asset.MustNewPair("ufoo:ubar")),
s.DoCreateMarketTestWithParams(asset.MustNewPair("ufoo2:ubar")),
} {
s.NoError(err)
}
}

func (s *TestSuitePerpExecutor) DoInsuranceFundWithdrawTest(
amt sdkmath.Int, to sdk.AccAddress,
) error {
cwMsg := &bindings.InsuranceFundWithdraw{
Amount: amt,
To: to.String(),
}

err := testapp.FundModuleAccount(
s.nibiru.BankKeeper,
s.ctx,
perpv2types.PerpEFModuleAccount,
sdk.NewCoins(sdk.NewCoin(perpv2types.TestingCollateralDenomNUSD, sdk.NewInt(420))),
)
s.NoError(err)

return s.exec.InsuranceFundWithdraw(cwMsg, s.ctx)
}

func (s *TestSuitePerpExecutor) DoCreateMarketTest(pair asset.Pair) error {
cwMsg := &bindings.CreateMarket{
Pair: pair.String(),
Expand Down Expand Up @@ -197,11 +177,6 @@ func (s *TestSuitePerpExecutor) DoCreateMarketTestWithParams(pair asset.Pair) er
return s.exec.CreateMarket(cwMsg, s.ctx)
}

func (s *TestSuitePerpExecutor) TestSadPaths_Nil() {
err := s.exec.InsuranceFundWithdraw(nil, s.ctx)
s.Error(err)
}

func (s *TestSuitePerpExecutor) DoSetMarketEnabledTest(
pair asset.Pair, enabled bool,
) error {
Expand All @@ -220,13 +195,6 @@ func (s *TestSuitePerpExecutor) DoSetMarketEnabledTest(
return err
}

func (s *TestSuitePerpExecutor) TestSadPath_InsuranceFundWithdraw() {
fundsToWithdraw := sdk.NewCoin(perpv2types.TestingCollateralDenomNUSD, sdk.NewInt(69_000))

err := s.DoInsuranceFundWithdrawTest(fundsToWithdraw.Amount, s.contractDeployer)
s.Error(err)
}

func (s *TestSuitePerpExecutor) TestSadPaths_InvalidPair() {
sadPair := asset.Pair("ftt:ust:doge")
pair := sadPair
Expand Down
49 changes: 0 additions & 49 deletions wasmbinding/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,55 +313,6 @@ func (s *TestSuiteExecutor) TestNoOp() {
s.NoErrorf(err, "contractRespBz: %s", contractRespBz)
}

func (s *TestSuiteExecutor) TestInsuranceFundWithdraw() {
admin := s.contractDeployer.String()
amtToWithdraw := sdk.NewInt(69)
execMsg := bindings.NibiruMsg{
InsuranceFundWithdraw: &bindings.InsuranceFundWithdraw{
Amount: amtToWithdraw,
To: admin,
},
}

s.T().Log("Executing should fail since the IF doesn't have funds")
contract := s.contractController
s.keeper.SetSudoContracts(
[]string{contract.String()}, s.ctx,
)
contractRespBz, err := s.ExecuteAgainstContract(contract, execMsg)
s.Errorf(err, "contractRespBz: %s", contractRespBz)

s.T().Log("Executing without permission should fail")
s.keeper.SetSudoContracts(
[]string{}, s.ctx,
)
contractRespBz, err = s.ExecuteAgainstContract(contract, execMsg)
s.Errorf(err, "contractRespBz: %s", contractRespBz)

s.T().Log("Executing should work when the IF has funds")
err = testapp.FundModuleAccount(
s.nibiru.BankKeeper,
s.ctx,
perpv2types.PerpEFModuleAccount,
sdk.NewCoins(sdk.NewCoin(perpv2types.TestingCollateralDenomNUSD, sdk.NewInt(420))),
)
s.NoError(err)
s.keeper.SetSudoContracts(
[]string{contract.String()}, s.ctx,
)
contractRespBz, err = s.ExecuteAgainstContract(contract, execMsg)
s.NoErrorf(err, "contractRespBz: %s", contractRespBz)

s.T().Log("Executing the wrong contract should fail")
contract = s.contractPerp
s.keeper.SetSudoContracts(
[]string{contract.String()}, s.ctx,
)
contractRespBz, err = s.ExecuteAgainstContract(contract, execMsg)
s.Errorf(err, "contractRespBz: %s", contractRespBz)
s.Contains(err.Error(), "Error parsing into type")
}

func (s *TestSuiteExecutor) TestSetMarketEnabled() {
// admin := s.contractDeployer.String()
perpv2Genesis := genesis.PerpV2Genesis()
Expand Down
7 changes: 0 additions & 7 deletions wasmbinding/message_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ func (messenger *CustomMessenger) DispatchMsg(
err = messenger.Perp.CreateMarket(cwMsg, ctx)
return events, data, err

case contractExecuteMsg.ExecuteMsg.InsuranceFundWithdraw != nil:
if err := messenger.Sudo.CheckPermissions(contractAddr, ctx); err != nil {
return events, data, err
}
cwMsg := contractExecuteMsg.ExecuteMsg.InsuranceFundWithdraw
err = messenger.Perp.InsuranceFundWithdraw(cwMsg, ctx)
return events, data, err
case contractExecuteMsg.ExecuteMsg.SetMarketEnabled != nil:
if err := messenger.Sudo.CheckPermissions(contractAddr, ctx); err != nil {
return events, data, err
Expand Down
2 changes: 1 addition & 1 deletion x/oracle/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (h Hooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, _ uint64)
}

if !totalRemainder.IsZero() {
err = h.bankKeeper.SendCoinsFromModuleToModule(ctx, perptypes.FeePoolModuleAccount, perptypes.PerpEFModuleAccount, totalRemainder)
err = h.bankKeeper.SendCoinsFromModuleToModule(ctx, perptypes.FeePoolModuleAccount, perptypes.PerpFundModuleAccount, totalRemainder)
if err != nil {
h.k.Logger(ctx).Error("Failed to send coins to perp ef module", "err", err)
}
Expand Down
2 changes: 1 addition & 1 deletion x/oracle/keeper/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestHooks_AfterEpochEnd(t *testing.T) {
balances := app.BankKeeper.GetAllBalances(ctx, account.GetAddress())
assert.Equal(t, tt.expectedOracleBalances, balances)

account = app.AccountKeeper.GetModuleAccount(ctx, perptypes.PerpEFModuleAccount)
account = app.AccountKeeper.GetModuleAccount(ctx, perptypes.PerpFundModuleAccount)
balances = app.BankKeeper.GetAllBalances(ctx, account.GetAddress())
assert.Equal(t, tt.expectedEFBalances, balances)
})
Expand Down
40 changes: 24 additions & 16 deletions x/perp/v2/keeper/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,42 @@ import (
// function is being used when it's called from the PerpKeeper.Admin struct.
type admin struct{ *Keeper }

/*
WithdrawFromInsuranceFund sends funds from the Insurance Fund to the given "to"
address.

Args:
- ctx: Blockchain context holding the current state
- amount: Amount of micro-NUSD to withdraw.
- to: Recipient address
*/
func (k admin) WithdrawFromInsuranceFund(
ctx sdk.Context, amount sdkmath.Int, to sdk.AccAddress,
// WithdrawFromPerpFund sends funds from the Perp Fund to the "to" address.
//
// Args:
// - ctx: Blockchain context holding the current state
// - amount: Amount of micro-NUSD to withdraw.
// - sender: Admin address registered in x/sudo
// - to: Recipient address
func (k admin) WithdrawFromPerpFund(
ctx sdk.Context, amount sdkmath.Int, sender, to sdk.AccAddress, denom string,
) (err error) {
collateral, err := k.Collateral.Get(ctx)
if err != nil {
if err := k.SudoKeeper.CheckPermissions(sender, ctx); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some opinions related to checking permissions here. I created some PR here: #1751

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a stateful validation, reading from the KV store of another module, so it's not really for the MsgServer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. It does not change that principle if this call is still in the message server. MsgServer delegates validation of the request to the SudoKeeper (that he really is the one reading the KVStore, not the MsgServer), and if it is correct, it calls the Admin function.

What I don't believe is in the admin function to validate the permissions. It is a thin line and a matter of convention, but I can show you some benefits of this slight separation.

Copy link
Member Author

@Unique-Divine Unique-Divine Dec 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking a whitelist is not a message field validation. If the keeper method doesn't have the permission check, other modules that import the methods can sidestep the sudo, defeating the purpose for the check.

It's not just a difference by convention. Switching SudoKeeper.CheckPermissions to happen in the MsgServer changes the security of the keeper method itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message field validation should be done by default out of the message server as long as you implement ValidateBasic(). Validating any field in the message server is duplicating computing cycles. What, then, is the use case of the MsgServer if we remove the ValidateBasic?

There are two paradigms here: the MsgServer as orchestrator of keepers (my ideal) or the Keepers as holders of all the logic and being pure defensive (your ideal; this removes the need for MsgServer in my opinion as it becomes only a wrapper of the keeper).

It is a matter of convention from the moment you agree on what it means to share some Keeper method. Is there any use case at this stage for CloseMarket to be public in the keeper? Try to avoid the what-if thing.

I am open to anything as long as it makes sense, and we will follow the convention from now on.

return err
}

coinToSend := sdk.NewCoin(collateral, amount)
var collateralDenom string
if denom == "" {
denomFromState, err := k.Collateral.Get(ctx)
if err != nil {
return err
}
collateralDenom = denomFromState
} else {
collateralDenom = denom
}

coinToSend := sdk.NewCoin(collateralDenom, amount)
if err = k.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
/* from */ types.PerpEFModuleAccount,
/* from */ types.PerpFundModuleAccount,
/* to */ to,
/* amount */ sdk.NewCoins(coinToSend),
); err != nil {
return err
}
ctx.EventManager().EmitEvent(sdk.NewEvent(
"withdraw_from_if",
"withdraw_from_perp_fund",
sdk.NewAttribute("to", to.String()),
sdk.NewAttribute("funds", coinToSend.String()),
))
Expand Down
Loading
Loading