Skip to content

Commit

Permalink
Hostzone reward reallocation (#621)
Browse files Browse the repository at this point in the history
  • Loading branch information
hieuvubk authored Mar 7, 2023
1 parent 4021f78 commit 3539983
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 9 deletions.
4 changes: 4 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ var (
claimtypes.ModuleName: nil,
interchainquerytypes.ModuleName: nil,
icatypes.ModuleName: nil,
stakeibcmoduletypes.RewardCollectorName: nil,
// this line is used by starport scaffolding # stargate/app/maccPerms
}
)
Expand Down Expand Up @@ -933,6 +934,9 @@ func (app *StrideApp) BlacklistedModuleAccountAddrs() map[string]bool {
if acc == "stakeibc" {
continue
}
if acc == stakeibcmoduletypes.RewardCollectorName {
continue
}
modAccAddrs[authtypes.NewModuleAddress(acc).String()] = true
}

Expand Down
5 changes: 4 additions & 1 deletion app/apptesting/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ func SetupSuitelessTestHelper() SuitelessAppTestHelper {

// Mints coins directly to a module account
func (s *AppTestHelper) FundModuleAccount(moduleName string, amount sdk.Coin) {
err := s.App.BankKeeper.MintCoins(s.Ctx, moduleName, sdk.NewCoins(amount))
amountCoins := sdk.NewCoins(amount)
err := s.App.BankKeeper.MintCoins(s.Ctx, minttypes.ModuleName, amountCoins)
s.Require().NoError(err)
err = s.App.BankKeeper.SendCoinsFromModuleToModule(s.Ctx, minttypes.ModuleName, moduleName, amountCoins)
s.Require().NoError(err)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/Stride-Labs/stride/v6
go 1.19

require (
cosmossdk.io/errors v1.0.0-beta.7
cosmossdk.io/math v1.0.0-beta.3
github.com/cosmos/cosmos-proto v1.0.0-alpha8
github.com/cosmos/cosmos-sdk v0.46.7
Expand All @@ -29,7 +30,6 @@ require (
cloud.google.com/go/compute/metadata v0.2.1 // indirect
cloud.google.com/go/iam v0.7.0 // indirect
cloud.google.com/go/storage v1.27.0 // indirect
cosmossdk.io/errors v1.0.0-beta.7 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect
Expand Down
46 changes: 46 additions & 0 deletions x/stakeibc/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
epochstypes "github.com/Stride-Labs/stride/v6/x/epochs/types"
recordstypes "github.com/Stride-Labs/stride/v6/x/records/types"
"github.com/Stride-Labs/stride/v6/x/stakeibc/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)

func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInfo) {
Expand Down Expand Up @@ -68,6 +69,14 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf
k.ReinvestRewards(ctx)
}
}
if epochInfo.Identifier == epochstypes.MINT_EPOCH {
err := k.AllocateHostZoneReward(ctx)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Unable to allocate host zone reward, err: %s", err.Error()))
return
}

}
}

func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochInfo epochstypes.EpochInfo) {}
Expand Down Expand Up @@ -230,3 +239,40 @@ func (k Keeper) ReinvestRewards(ctx sdk.Context) {
}
}
}

func (k Keeper) AllocateHostZoneReward(ctx sdk.Context) error {
k.Logger(ctx).Info("Allocate host zone reward to delegator")

rewardCollectorAddress := k.accountKeeper.GetModuleAccount(ctx, types.RewardCollectorName).GetAddress()
rewardedTokens := k.bankKeeper.GetAllBalances(ctx, rewardCollectorAddress)
if rewardedTokens.IsEqual(sdk.Coins{}) {
return nil
}

msgSvr := NewMsgServerImpl(k)
for _, token := range rewardedTokens {
// get hostzone by reward token (in ibc denom format)
hz, err := k.GetHostZoneFromIBCDenom(ctx, token.Denom)
if err != nil {
k.Logger(ctx).Info("Can't get host zone from ibc token %s", token.Denom)
return err
}

// liquid stake all tokens
msg := types.NewMsgLiquidStake(rewardCollectorAddress.String(), token.Amount, hz.HostDenom)
_, err = msgSvr.LiquidStake(ctx, msg)
if err != nil {
k.Logger(ctx).Info("Can't liquid stake %s for hostzone %s", token.String(), hz.ChainId)
return err
}
}
// After liquid stake all tokens, reward collector receive stTokens
// Send all stTokens to fee collector to distribute to delegator later
stTokens := k.bankKeeper.GetAllBalances(ctx, rewardCollectorAddress)
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.RewardCollectorName, authtypes.FeeCollectorName, stTokens)
if err != nil {
k.Logger(ctx).Info("Can't send coins from module %s to module %s", types.RewardCollectorName, authtypes.FeeCollectorName)
return err
}
return nil
}
18 changes: 12 additions & 6 deletions x/stakeibc/keeper/icqcallbacks_withdrawal_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/spf13/cast"

ibctransfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types"
ibctypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types"

"github.com/Stride-Labs/stride/v6/utils"
icqtypes "github.com/Stride-Labs/stride/v6/x/interchainquery/types"
"github.com/Stride-Labs/stride/v6/x/stakeibc/types"
Expand All @@ -23,6 +27,7 @@ import (
// to the delegation account (for reinvestment) and fee account (for commission)
// Note: for now, to get proofs in your ICQs, you need to query the entire store on the host zone! e.g. "store/bank/key"
func WithdrawalBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
fmt.Println("WithdrawalBalanceCallback")
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_WithdrawalBalance,
"Starting withdrawal balance callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId))

Expand Down Expand Up @@ -91,13 +96,14 @@ func WithdrawalBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query icq

var msgs []sdk.Msg
if feeCoin.Amount.GT(sdk.ZeroInt()) {
msgs = append(msgs, &banktypes.MsgSend{
FromAddress: withdrawalAccount.Address,
ToAddress: feeAccount.Address,
Amount: sdk.NewCoins(feeCoin),
})
ibcTransferTimeoutNanos := k.GetParam(ctx, types.KeyIBCTransferTimeoutNanos)
timeoutTimestamp := uint64(ctx.BlockTime().UnixNano()) + ibcTransferTimeoutNanos
receiver := k.accountKeeper.GetModuleAccount(ctx, types.RewardCollectorName).GetAddress()
msg := ibctypes.NewMsgTransfer(ibctransfertypes.PortID, hostZone.TransferChannelId, feeCoin, withdrawalAccount.Address, receiver.String(), clienttypes.Height{}, timeoutTimestamp)

msgs = append(msgs, msg)
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance,
"Preparing MsgSends of %v from the withdrawal account to the fee account (for commission)", feeCoin.String()))
"Preparing MsgSends of %v from the withdrawal account to the distribution module account (for commission)", feeCoin.String()))
}
if reinvestCoin.Amount.GT(sdk.ZeroInt()) {
msgs = append(msgs, &banktypes.MsgSend{
Expand Down
238 changes: 238 additions & 0 deletions x/stakeibc/keeper/reward_allocation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package keeper_test

import (
"fmt"
"strings"

_ "github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
ibctesting "github.com/cosmos/ibc-go/v5/testing"
icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types"
clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types"
sdkmath "cosmossdk.io/math"
epochtypes "github.com/Stride-Labs/stride/v6/x/epochs/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
stakeibctypes "github.com/Stride-Labs/stride/v6/x/stakeibc/types"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/x/staking/teststaking"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
hosttypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/host/types"
ibctypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types"
abci "github.com/tendermint/tendermint/abci/types"
recordtypes "github.com/Stride-Labs/stride/v6/x/records/types"
)

var (
validators = []*stakeibctypes.Validator{
{
Name: "val1",
Address: "gaia_VAL1",
Weight: 1,
},
{
Name: "val2",
Address: "gaia_VAL2",
Weight: 4,
},
}
hostModuleAddress = stakeibctypes.NewZoneAddress(HostChainId)
)

func (s *KeeperTestSuite) SetupWithdrawAccount() (stakeibctypes.HostZone, Channel) {
// Set up host zone ica
delegationAccountOwner := fmt.Sprintf("%s.%s", HostChainId, "DELEGATION")
_ = s.CreateICAChannel(delegationAccountOwner)
delegationAddress := s.IcaAddresses[delegationAccountOwner]

withdrawalAccountOwner := fmt.Sprintf("%s.%s", HostChainId, "WITHDRAWAL")
withdrawalChannelID := s.CreateICAChannel(withdrawalAccountOwner)
withdrawalAddress := s.IcaAddresses[withdrawalAccountOwner]

feeAccountOwner := fmt.Sprintf("%s.%s", HostChainId, "FEE")
s.CreateICAChannel(feeAccountOwner)
feeAddress := s.IcaAddresses[feeAccountOwner]

// Set up ibc denom
ibcDenomTrace := s.GetIBCDenomTrace(Atom) // we need a true IBC denom here
s.App.TransferKeeper.SetDenomTrace(s.Ctx, ibcDenomTrace)

// Fund withdraw ica
initialModuleAccountBalance := sdk.NewCoin(Atom, sdkmath.NewInt(15_000))
s.FundAccount(sdk.MustAccAddressFromBech32(withdrawalAddress), initialModuleAccountBalance)
err := s.HostApp.BankKeeper.MintCoins(s.HostChain.GetContext(), minttypes.ModuleName, sdk.NewCoins(initialModuleAccountBalance))
s.Require().NoError(err)
err = s.HostApp.BankKeeper.SendCoinsFromModuleToAccount(s.HostChain.GetContext(), minttypes.ModuleName, sdk.MustAccAddressFromBech32(withdrawalAddress), sdk.NewCoins(initialModuleAccountBalance))
s.Require().NoError(err)

// Allow ica call ibc transfer in host chain
s.HostApp.ICAHostKeeper.SetParams(s.HostChain.GetContext(), hosttypes.Params{
HostEnabled: true,
AllowMessages: []string{
"/ibc.applications.transfer.v1.MsgTransfer",
},
})

hostZone := stakeibctypes.HostZone{
ChainId: HostChainId,
Address: hostModuleAddress.String(),
DelegationAccount: &stakeibctypes.ICAAccount{Address: delegationAddress},
WithdrawalAccount: &stakeibctypes.ICAAccount{
Address: withdrawalAddress,
Target: stakeibctypes.ICAAccountType_WITHDRAWAL,
},
FeeAccount: &stakeibctypes.ICAAccount{
Address: feeAddress,
Target: stakeibctypes.ICAAccountType_FEE,
},
ConnectionId: ibctesting.FirstConnectionID,
TransferChannelId: ibctesting.FirstChannelID,
HostDenom: Atom,
IbcDenom: ibcDenomTrace.IBCDenom(),
Validators: validators,
RedemptionRate: sdk.OneDec(),
}

currentEpoch := uint64(2)
strideEpochTracker := stakeibctypes.EpochTracker{
EpochIdentifier: epochtypes.STRIDE_EPOCH,
EpochNumber: currentEpoch,
NextEpochStartTime: uint64(s.Coordinator.CurrentTime.UnixNano() + 30_000_000_000), // dictates timeouts
}
mintEpochTracker := stakeibctypes.EpochTracker{
EpochIdentifier: epochtypes.MINT_EPOCH,
EpochNumber: currentEpoch,
NextEpochStartTime: uint64(s.Coordinator.CurrentTime.UnixNano() + 60_000_000_000), // dictates timeouts
}

initialDepositRecord := recordtypes.DepositRecord{
Id: 1,
DepositEpochNumber: 2,
HostZoneId: "GAIA",
Amount: sdkmath.ZeroInt(),
}

s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone)
s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, strideEpochTracker)
s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, mintEpochTracker)
s.App.RecordsKeeper.SetDepositRecord(s.Ctx, initialDepositRecord)

return hostZone, Channel{
PortID: icatypes.PortPrefix + withdrawalAccountOwner,
ChannelID: withdrawalChannelID,
}
}

