Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions proto/reserve/vaults/proposal.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ message UpdatesCollateralProposal {
string description = 2;
MsgUpdatesCollateral updates_collateral = 3 [(gogoproto.nullable) = false];
}

message BurnShortfallProposal {
option (gogoproto.goproto_getters) = false;
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
option (amino.name) = "reserve/BurnShortfallProposal";

string title = 1;
string description = 2;
MsgBurnShortfall burn_shortfall = 3 [(gogoproto.nullable) = false];
}
19 changes: 19 additions & 0 deletions proto/reserve/vaults/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ service Msg {

// Close defines a method for close vault
rpc Close(MsgClose) returns (MsgCloseResponse);

rpc BurnShortfall(MsgBurnShortfall) returns (MsgBurnShortfallResponse);
}

message MsgUpdateParams {
Expand Down Expand Up @@ -295,3 +297,20 @@ message MsgClose {

// MsgRepayResponse defines the Msg/Mint response type.
message MsgCloseResponse {}

message MsgBurnShortfall {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "authority";

string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
string mint_denom = 2;
string amount = 3 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(amino.dont_omitempty) = true,
(gogoproto.nullable) = false
];
}

message MsgBurnShortfallResponse {}
30 changes: 29 additions & 1 deletion x/vaults/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,32 @@ func (k *Keeper) burnDebt(ctx context.Context, vmKey string, vm types.VaultManag
}
vm.MintAvailable = vm.MintAvailable.Add(coin.Amount)
return k.VaultsManager.Set(ctx, vmKey, vm)
}
}

func (k *Keeper) BurnShortfallByMintDenom(ctx context.Context, mintDenom string, amount math.Int) error {
// get amount shortfall by mintdenom
amountShortfall, err := k.ShortfallAmount.Get(ctx, mintDenom)
if err != nil {
return err
}
if amountShortfall.LT(amount) {
return fmt.Errorf("amount shortfall is less than")
}
// get amount reserve by mintdenom
amountReserve := k.BankKeeper.GetAllBalances(ctx, k.accountKeeper.GetModuleAddress(types.ReserveModuleName)).AmountOf(mintDenom)
if amountReserve.LT(amount) {
return fmt.Errorf("amount reserve is less than")
}

// Burn token from Reserve module
if err := k.BankKeeper.BurnCoins(ctx, types.ReserveModuleName, sdk.NewCoins(sdk.NewCoin(mintDenom, amount))); err != nil {
return err
}

// Update shortfall amount after burning
remainingShortfall := amountShortfall.Sub(amount)
if err := k.ShortfallAmount.Set(ctx, mintDenom, remainingShortfall); err != nil {
return err
}
return nil
}
24 changes: 24 additions & 0 deletions x/vaults/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"
"fmt"
"slices"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -180,3 +181,26 @@ func (k msgServer) Close(ctx context.Context, msg *types.MsgClose) (*types.MsgCl
}
return &types.MsgCloseResponse{}, nil
}

// use mintDenom in reserve to burn Shortfall via gov
func (k msgServer) BurnShortfall(ctx context.Context, msg *types.MsgBurnShortfall) (*types.MsgBurnShortfallResponse, error) {
err := msg.ValidateBasic()
if err != nil {
return &types.MsgBurnShortfallResponse{}, err
}

if k.authority != msg.Authority {
return nil, errorsmod.Wrapf(types.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority)
}

if !slices.Contains(k.GetParams(ctx).AllowedMintDenom, msg.MintDenom) {
return nil, fmt.Errorf("denom %s is not in the allowed mint denom list", msg.MintDenom)
}

err = k.BurnShortfallByMintDenom(ctx, msg.MintDenom, msg.Amount)
if err != nil {
return nil, err
}

return &types.MsgBurnShortfallResponse{}, nil
}
194 changes: 194 additions & 0 deletions x/vaults/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package keeper_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"

"cosmossdk.io/math"

"github.com/onomyprotocol/reserve/x/vaults/types"
)

