diff --git a/app/upgrades.go b/app/upgrades.go index 54a4c99..b5eb39a 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -35,6 +35,7 @@ func (app *TacChainApp) RegisterUpgradeHandlers() { LiquidStakeKeeper: &app.LiquidStakeKeeper, BankKeeper: app.BankKeeper, Erc20Keeper: &app.Erc20Keeper, + StakingKeeper: app.StakingKeeper, } app.GetStoreKeys() // register all upgrade handlers diff --git a/app/upgrades/types.go b/app/upgrades/types.go index b40b013..a170249 100644 --- a/app/upgrades/types.go +++ b/app/upgrades/types.go @@ -14,6 +14,7 @@ import ( authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" consensusparamkeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" evmerc20keeper "github.com/cosmos/evm/x/erc20/keeper" @@ -31,6 +32,7 @@ type AppKeepers struct { LiquidStakeKeeper *liquidstakekeeper.Keeper BankKeeper bankkeeper.Keeper Erc20Keeper *evmerc20keeper.Keeper + StakingKeeper *stakingkeeper.Keeper } type ModuleManager interface { diff --git a/app/upgrades/v1.0.2/upgrades.go b/app/upgrades/v1.0.2/upgrades.go index 2e1df16..9556af4 100644 --- a/app/upgrades/v1.0.2/upgrades.go +++ b/app/upgrades/v1.0.2/upgrades.go @@ -4,20 +4,26 @@ package v102 import ( "context" + "errors" + "fmt" + "regexp" + "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" upgradetypes "cosmossdk.io/x/upgrade/types" "github.com/Asphere-xyz/tacchain/app/upgrades" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" evmerc20types "github.com/cosmos/evm/x/erc20/types" "github.com/ethereum/go-ethereum/common" "golang.org/x/crypto/sha3" - ) +var WhitelistAdminAddressNotFound = errors.New("failed to find whitelist admin address") + // UpgradeName defines the on-chain upgrade name const UpgradeName = "v1.0.2" @@ -30,14 +36,6 @@ var Upgrade = upgrades.Upgrade{ }, } -func generateAddressFromDenom(denom string) (common.Address, error) { - hash := sha3.NewLegacyKeccak256() - if _, err := hash.Write([]byte(denom)); err != nil { - return common.Address{}, err - } - return common.BytesToAddress(hash.Sum(nil)), nil -} - func CreateUpgradeHandler( mm upgrades.ModuleManager, configurator module.Configurator, @@ -69,6 +67,99 @@ func CreateUpgradeHandler( ak.Erc20Keeper.SetToken(sdkCtx, lsmTokenPair) + logger := sdkCtx.Logger() + params := ak.LiquidStakeKeeper.GetParams(sdkCtx) + adminAddress, err := getAdminAddressFromPlanInfo(plan.Info) + switch err { + case WhitelistAdminAddressNotFound, nil: + default: + logger.Error("invalid whitelist admin address in plan info", "error", err) + } + params.WhitelistAdminAddress = adminAddress + if err := ak.LiquidStakeKeeper.SetParams(sdkCtx, params); err != nil { + return newVM, fmt.Errorf("failed to set params for liquidstake module: %w", err) + } + + stakingParams, err := ak.StakingKeeper.GetParams(ctx) + if err != nil { + return newVM, err + } + + stakingParams.ValidatorBondFactor = stakingtypes.DefaultValidatorBondFactor + stakingParams.GlobalLiquidStakingCap = stakingtypes.DefaultGlobalLiquidStakingCap + stakingParams.ValidatorLiquidStakingCap = stakingtypes.DefaultValidatorLiquidStakingCap + + if err := ak.StakingKeeper.SetParams(ctx, stakingParams); err != nil { + return newVM, fmt.Errorf("failed to set params for staking module: %w", err) + } + + if err := initializeNewValidatorFields(ctx, ak); err != nil { + return newVM, fmt.Errorf("failed to initialize new validtor fields: %w", err) + } + return newVM, nil } } + +func generateAddressFromDenom(denom string) (common.Address, error) { + hash := sha3.NewLegacyKeccak256() + if _, err := hash.Write([]byte(denom)); err != nil { + return common.Address{}, err + } + return common.BytesToAddress(hash.Sum(nil)), nil +} + +func initializeNewValidatorFields(ctx context.Context, ak *upgrades.AppKeepers) error { + params, err := ak.StakingKeeper.GetParams(ctx) + if err != nil { + return fmt.Errorf("failed to get staking params: %w", err) + } + + validators, err := ak.StakingKeeper.GetValidators(ctx, params.MaxValidators) + if err != nil { + return fmt.Errorf("failed to get validators: %w", err) + } + + for _, validator := range validators { + newValidator := validator + newValidator.ValidatorBondShares = math.LegacyZeroDec() + newValidator.LiquidShares = math.LegacyZeroDec() + + err = ak.StakingKeeper.RemoveValidator(ctx, sdk.ValAddress(validator.OperatorAddress)) + if err != nil { + return fmt.Errorf("failed to remove validator: %w", err) + } + err = ak.StakingKeeper.SetValidator(ctx, newValidator) + if err != nil { + return fmt.Errorf("failed to set validator: %w", err) + } + err = ak.StakingKeeper.SetValidatorByConsAddr(ctx, newValidator) + if err != nil { + return fmt.Errorf("failed to set validator by consensus address: %w", err) + } + err = ak.StakingKeeper.SetValidatorByPowerIndex(ctx, newValidator) + if err != nil { + return fmt.Errorf("failed to set validator by power index: %w", err) + } + } + + return nil +} + +func getAdminAddressFromPlanInfo(info string) (string, error) { + key := "whitelist_admin_address" + addressPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + re := regexp.MustCompile(key + `:\s*(` + addressPrefix + `[a-zA-Z0-9]+)`) + matches := re.FindStringSubmatch(info) + + var addr string + if len(matches) > 1 { + addr = matches[1] + } else { + return "", WhitelistAdminAddressNotFound + } + if _, err := sdk.AccAddressFromBech32(addr); err != nil { + return "", fmt.Errorf("failed to validate whitelist admin address %s: %w", addr, err) + } + return addr, nil +} diff --git a/app/upgrades/v1.0.2/upgrades_test.go b/app/upgrades/v1.0.2/upgrades_test.go new file mode 100644 index 0000000..57abdd5 --- /dev/null +++ b/app/upgrades/v1.0.2/upgrades_test.go @@ -0,0 +1,37 @@ +package v102 + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestGetAdminAddressFromPlanInfo(t *testing.T) { + testCases := []struct { + info string + expected string + expectedError bool + }{ + { + info: "", + expected: "", + expectedError: true, + }, + { + info: "This proposal aims to add liquid stake possibility for the TAC network. whitelist_admin_address: tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr", + expected: "tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr", + expectedError: false, + }, + } + + sdk.GetConfig().SetBech32PrefixForAccount("tac", "tacpub") + + for _, tc := range testCases { + addr, err := getAdminAddressFromPlanInfo(tc.info) + if !tc.expectedError && err != nil { + t.Error(err) + } + require.Equal(t, tc.expected, addr) + } +} diff --git a/proposals/liquidstake_upgrade/1_admin_tx.json b/proposals/liquidstake_upgrade/1_admin_tx.json new file mode 100644 index 0000000..272760e --- /dev/null +++ b/proposals/liquidstake_upgrade/1_admin_tx.json @@ -0,0 +1,10 @@ +{ + "liquid_bond_denom": "stk/utac", + "unstake_fee_rate": "0.000000000000000000", + "lsm_disabled": true, + "min_liquid_stake_amount": "1000", + "cw_locked_pool_address": "", + "fee_account_address": "tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr", + "autocompound_fee_rate": "0.050000000000000000", + "whitelist_admin_address": "tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr" +} diff --git a/proposals/liquidstake_upgrade/2_admin_tx.json b/proposals/liquidstake_upgrade/2_admin_tx.json new file mode 100644 index 0000000..097cf73 --- /dev/null +++ b/proposals/liquidstake_upgrade/2_admin_tx.json @@ -0,0 +1,6 @@ +[ + { + "validator_address": "tacvaloper15lvhklny0khnwy7hgrxsxut6t6ku2cgkwu9tyt", + "target_weight": "10000" + } +] diff --git a/proposals/liquidstake_upgrade/README.md b/proposals/liquidstake_upgrade/README.md index a04da9c..571a4fc 100644 --- a/proposals/liquidstake_upgrade/README.md +++ b/proposals/liquidstake_upgrade/README.md @@ -1,9 +1,56 @@ ## Liquidstake upgrade **Steps:** -1. **Send gov tx to upgrade binary** - - [Localnet example](./example_step_1.json) -2. **Send gov tx to update module params** +1. Prepare proposal. - [Localnet example](./example_step_2.json) + **Important note**: If you want to set the `WhitelistAdminAddress`, you need to add the line `whitelist_admin_address: ` (e.g., tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr) to the `plan.info` variable in the governance transaction message. The address must be valid; otherwise, an error log will appear during the update, and you will have to submit the governance transaction again. +2. Send gov tx to upgrade binary. (e.g., [gov_transaction.json](./gov_transaction.json)) +3. Send admin tx to update module state. + + - Update params. (e.g., [1_admin_tx.json](./1_admin_tx.json)) + - Update validators. (e.g., [2_admin_tx.json](./2_admin_tx.json)) + +**Localnet Node Upgrade Checklist (v1.0.1 to v1.0.2)** + +1. Initial Setup and Governance Proposal + 1. Checkout to the old node version. + 2. Build the old version. + ```shell + make build + ``` + 3. Initialize and start the local network (with a short voting period). + ```shell + make localnet-init GOV_TIME_SECONDS=60 TACCHAIND=./build/tacchaind + make localnet-start TACCHAIND=./build/tacchaind + ``` + 4. (New Session) Submit the upgrade governance proposal (targeting height 400). + ```shell + ./build/tacchaind tx gov submit-proposal ./proposals/liquidstake_upgrade/gov_transaction.json --from validator --fees 200000000000000000utac --gas-adjustment 2 --gas 500000 + ``` + 5. Vote 'Yes' on the proposal. + ```shell + ./build/tacchaind tx gov vote 1 yes --from validator --fees 80000000000000000utac + ``` + 6. Wait for the node to stop automatically at upgrade height (400). +2. Upgrade the Node Software + 1. Checkout to the new node version. + 2. Build the new version. + ```shell + make build + ``` + 3. Restart the node using the new binary. + ```shell + make localnet-start TACCHAIND=./build/tacchaind + ``` + 4. Verification: Node starts successfully at height 400. +3. Post-Upgrade Liquidstake Module Configuration + 1. Update Liquidstake module parameters (e.g., setting the admin address). + ```shell + tacchaind tx liquidstake update-params ./proposals/liquidstake_upgrade/1_admin_tx.json --from validator --fees 80000000000000000utac + ``` + 2. Unpause/enable the Liquidstake module. + ```shell + tacchaind tx liquidstake pause-module false --from validator --fees 80000000000000000utac + ``` + +The upgraded node is successfully running, and the module is active. diff --git a/proposals/liquidstake_upgrade/example_step_1.json b/proposals/liquidstake_upgrade/example_step_1.json deleted file mode 100644 index b4bb053..0000000 --- a/proposals/liquidstake_upgrade/example_step_1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "messages": [ - { - "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", - "authority": "tac10d07y265gmmuvt4z0w9aw880jnsr700jlgpywe", - "plan": { - "name": "v1.0.2", - "height": "50", - "info": "Liquidstake module implementation", - "upgraded_client_state": null - } - } - ], - "metadata": "", - "deposit": "1000000000000000000utac", - "title": "Liquidstake upgrade", - "summary": "Liquidstake implementation", - "expedited": false -} diff --git a/proposals/liquidstake_upgrade/example_step_2.json b/proposals/liquidstake_upgrade/gov_transaction.json similarity index 66% rename from proposals/liquidstake_upgrade/example_step_2.json rename to proposals/liquidstake_upgrade/gov_transaction.json index fa9dffd..97cef14 100644 --- a/proposals/liquidstake_upgrade/example_step_2.json +++ b/proposals/liquidstake_upgrade/gov_transaction.json @@ -1,34 +1,15 @@ { "messages": [ { - "@type": "/tac.liquidstake.v1beta1.MsgUpdateParams", + "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", "authority": "tac10d07y265gmmuvt4z0w9aw880jnsr700jlgpywe", - "params": { - "liquid_bond_denom": "stk/utac", - "unstake_fee_rate": "0.000000000000000000", - "lsm_disabled": true, - "min_liquid_stake_amount": "1000", - "cw_locked_pool_address": "", - "fee_account_address": "tac1w2q3mashs2k4wcpqzs5q5xewnhnnr7wslr34safzvwqzvuqh3gjqn6xzrj", - "autocompound_fee_rate": "0.050000000000000000", - "whitelist_admin_address": "tac1w2q3mashs2k4wcpqzs5q5xewnhnnr7wslr34safzvwqzvuqh3gjqn6xzrj" + "plan": { + "name": "v1.0.2", + "height": "400", + "info": "This proposal aims to add liquid stake possibility for the TAC network. whitelist_admin_address: tac15lvhklny0khnwy7hgrxsxut6t6ku2cgknw79fr", + "upgraded_client_state": null } }, - { - "@type": "/tac.liquidstake.v1beta1.MsgUpdateWhitelistedValidators", - "authority": "tac10d07y265gmmuvt4z0w9aw880jnsr700jlgpywe", - "whitelisted_validators": [ - { - "validator_address": "tacvaloper15lvhklny0khnwy7hgrxsxut6t6ku2cgkwu9tyt", - "target_weight": "10000" - } - ] - }, - { - "@type": "/tac.liquidstake.v1beta1.MsgSetModulePaused", - "authority": "tac10d07y265gmmuvt4z0w9aw880jnsr700jlgpywe", - "is_paused": false - }, { "@type": "/cosmos.evm.vm.v1.MsgUpdateParams", "authority": "tac10d07y265gmmuvt4z0w9aw880jnsr700jlgpywe", @@ -90,9 +71,9 @@ } } ], - "metadata": "", + "metadata": "https://github.com/TacBuild/tacchain/pull/61", "deposit": "1000000000000000000utac", - "title": "Update Liquidstake params", - "summary": "Liquidstake params", + "title": "Add liquid staking functionality to TAC network", + "summary": "The main liquid stake module is compatible with the existing staking and ERC-20 modules of the TAC node, introducing liquid staking tokens (gTAC) that are automatically represented as ERC-20 assets. The second module expands the capabilities of liquid staking and allows users to transfer staked funds to LST bypassing an unbound staking period", "expedited": false }