func (s *KeeperTestSuite) TestAllocateRewardIBC() {
hz, channel := s.SetupWithdrawAccount()

rewardCollector := s.App.AccountKeeper.GetModuleAccount(s.Ctx, stakeibctypes.RewardCollectorName)

// Send tx to withdraw ica to perform ibc transfer from hostzone to stride
var msgs []sdk.Msg
ibcTransferTimeoutNanos := s.App.StakeibcKeeper.GetParam(s.Ctx, stakeibctypes.KeyIBCTransferTimeoutNanos)
timeoutTimestamp := uint64(s.HostChain.GetContext().BlockTime().UnixNano()) + ibcTransferTimeoutNanos
msg := ibctypes.NewMsgTransfer("transfer", "channel-0", sdk.NewCoin(Atom, sdkmath.NewInt(15_000)), hz.WithdrawalAccount.Address, rewardCollector.GetAddress().String(), clienttypes.NewHeight(1, 100), timeoutTimestamp)
msgs = append(msgs, msg)
data, _ := icatypes.SerializeCosmosTx(s.App.AppCodec(), msgs)
icaTimeOutNanos := s.App.StakeibcKeeper.GetParam(s.Ctx, stakeibctypes.KeyICATimeoutNanos)
icaTimeoutTimestamp := uint64(s.StrideChain.GetContext().BlockTime().UnixNano()) + icaTimeOutNanos

packetData := icatypes.InterchainAccountPacketData{
Type: icatypes.EXECUTE_TX,
Data: data,
}
packet := channeltypes.NewPacket(
packetData.GetBytes(),
1,
channel.PortID,
channel.ChannelID,
s.TransferPath.EndpointB.ChannelConfig.PortID,
s.TransferPath.EndpointB.ChannelID,
clienttypes.NewHeight(1, 100),
0,
)
_, err := s.App.StakeibcKeeper.SubmitTxs(s.Ctx, hz.ConnectionId, msgs, *hz.WithdrawalAccount, icaTimeoutTimestamp, "", nil)
s.Require().NoError(err)

// Simulate the process of receiving ica packets on the hostchain
module, _, err := s.HostChain.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(s.HostChain.GetContext(), "icahost")
s.Require().NoError(err)
cbs, ok := s.HostChain.App.GetIBCKeeper().Router.GetRoute(module)
s.Require().True(ok)
cbs.OnRecvPacket(s.HostChain.GetContext(), packet, nil)

// After withdraw ica send ibc transfer, simulate receiving transfer packet at stride
transferPacketData := ibctypes.NewFungibleTokenPacketData(
Atom, sdkmath.NewInt(15_000).String(), hz.WithdrawalAccount.Address, rewardCollector.GetAddress().String(),
)
transferPacketData.Memo = ""
transferPacket := channeltypes.NewPacket(
transferPacketData.GetBytes(),
1,
s.TransferPath.EndpointB.ChannelConfig.PortID,
s.TransferPath.EndpointB.ChannelID,
s.TransferPath.EndpointA.ChannelConfig.PortID,
s.TransferPath.EndpointA.ChannelID,
clienttypes.NewHeight(1, 100),
0,
)
cbs, ok = s.StrideChain.App.GetIBCKeeper().Router.GetRoute("transfer")
s.Require().True(ok)
cbs.OnRecvPacket(s.StrideChain.GetContext(), transferPacket, nil)

// Liquid stake all hostzone token then get stTokens back
// s.App.BeginBlocker(s.Ctx, abci.RequestBeginBlock{})
err = s.App.StakeibcKeeper.AllocateHostZoneReward(s.Ctx)
s.Require().NoError(err)

// Set up validator & delegation
addrs := s.TestAccs
for _, acc := range addrs {
s.FundAccount(acc, sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000000)))
}
valAddrs := simapp.ConvertAddrsToValAddrs(addrs)
tstaking := teststaking.NewHelper(s.T(), s.Ctx, s.App.StakingKeeper)

