diff --git a/app/upgrades.go b/app/upgrades.go index 8cb83e3..1f1ce7a 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -71,6 +71,8 @@ func (app *App) setupUpgradeHandlers() { app.configurator, app.ICAHostKeeper, app.StakingKeeper, + app.BankKeeper, + app.TransferKeeper, ), ) diff --git a/app/upgrades/v11/constants.go b/app/upgrades/v11/constants.go index bab2749..8253f6e 100644 --- a/app/upgrades/v11/constants.go +++ b/app/upgrades/v11/constants.go @@ -1,5 +1,59 @@ package v11 +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( - UpgradeName = "v11.0.0" + UpgradeName = "v11.0.0" + EVMCoinDenom = "axrp" + + // mainnet + MainnetChainID = "xrplevm_1440000-1" + ElysChannelID = "channel-1" + WithdrawalAddress = "ethm1m2pp8zjwk3ystxyxvw5h3mrhhhnzcr2ltjntz9" + + // testnet + TestnetChainID = "xrplevm_1449000-1" + TestnetElysChannelID = "channel-17" + TestnetWithdrawalAddress = "ethm16gt28px9q0fp48eatecp7j032lm5vaxs2t29pa" + + // devnet + DevnetChainID = "xrplevm_1449900-1" + DevnetElysChannelID = "channel-4" + DevnetWithdrawalAddress = "ethm16gt28px9q0fp48eatecp7j032lm5vaxs2t29pa" ) + +var devnetAmount, _ = sdkmath.NewIntFromString("2000000000000000000") +var testnetAmount, _ = sdkmath.NewIntFromString("2000000000000000000") +var mainnetElysAmount, _ = sdkmath.NewIntFromString("6955539034646993768414") + +// ElysRecovery holds, for a single network, the Elys transfer channel whose +// escrow holds the stranded XRP, the address that should receive it, and the +// coin (denom + amount of XRP in axrp base units) to unescrow. +type ElysRecovery struct { + ChannelID string + WithdrawalAddress string + Coin sdk.Coin +} + +// ElysRecoveryByNetwork maps each network's Cosmos chain ID to its Elys recovery +// parameters. The v11 handler selects the entry matching ctx.ChainID(). +var ElysRecoveryByNetwork = map[string]ElysRecovery{ + MainnetChainID: { + ChannelID: ElysChannelID, + WithdrawalAddress: WithdrawalAddress, + Coin: sdk.NewCoin(EVMCoinDenom, mainnetElysAmount), + }, + TestnetChainID: { + ChannelID: TestnetElysChannelID, + WithdrawalAddress: TestnetWithdrawalAddress, + Coin: sdk.NewCoin(EVMCoinDenom, testnetAmount), + }, + DevnetChainID: { + ChannelID: DevnetElysChannelID, + WithdrawalAddress: DevnetWithdrawalAddress, + Coin: sdk.NewCoin(EVMCoinDenom, devnetAmount), + }, +} diff --git a/app/upgrades/v11/keepers.go b/app/upgrades/v11/keepers.go index ab798b0..b757855 100644 --- a/app/upgrades/v11/keepers.go +++ b/app/upgrades/v11/keepers.go @@ -20,3 +20,15 @@ type StakingKeeper interface { GetParams(ctx context.Context) (params stakingtypes.Params, err error) SetParams(ctx context.Context, params stakingtypes.Params) error } + +// BankKeeper is the narrow interface required by the v11 upgrade +// handler. It matches a subset of bankkeeper.Keeper. +type BankKeeper interface { + GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin +} + +// TransferKeeper is the narrow interface required by the v11 upgrade +// handler. It matches a subset of transferkeeper.Keeper. +type TransferKeeper interface { + UnescrowCoin(ctx sdk.Context, escrowAddress, receiver sdk.AccAddress, coin sdk.Coin) error +} diff --git a/app/upgrades/v11/upgrades.go b/app/upgrades/v11/upgrades.go index 0898b70..3756873 100644 --- a/app/upgrades/v11/upgrades.go +++ b/app/upgrades/v11/upgrades.go @@ -2,12 +2,15 @@ package v11 import ( "context" + "fmt" "time" + "cosmossdk.io/log" upgradetypes "cosmossdk.io/x/upgrade/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" ) func CreateUpgradeHandler( @@ -15,6 +18,8 @@ func CreateUpgradeHandler( configurator module.Configurator, icaHostKeeper ICAHostKeeper, stakingKeeper StakingKeeper, + bankKeeper BankKeeper, + transferKeeper TransferKeeper, ) upgradetypes.UpgradeHandler { return func(c context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { ctx := sdk.UnwrapSDKContext(c) @@ -40,7 +45,42 @@ func CreateUpgradeHandler( logger.Info("Disabling ICA host module...") icaHostKeeper.SetParams(ctx, icahosttypes.NewParams(false, nil)) + + logger.Info("Withdrawing Elys escrow to provided address...") + if err := withdrawElysEscrow(ctx, logger, bankKeeper, transferKeeper); err != nil { + return nil, err + } + logger.Info("Finished v11 upgrade handler") return vm, nil } } + +// withdrawElysEscrow releases the configured amount of XRP from the Elys channel +// escrow to the recovery address configured for the running network. +func withdrawElysEscrow(ctx sdk.Context, logger log.Logger, bankKeeper BankKeeper, transferKeeper TransferKeeper) error { + recoveryCfg, ok := ElysRecoveryByNetwork[ctx.ChainID()] + if !ok || recoveryCfg.ChannelID == "" { + logger.Info("no Elys escrow recovery configured for this network, skipping", "chainID", ctx.ChainID()) + return nil + } + + if !recoveryCfg.Coin.IsPositive() { + logger.Info("Elys escrow recovery amount is zero, nothing to withdraw", "chainID", ctx.ChainID()) + return nil + } + + escrowAddr := transfertypes.GetEscrowAddress(transfertypes.PortID, recoveryCfg.ChannelID) + + destAddr, err := sdk.AccAddressFromBech32(recoveryCfg.WithdrawalAddress) + if err != nil { + return fmt.Errorf("invalid withdrawal address %q: %w", recoveryCfg.WithdrawalAddress, err) + } + + if err := transferKeeper.UnescrowCoin(ctx, escrowAddr, destAddr, recoveryCfg.Coin); err != nil { + return fmt.Errorf("failed to unescrow %s from elys escrow: %w", recoveryCfg.Coin, err) + } + + logger.Info("Withdrew stranded Elys escrow", "amount", recoveryCfg.Coin.String(), "from", escrowAddr.String(), "to", destAddr.String()) + return nil +} diff --git a/x/poa/keeper/msg_server_add_validator_test.go b/x/poa/keeper/msg_server_add_validator_test.go index c9e9ad3..eaf782d 100644 --- a/x/poa/keeper/msg_server_add_validator_test.go +++ b/x/poa/keeper/msg_server_add_validator_test.go @@ -56,7 +56,7 @@ func TestMsgServer_AddValidator(t *testing.T) { stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) - stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegations(ctx, gomock.Any(), gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) }, bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{