func (s *KeeperTestSuite) TestBurnShortfallByMintDenom() {
testcases := []struct {
name string
mintDenom string
setup func() types.MsgBurnShortfall
expShortfallAmountAfterBurn math.Int
expReserveBalcesAfterBurn math.Int
expPass bool
}{
{
name: "success burn part",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(10_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(5_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: "onomy10d07y265gmmuvt4z0w9aw880jnsr700jqr8n8k",
MintDenom: "fxUSD",
Amount: math.NewInt(1_000_000),
}
},
expShortfallAmountAfterBurn: math.NewInt(4_000_000),
expReserveBalcesAfterBurn: math.NewInt(9_000_000),
expPass: true,
},
{
name: "success maximum burn, shortfallAmount is less than reserve balances",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(10_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(1_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: "onomy10d07y265gmmuvt4z0w9aw880jnsr700jqr8n8k",
MintDenom: "fxUSD",
Amount: math.NewInt(1_000_000),
}
},
expShortfallAmountAfterBurn: math.ZeroInt(),
expReserveBalcesAfterBurn: math.NewInt(9_000_000),
expPass: true,
},
{
name: "success maximum burn, reserve balancess is less than shortfallAmount",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(1_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(10_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: "onomy10d07y265gmmuvt4z0w9aw880jnsr700jqr8n8k",
MintDenom: "fxUSD",
Amount: math.NewInt(1_000_000),
}
},
expShortfallAmountAfterBurn: math.NewInt(9_000_000),
expReserveBalcesAfterBurn: math.ZeroInt(),
expPass: true,
},
{
name: "fail, reserve balancess no money",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure Guaranteed Shortfall Amount
err := s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(10_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: "onomy10d07y265gmmuvt4z0w9aw880jnsr700jqr8n8k",
MintDenom: "fxUSD",
Amount: math.NewInt(1_000_000),
}
},
expPass: false,
},
{
name: "fail, government account not the signatory for the proposed message",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(1_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(10_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: s.TestAccs[0].String(),
MintDenom: "fxUSD",
Amount: math.NewInt(1_000_000),
}
},
expPass: false,
},
{
name: "fail, denom is not in the allowed mint denom list",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(1_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(10_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: s.TestAccs[0].String(),
MintDenom: "atom",
Amount: math.NewInt(1_000_000),
}
},
expPass: false,
},
{
name: "fail, burn all",
mintDenom: "fxUSD",
setup: func() types.MsgBurnShortfall {
// make sure reserve has money
mintCoin := sdk.NewCoins(sdk.NewCoin("fxUSD", math.NewInt(5_000_000)))
s.FundAccount(s.TestAccs[0], types.ModuleName, mintCoin)
err := s.k.BankKeeper.SendCoinsFromAccountToModule(s.Ctx, s.TestAccs[0], types.ReserveModuleName, mintCoin)
s.Require().NoError(err)

// make sure Guaranteed Shortfall Amount
err = s.k.ShortfallAmount.Set(s.Ctx, "fxUSD", math.NewInt(5_000_000))
s.Require().NoError(err)
return types.MsgBurnShortfall{
Authority: "onomy10d07y265gmmuvt4z0w9aw880jnsr700jqr8n8k",
MintDenom: "fxUSD",
Amount: math.NewInt(10_000_000),
}
},
expPass: false,
},
}

for _, t := range testcases {
s.Run(t.name, func() {
s.SetupTest()
msg := t.setup()

// burn Shortfall
_, err := s.msgServer.BurnShortfall(s.Ctx, &msg)
if t.expPass {
s.Require().NoError(err)

// check reserve balances after burn
reserveBalces := s.k.BankKeeper.GetAllBalances(s.Ctx, s.App.AccountKeeper.GetModuleAddress(types.ReserveModuleName))
s.Require().True(reserveBalces.AmountOf(t.mintDenom).Equal(t.expReserveBalcesAfterBurn))

// check ShortfallAmount after burn

shortfallAmountAfterBurn, err := s.k.ShortfallAmount.Get(s.Ctx, t.mintDenom)
s.Require().NoError(err)

s.Require().True(shortfallAmountAfterBurn.Equal(t.expShortfallAmountAfterBurn))
} else {
s.Require().Error(err)
}
})
}
}
3 changes: 3 additions & 0 deletions x/vaults/module/proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func NewVaultsProposalHandler(k *keeper.Keeper) govtypes.Handler {
case *types.UpdatesCollateralProposal:
_, err := msgSv.UpdatesCollateral(ctx, types.NewMsgUpdatesCollateral(c))
return err
case *types.BurnShortfallProposal:
_, err := msgSv.BurnShortfall(ctx, types.NewMsgBurnShortfall(c))
return err
default:
return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s proposal content type: %T", types.ModuleName, c)
}
Expand Down
3 changes: 3 additions & 0 deletions x/vaults/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&ActiveCollateralProposal{}, "reserve/ActiveCollateralProposal", nil)
cdc.RegisterConcrete(&UpdatesCollateralProposal{}, "reserve/UpdatesCollateralProposal", nil)
cdc.RegisterConcrete(&BurnShortfallProposal{}, "reserve/BurnShortfallProposal", nil)
}

func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
Expand All @@ -20,6 +21,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgUpdateParams{},
&MsgActiveCollateral{},
&MsgBurnShortfall{},
&MsgUpdatesCollateral{},
&MsgCreateVault{},
&MsgDeposit{},
Expand All @@ -33,6 +35,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
(*govtypes.Content)(nil),
&ActiveCollateralProposal{},
&UpdatesCollateralProposal{},
&BurnShortfallProposal{},
)

}
Loading
Loading