PK := simapp.CreateTestPubKeys(2)

// create validator with 50% commission
tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0))
tstaking.CreateValidator(valAddrs[0], PK[0], sdk.NewInt(100), true)

// create second validator with 0% commission
tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDec(0), sdk.NewDec(0), sdk.NewDec(0))
tstaking.CreateValidator(valAddrs[1], PK[1], sdk.NewInt(100), true)

s.App.EndBlocker(s.Ctx, abci.RequestEndBlock{})
s.Ctx = s.Ctx.WithBlockHeight(s.Ctx.BlockHeight() + 1)

// Simulate the token distribution from feeCollector to validators
abciValA := abci.Validator{
Address: PK[0].Address(),
Power: 100,
}
abciValB := abci.Validator{
Address: PK[1].Address(),
Power: 100,
}
votes := []abci.VoteInfo{
{
Validator: abciValA,
SignedLastBlock: true,
},
{
Validator: abciValB,
SignedLastBlock: true,
},
}
s.App.DistrKeeper.AllocateTokens(s.Ctx, 200, 200, sdk.ConsAddress(PK[1].Address()), votes)

// Withdraw reward
rewards, err := s.App.DistrKeeper.WithdrawDelegationRewards(s.Ctx, sdk.AccAddress(valAddrs[1]), valAddrs[1])
s.Require().NoError(err)

// Check balances contains stTokens
s.Require().True(strings.Contains(rewards.String(), "stuatom"))

}
2 changes: 1 addition & 1 deletion x/stakeibc/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,6 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {

// EndBlock executes all ABCI EndBlock logic respective to the capability module. It
// returns no validator updates.
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
Loading

0 comments on commit 3539983

Please sign in to comment.