diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 52a31479a..bae89205b 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -295,13 +295,6 @@ func (a *AppKeepers) InitKeepers( // Osmosis keepers - a.LockupKeeper = lockupkeeper.NewKeeper( - a.keys[lockuptypes.StoreKey], - a.GetSubspace(lockuptypes.ModuleName), - a.AccountKeeper, - a.BankKeeper, - ) - a.EpochsKeeper = epochskeeper.NewKeeper( a.keys[epochstypes.StoreKey], ) @@ -337,6 +330,14 @@ func (a *AppKeepers) InitKeepers( a.GAMMKeeper.SetPoolManager(a.PoolManagerKeeper) a.GAMMKeeper.SetTxFees(a.TxFeesKeeper) + a.LockupKeeper = lockupkeeper.NewKeeper( + a.keys[lockuptypes.StoreKey], + a.GetSubspace(lockuptypes.ModuleName), + a.AccountKeeper, + a.BankKeeper, + a.TxFeesKeeper, + ) + // Create IBC Keeper a.IBCKeeper = ibckeeper.NewKeeper( appCodec, diff --git a/x/lockup/keeper/keeper.go b/x/lockup/keeper/keeper.go index 745c7501d..18d32a2ca 100644 --- a/x/lockup/keeper/keeper.go +++ b/x/lockup/keeper/keeper.go @@ -21,10 +21,11 @@ type Keeper struct { ak types.AccountKeeper bk types.BankKeeper + tk types.TxFeesKeeper } // NewKeeper returns an instance of Keeper. -func NewKeeper(storeKey stroretypes.StoreKey, paramSpace paramtypes.Subspace, ak types.AccountKeeper, bk types.BankKeeper) *Keeper { +func NewKeeper(storeKey stroretypes.StoreKey, paramSpace paramtypes.Subspace, ak types.AccountKeeper, bk types.BankKeeper, tk types.TxFeesKeeper) *Keeper { // set KeyTable if it has not already been set if !paramSpace.HasKeyTable() { paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) @@ -35,6 +36,7 @@ func NewKeeper(storeKey stroretypes.StoreKey, paramSpace paramtypes.Subspace, ak paramSpace: paramSpace, ak: ak, bk: bk, + tk: tk, } } diff --git a/x/lockup/keeper/msg_server.go b/x/lockup/keeper/msg_server.go index 075ff56dd..4a77fa142 100644 --- a/x/lockup/keeper/msg_server.go +++ b/x/lockup/keeper/msg_server.go @@ -38,6 +38,11 @@ func (server msgServer) LockTokens(goCtx context.Context, msg *types.MsgLockToke return nil, err } + // Charge fess for locking tokens + if err = server.keeper.ChargeLockFee(ctx, owner, types.DefaultLockFee, msg.Coins); err != nil { + return nil, fmt.Errorf("charge gauge fee: %w", err) + } + // check if there's an existing lock from the same owner with the same duration. // If so, simply add tokens to the existing lock. lockExists := server.keeper.HasLock(ctx, owner, msg.Coins[0].Denom, msg.Duration) @@ -217,3 +222,27 @@ func (server msgServer) ForceUnlock(goCtx context.Context, msg *types.MsgForceUn return &types.MsgForceUnlockResponse{Success: true}, nil } + +// ChargeLockFee deducts a fee in the base denom from the specified address. The total cost is calculated as the sum +// of the fee and the amount of the base denom coin from lockCoins. If the account's balance is less than the total +// cost, the error is returned. Otherwise, the fee is charged from the payer and sent to x/txfees to be burned. +func (k Keeper) ChargeLockFee(ctx sdk.Context, payer sdk.AccAddress, fee sdk.Int, lockCoins sdk.Coins) (err error) { + var feeDenom string + if k.tk == nil { + feeDenom, err = sdk.GetBaseDenom() + } else { + feeDenom, err = k.tk.GetBaseDenom(ctx) + } + if err != nil { + return err + } + + totalCost := lockCoins.AmountOf(feeDenom).Add(fee) + accountBalance := k.bk.GetBalance(ctx, payer, feeDenom).Amount + + if accountBalance.LT(totalCost) { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, "account's balance is less than the total cost of the message: balance: %s%s, total cost: %s%s", accountBalance, feeDenom, totalCost, feeDenom) + } + + return k.tk.ChargeFeesFromPayer(ctx, payer, sdk.NewCoin(feeDenom, fee), nil) +} diff --git a/x/lockup/keeper/msg_server_test.go b/x/lockup/keeper/msg_server_test.go index b5897ab0e..34b7e8501 100644 --- a/x/lockup/keeper/msg_server_test.go +++ b/x/lockup/keeper/msg_server_test.go @@ -51,6 +51,9 @@ func (suite *KeeperTestSuite) TestMsgLockTokens() { suite.SetupTest() suite.FundAcc(test.param.lockOwner, test.param.coinsInOwnerAddress) + // fund address with lock fee + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + suite.FundAcc(test.param.lockOwner, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) msgServer := keeper.NewMsgServerImpl(suite.App.LockupKeeper) c := sdk.WrapSDKContext(suite.Ctx) @@ -75,6 +78,8 @@ func (suite *KeeperTestSuite) TestMsgLockTokens() { // add more tokens to lock via LockTokens suite.FundAcc(test.param.lockOwner, test.param.coinsInOwnerAddress) + // fund address with lock fee + suite.FundAcc(test.param.lockOwner, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) _, err = msgServer.LockTokens(sdk.WrapSDKContext(suite.Ctx), types.NewMsgLockTokens(test.param.lockOwner, locks[0].Duration, test.param.coinsToLock)) suite.Require().NoError(err) @@ -156,6 +161,9 @@ func (suite *KeeperTestSuite) TestMsgBeginUnlocking() { suite.SetupTest() suite.FundAcc(test.param.lockOwner, test.param.coinsInOwnerAddress) + // fund address with lock fee + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + suite.FundAcc(test.param.lockOwner, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) msgServer := keeper.NewMsgServerImpl(suite.App.LockupKeeper) goCtx := sdk.WrapSDKContext(suite.Ctx) @@ -209,6 +217,9 @@ func (suite *KeeperTestSuite) TestMsgBeginUnlockingAll() { suite.SetupTest() suite.FundAcc(test.param.lockOwner, test.param.coinsInOwnerAddress) + // fund address with lock fee + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + suite.FundAcc(test.param.lockOwner, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) msgServer := keeper.NewMsgServerImpl(suite.App.LockupKeeper) c := sdk.WrapSDKContext(suite.Ctx) @@ -268,6 +279,9 @@ func (suite *KeeperTestSuite) TestMsgEditLockup() { err := bankutil.FundAccount(suite.App.BankKeeper, suite.Ctx, test.param.lockOwner, test.param.coinsToLock) suite.Require().NoError(err) + // fund address with lock fee + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + suite.FundAcc(test.param.lockOwner, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) msgServer := keeper.NewMsgServerImpl(suite.App.LockupKeeper) c := sdk.WrapSDKContext(suite.Ctx) @@ -351,6 +365,10 @@ func (suite *KeeperTestSuite) TestMsgForceUnlock() { coinsToLock := sdk.Coins{sdk.NewCoin(poolDenom, defaultLockAmount)} suite.FundAcc(addr1, coinsToLock) + // fund address with lock fee + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + suite.FundAcc(addr1, sdk.NewCoins(sdk.NewCoin(baseDenom, types.DefaultLockFee))) + unbondingDuration := suite.App.StakingKeeper.GetParams(suite.Ctx).UnbondingTime resp, err := msgServer.LockTokens(c, types.NewMsgLockTokens(addr1, unbondingDuration, coinsToLock)) suite.Require().NoError(err) diff --git a/x/lockup/types/constants.go b/x/lockup/types/constants.go new file mode 100644 index 000000000..768eda6cf --- /dev/null +++ b/x/lockup/types/constants.go @@ -0,0 +1,7 @@ +package types + +import ( + "github.com/dymensionxyz/dymension/v3/x/common/types" +) + +var DefaultLockFee = types.DYM.QuoRaw(4) // 0.25 DYM diff --git a/x/lockup/types/expected_keepers.go b/x/lockup/types/expected_keepers.go index 3037b2be0..e884a70b3 100644 --- a/x/lockup/types/expected_keepers.go +++ b/x/lockup/types/expected_keepers.go @@ -12,7 +12,14 @@ type AccountKeeper interface { // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error } + +// TxFeesKeeper defines the expected interface needed to managing transaction fees. +type TxFeesKeeper interface { + GetBaseDenom(ctx sdk.Context) (denom string, err error) + ChargeFeesFromPayer(ctx sdk.Context, payer sdk.AccAddress, takerFeeCoin sdk.Coin, beneficiary *sdk.AccAddress) error +}