From 853288f6d36249734eb7a06502c25d3ad52640bf Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Sun, 29 Sep 2024 12:06:26 -0500 Subject: [PATCH 01/14] feat(x/evmstaking): max unbond withdrawal --- client/x/evmstaking/keeper/abci.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 348e747a..29862c38 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -84,17 +84,18 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { } for _, entry := range unbondedEntries { - log.Debug(ctx, "Adding undelegation to withdrawal queue", - "delegator", entry.delegatorAddress, - "validator", entry.validatorAddress, - "amount", entry.amount.String()) - delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(entry.delegatorAddress) if err != nil { return nil, errors.Wrap(err, "delegator address from bech32") } + + maxAmount := k.bankKeeper.GetBalance(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount + if entry.amount.LT(maxAmount) { + maxAmount = entry.amount + } + // Burn tokens from the delegator - _, coins := IPTokenToBondCoin(entry.amount.BigInt()) + _, coins := IPTokenToBondCoin(maxAmount.BigInt()) err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins) if err != nil { return nil, errors.Wrap(err, "send coins from account to module") @@ -104,6 +105,16 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { return nil, errors.Wrap(err, "burn coins") } + log.Debug(ctx, "Adding undelegation to withdrawal queue", + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, + "max_amount", maxAmount.String()) + if entry.amount.LT(maxAmount) { + log.Debug(ctx, "Undelegation amount is less than max amount", + "original_amount", entry.amount.String(), + "max_amount", maxAmount.String()) + } + // This should not produce error, as all delegations are done via the evmstaking module via EL. // However, we should gracefully handle in case Get fails. delEvmAddr, err := k.DelegatorMap.Get(ctx, entry.delegatorAddress) @@ -117,7 +128,7 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { entry.delegatorAddress, entry.validatorAddress, delEvmAddr, - entry.amount.Uint64(), + maxAmount.Uint64(), )) if err != nil { return nil, err From ffeccda0f24138ba0b20a10f81a5ca6533231457 Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Sun, 29 Sep 2024 12:17:22 -0500 Subject: [PATCH 02/14] chore(x/evmstaking): bump log level --- client/x/evmstaking/keeper/abci.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 29862c38..5b4ac411 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -110,7 +110,10 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { "validator", entry.validatorAddress, "max_amount", maxAmount.String()) if entry.amount.LT(maxAmount) { - log.Debug(ctx, "Undelegation amount is less than max amount", + log.Warn(ctx, "Undelegation amount is less than max amount", + errors.New("undelegation amount is less than max amount"), + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, "original_amount", entry.amount.String(), "max_amount", maxAmount.String()) } From b4f98359504ebad6c2b7ccbc3f5c976045e3b294 Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Sun, 29 Sep 2024 12:27:53 -0500 Subject: [PATCH 03/14] feat(x/evmstaking): spendable coin max amount with tests --- client/x/evmstaking/keeper/abci.go | 7 ++++--- client/x/evmstaking/keeper/abci_test.go | 6 ++++++ .../evmstaking/testutil/expected_keepers_mocks.go | 14 ++++++++++++++ client/x/evmstaking/types/expected_keepers.go | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 5b4ac411..587abf0f 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -89,8 +89,9 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { return nil, errors.Wrap(err, "delegator address from bech32") } - maxAmount := k.bankKeeper.GetBalance(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount - if entry.amount.LT(maxAmount) { + maxAmount := k.bankKeeper.SpendableCoin(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount + maxBounded := entry.amount.LT(maxAmount) + if maxBounded { maxAmount = entry.amount } @@ -109,7 +110,7 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { "delegator", entry.delegatorAddress, "validator", entry.validatorAddress, "max_amount", maxAmount.String()) - if entry.amount.LT(maxAmount) { + if maxBounded { log.Warn(ctx, "Undelegation amount is less than max amount", errors.New("undelegation amount is less than max amount"), "delegator", entry.delegatorAddress, diff --git a/client/x/evmstaking/keeper/abci_test.go b/client/x/evmstaking/keeper/abci_test.go index ccf26b94..d300159f 100644 --- a/client/x/evmstaking/keeper/abci_test.go +++ b/client/x/evmstaking/keeper/abci_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + sdkmath "cosmossdk.io/math" + abcitypes "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" dtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" @@ -363,6 +365,7 @@ func compareValUpdates(t *testing.T, expected, actual abcitypes.ValidatorUpdates // setupMaturedUnbonding creates matured unbondings for testing. func (s *TestSuite) setupMatureUnbondingDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt string, duration time.Duration) { + require := s.Require() pastHeader := ctx.BlockHeader() pastHeader.Time = pastHeader.Time.Add(-duration).Add(-time.Minute) pastCtx := ctx.WithBlockHeader(pastHeader) @@ -372,6 +375,9 @@ func (s *TestSuite) setupMatureUnbondingDelegation(ctx sdk.Context, delAddr sdk. // Mock staking.EndBlocker s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil) // Mock evmstaking.EndBlocker + amtInt, ok := sdkmath.NewIntFromString(amt) + require.True(ok) + s.BankKeeper.EXPECT().SpendableCoin(gomock.Any(), delAddr, sdk.DefaultBondDenom).Return(sdk.NewCoin(sdk.DefaultBondDenom, amtInt)) s.BankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), delAddr, types.ModuleName, gomock.Any()).Return(nil) s.BankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) } diff --git a/client/x/evmstaking/testutil/expected_keepers_mocks.go b/client/x/evmstaking/testutil/expected_keepers_mocks.go index cae3d6d8..33924c54 100644 --- a/client/x/evmstaking/testutil/expected_keepers_mocks.go +++ b/client/x/evmstaking/testutil/expected_keepers_mocks.go @@ -329,6 +329,20 @@ func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderPoo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderPool, recipientPool, amt) } +// SpendableCoin mocks base method. +func (m *MockBankKeeper) SpendableCoin(ctx context.Context, addr types0.AccAddress, denom string) types0.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableCoin", ctx, addr, denom) + ret0, _ := ret[0].(types0.Coin) + return ret0 +} + +// SpendableCoin indicates an expected call of SpendableCoin. +func (mr *MockBankKeeperMockRecorder) SpendableCoin(ctx, addr, denom any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableCoin", reflect.TypeOf((*MockBankKeeper)(nil).SpendableCoin), ctx, addr, denom) +} + // SpendableCoins mocks base method. func (m *MockBankKeeper) SpendableCoins(ctx context.Context, addr types0.AccAddress) types0.Coins { m.ctrl.T.Helper() diff --git a/client/x/evmstaking/types/expected_keepers.go b/client/x/evmstaking/types/expected_keepers.go index e4f5e20d..d3317890 100644 --- a/client/x/evmstaking/types/expected_keepers.go +++ b/client/x/evmstaking/types/expected_keepers.go @@ -40,6 +40,7 @@ type BankKeeper interface { GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins LockedCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins + SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin GetSupply(ctx context.Context, denom string) sdk.Coin SendCoinsFromModuleToModule(ctx context.Context, senderPool, recipientPool string, amt sdk.Coins) error } From 361dee1485a9c7cb05d625c9950a85ef202f5586 Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Sun, 29 Sep 2024 12:31:00 -0500 Subject: [PATCH 04/14] feat(x/evmstaking): pass zero amount --- client/x/evmstaking/keeper/abci.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 587abf0f..c3c55014 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -90,6 +90,16 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { } maxAmount := k.bankKeeper.SpendableCoin(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount + if maxAmount.IsZero() { + log.Warn(ctx, "No spendable coins for undelegation", + errors.New("no spendable coins for undelegation"), + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, + "original_amount", entry.amount.String()) + + continue + } + maxBounded := entry.amount.LT(maxAmount) if maxBounded { maxAmount = entry.amount From 2680685419cf8b925817eb981571eed76a13425e Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Sun, 29 Sep 2024 12:41:57 -0500 Subject: [PATCH 05/14] chore(x/evmstaking): move logs --- client/x/evmstaking/keeper/abci.go | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index c3c55014..9844ff15 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -100,11 +100,21 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { continue } - maxBounded := entry.amount.LT(maxAmount) - if maxBounded { + if entry.amount.LT(maxAmount) { maxAmount = entry.amount + log.Warn(ctx, "Undelegation amount is less than max amount", + errors.New("undelegation amount is less than max amount"), + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, + "original_amount", entry.amount.String(), + "max_amount", maxAmount.String()) } + log.Debug(ctx, "Adding undelegation to withdrawal queue", + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, + "max_amount", maxAmount.String()) + // Burn tokens from the delegator _, coins := IPTokenToBondCoin(maxAmount.BigInt()) err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins) @@ -116,19 +126,6 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { return nil, errors.Wrap(err, "burn coins") } - log.Debug(ctx, "Adding undelegation to withdrawal queue", - "delegator", entry.delegatorAddress, - "validator", entry.validatorAddress, - "max_amount", maxAmount.String()) - if maxBounded { - log.Warn(ctx, "Undelegation amount is less than max amount", - errors.New("undelegation amount is less than max amount"), - "delegator", entry.delegatorAddress, - "validator", entry.validatorAddress, - "original_amount", entry.amount.String(), - "max_amount", maxAmount.String()) - } - // This should not produce error, as all delegations are done via the evmstaking module via EL. // However, we should gracefully handle in case Get fails. delEvmAddr, err := k.DelegatorMap.Get(ctx, entry.delegatorAddress) From 15c3129a93d37245f03a3b2c8109b6f640e00b99 Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Sun, 29 Sep 2024 14:02:06 -0700 Subject: [PATCH 06/14] chore(release): finalize client 0.10.1 stable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 011b1ce0..4bf5301e 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 10 // Minor version component of the current release - VersionPatch = 1 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 10 // Minor version component of the current release + VersionPatch = 1 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From ef60858f1104ccc6029c57e9f1bd2684c9b6d923 Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Sun, 29 Sep 2024 14:05:41 -0700 Subject: [PATCH 07/14] chore(release): begin story v0.10.2 unstable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 4bf5301e..c8bcf637 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 10 // Minor version component of the current release - VersionPatch = 1 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 10 // Minor version component of the current release + VersionPatch = 2 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From a8c4d8b7574d54271ff04ac5e3d6cefa01a86fee Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Wed, 2 Oct 2024 18:48:05 -0700 Subject: [PATCH 08/14] chore(release): finalize client 0.10.2 stable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index c8bcf637..0daa846f 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 10 // Minor version component of the current release - VersionPatch = 2 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 10 // Minor version component of the current release + VersionPatch = 2 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From c193acbe48ce8f0d66bef7165947564ed4c3a5c1 Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Wed, 2 Oct 2024 18:49:52 -0700 Subject: [PATCH 09/14] chore(release): begin client 0.11.0 unstable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 0daa846f..8a8c128f 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 10 // Minor version component of the current release - VersionPatch = 2 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 11 // Minor version component of the current release + VersionPatch = 0 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From 1c21b32271dd8028e54de9d595e7d25d14214908 Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Tue, 1 Oct 2024 20:23:54 -0500 Subject: [PATCH 10/14] fix(x/evmstaking): endblock unbond branch check (#163) * fix(x/evmstaking): branch condition on max spendable * test(x/evmstaking): add expected call in tests --- client/x/evmstaking/keeper/abci.go | 24 +++++++++++++----------- client/x/evmstaking/keeper/abci_test.go | 2 ++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 9844ff15..3b374dc3 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -89,8 +89,8 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { return nil, errors.Wrap(err, "delegator address from bech32") } - maxAmount := k.bankKeeper.SpendableCoin(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount - if maxAmount.IsZero() { + spendableAmount := k.bankKeeper.SpendableCoin(ctx, delegatorAddr, sdk.DefaultBondDenom).Amount + if spendableAmount.IsZero() { log.Warn(ctx, "No spendable coins for undelegation", errors.New("no spendable coins for undelegation"), "delegator", entry.delegatorAddress, @@ -100,23 +100,25 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { continue } - if entry.amount.LT(maxAmount) { - maxAmount = entry.amount - log.Warn(ctx, "Undelegation amount is less than max amount", - errors.New("undelegation amount is less than max amount"), + // If the requested undelegation amount is greater than the spendable amount, set the real undelegation amount to + // the total spendable amount. + if entry.amount.GT(spendableAmount) { + entry.amount = spendableAmount + log.Warn(ctx, "Spendable amount is less than the requested undelegation amount", + errors.New("spendable amount is less than the requested undelegation amount"), "delegator", entry.delegatorAddress, "validator", entry.validatorAddress, - "original_amount", entry.amount.String(), - "max_amount", maxAmount.String()) + "requested_amount", entry.amount.String(), + "spendable_amount", spendableAmount.String()) } log.Debug(ctx, "Adding undelegation to withdrawal queue", "delegator", entry.delegatorAddress, "validator", entry.validatorAddress, - "max_amount", maxAmount.String()) + "amount", entry.amount.String()) // Burn tokens from the delegator - _, coins := IPTokenToBondCoin(maxAmount.BigInt()) + _, coins := IPTokenToBondCoin(entry.amount.BigInt()) err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins) if err != nil { return nil, errors.Wrap(err, "send coins from account to module") @@ -139,7 +141,7 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { entry.delegatorAddress, entry.validatorAddress, delEvmAddr, - maxAmount.Uint64(), + entry.amount.Uint64(), )) if err != nil { return nil, err diff --git a/client/x/evmstaking/keeper/abci_test.go b/client/x/evmstaking/keeper/abci_test.go index d300159f..dcf1dba4 100644 --- a/client/x/evmstaking/keeper/abci_test.go +++ b/client/x/evmstaking/keeper/abci_test.go @@ -247,6 +247,7 @@ func (s *TestSuite) TestEndBlock() { // Mock staking.EndBlocker s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil) // Mock evmstaking.EndBlocker + s.BankKeeper.EXPECT().SpendableCoin(gomock.Any(), delAddr, sdk.DefaultBondDenom).Return(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(10))) s.BankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), delAddr, types.ModuleName, gomock.Any()).Return(errors.New("failed to send coins to module")) return nil, []abcitypes.ValidatorUpdate{ @@ -273,6 +274,7 @@ func (s *TestSuite) TestEndBlock() { // Mock staking.EndBlocker s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil) // Mock evmstaking.EndBlocker + s.BankKeeper.EXPECT().SpendableCoin(gomock.Any(), delAddr, sdk.DefaultBondDenom).Return(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(10))) s.BankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), delAddr, types.ModuleName, gomock.Any()).Return(nil) s.BankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(errors.New("failed to burn coins")) From 9bb46f7166241b0c2fc2822dba39cd5682201319 Mon Sep 17 00:00:00 2001 From: Ramarti Date: Fri, 4 Oct 2024 16:59:25 -0300 Subject: [PATCH 11/14] feat(contracts): deploy script for iptokenslashing (#159) * deploy script for iptokenslashing * use solady CREATE3 to deploy IPTokenSlashing deterministically * fix create3 deployment * use create2 to deploy create3 deployer * use predeploy create3, make sure admin is not deployer --- contracts/package.json | 1 + contracts/pnpm-lock.yaml | 8 +++ contracts/script/DeployCore.s.sol | 70 +++++++++++++++++++ contracts/script/DeployIPTokenSlashing.s.sol | 72 ++++++++++++++++++++ contracts/src/deploy/ICreate3Deployer.sol | 15 ++++ contracts/test/deploy/Create3.t.sol | 13 +--- contracts/test/utils/Test.sol | 62 ++++++++++++++--- 7 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 contracts/script/DeployCore.s.sol create mode 100644 contracts/script/DeployIPTokenSlashing.s.sol create mode 100644 contracts/src/deploy/ICreate3Deployer.sol diff --git a/contracts/package.json b/contracts/package.json index 9411ca17..d726184c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,6 +34,7 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", + "solady": "^0.0.246", "solmate": "^6.2.0" } } diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 08901052..8c95d287 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@openzeppelin/contracts-upgradeable': specifier: 5.0.2 version: 5.0.2(@openzeppelin/contracts@5.0.2) + solady: + specifier: ^0.0.246 + version: 0.0.246 solmate: specifier: ^6.2.0 version: 6.2.0 @@ -655,6 +658,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + solady@0.0.246: + resolution: {integrity: sha512-SotcVbKUcz/d3aa4U58mIjxeOJwjSW5pLtPJJbpzm+N2F4iKfRrolMTW0M5nKwhrZcwqDnUGpNdlykZSvhd27g==} + solhint-community@4.0.0: resolution: {integrity: sha512-BERw3qYzkJE64EwvYrp2+iiTN8yAZOJ74FCiL4bTBp7v0JFUvRYCEGZKAqfHcfi/koKkzM6qThsJUceKm9vvfg==} hasBin: true @@ -1368,6 +1374,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + solady@0.0.246: {} + solhint-community@4.0.0(typescript@5.5.3): dependencies: '@solidity-parser/parser': 0.16.2 diff --git a/contracts/script/DeployCore.s.sol b/contracts/script/DeployCore.s.sol new file mode 100644 index 00000000..726be394 --- /dev/null +++ b/contracts/script/DeployCore.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { IPTokenStaking } from "../src/protocol/IPTokenStaking.sol"; +import { IPTokenSlashing } from "../src/protocol/IPTokenSlashing.sol"; +import { UpgradeEntrypoint } from "../src/protocol/UpgradeEntrypoint.sol"; + +/** + * @title DeployCore + * @dev A script + utilities to deploy the core contracts + */ +contract DeployCore is Script { + function run() public { + // TODO: read env + address protocolAccessManagerAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); + require(protocolAccessManagerAddr != address(0), "address not set"); + + uint256 deployerKey = vm.envUint("IPTOKENSTAKING_DEPLOYER_KEY"); + + vm.startBroadcast(deployerKey); + + address impl = address( + new IPTokenStaking( + 1 gwei, // stakingRounding + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ) + ); + IPTokenStaking ipTokenStaking = IPTokenStaking(address(new ERC1967Proxy(impl, ""))); + ipTokenStaking.initialize( + protocolAccessManagerAddr, + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 7 days // withdrawalAddressInterval + ); + + impl = address(new IPTokenSlashing(address(ipTokenStaking))); + IPTokenSlashing ipTokenSlashing = IPTokenSlashing(address(new ERC1967Proxy(impl, ""))); + ipTokenSlashing.initialize( + protocolAccessManagerAddr, + 1 ether // unjailFee + ); + + impl = address(new UpgradeEntrypoint()); + UpgradeEntrypoint upgradeEntrypoint = UpgradeEntrypoint(address(new ERC1967Proxy(impl, ""))); + upgradeEntrypoint.initialize(protocolAccessManagerAddr); + +<<<<<<< HEAD + vm.stopBroadcast(); + + console2.log("IPTokenStaking deployed at:", address(ipTokenStaking)); + console2.log("IPTokenSlashing deployed at:", address(ipTokenSlashing)); + console2.log("UpgradeEntrypoint deployed at:", address(upgradeEntrypoint)); +======= + console2.log("IPTokenStaking deployed at:", address(ipTokenStaking)); + console2.log("IPTokenSlashing deployed at:", address(ipTokenSlashing)); + console2.log("UpgradeEntrypoint deployed at:", address(upgradeEntrypoint)); + + vm.stopBroadcast(); +>>>>>>> 3f3ac51 (feat(contracts): deploy script for iptokenslashing (#159)) + } +} diff --git a/contracts/script/DeployIPTokenSlashing.s.sol b/contracts/script/DeployIPTokenSlashing.s.sol new file mode 100644 index 00000000..59e7291c --- /dev/null +++ b/contracts/script/DeployIPTokenSlashing.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { IPTokenSlashing } from "../src/protocol/IPTokenSlashing.sol"; +import { ICreate3Deployer } from "../src/deploy/ICreate3Deployer.sol"; + +/** + * @title DeployIPTokenSlashing + * @dev A script to deploy IPTokenSlashing for Illiad + */ +contract DeployIPTokenSlashing is Script { + // To run the script: + // - Dry run + // forge script script/DeployIPTokenSlashing.s.sol --fork-url + // + // - Deploy (OK for devnet) + // forge script script/DeployIPTokenSlashing.s.sol --fork-url --broadcast + // + // - Deploy and Verify (for testnet) + // forge script script/DeployIPTokenSlashing.s.sol --fork-url https://testnet.storyrpc.io --broadcast --verify --verifier blockscout --verifier-url https://testnet.storyscan.xyz/api\? + function run() public { + // Read env for admin address + address protocolAccessManagerAddr = vm.envAddress("ADMIN_ADDRESS"); + require(protocolAccessManagerAddr != address(0), "address not set"); + // Read env for deployer private key + uint256 deployerKey = vm.envUint("IPTOKENSTAKING_DEPLOYER_KEY"); + address deployer = vm.addr(deployerKey); + require(deployer != protocolAccessManagerAddr, "Deployer wallet can't be admin address"); + console2.log("deployer", deployer); + vm.startBroadcast(deployerKey); + + ICreate3Deployer c3Deployer = ICreate3Deployer(0x384a891dFDE8180b054f04D66379f16B7a678Ad6); + console2.log("Create3 deployer:", address(c3Deployer)); + + address ipTokenStaking = 0xCCcCcC0000000000000000000000000000000001; + + address impl = address(new IPTokenSlashing(ipTokenStaking)); + bytes memory initializationData = abi.encodeCall( + IPTokenSlashing.initialize, + ( + protocolAccessManagerAddr, + 1 ether // unjailFee + ) + ); + bytes memory creationCode = + abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(impl, initializationData)); + + bytes32 salt = keccak256(abi.encode("STORY", type(IPTokenSlashing).name)); + address predicted = c3Deployer.getDeployed(salt); + console2.log("IPTokenSlashing will be deployed at:", predicted); + IPTokenSlashing ipTokenSlashing = IPTokenSlashing(c3Deployer.deploy(salt, creationCode)); + + console2.log("IP_TOKEN_STAKING", address(ipTokenSlashing.IP_TOKEN_STAKING())); + console2.log("owner:", ipTokenSlashing.owner()); + console2.log("unjailFee:", ipTokenSlashing.unjailFee()); + + if (address(ipTokenSlashing) != predicted) { + revert("IPTokenSlashing mismatch"); + } + console2.log("IPTokenSlashing deployed at:", address(ipTokenSlashing)); + + vm.stopBroadcast(); + } + + +} diff --git a/contracts/src/deploy/ICreate3Deployer.sol b/contracts/src/deploy/ICreate3Deployer.sol new file mode 100644 index 00000000..f983e702 --- /dev/null +++ b/contracts/src/deploy/ICreate3Deployer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface ICreate3Deployer { + /// @notice Deploys a contract using CREATE3 + /// @param salt The salt to use for deployment + /// @param creationCode The contract creation code + /// @return deployed The address of the deployed contract + function deploy(bytes32 salt, bytes calldata creationCode) external payable returns (address); + + /// @notice Predicts the address of a deployed contract + /// @param salt The salt to use for deployment + /// @return deployed The address of the contract that will be deployed + function getDeployed(bytes32 salt) external view returns (address); +} diff --git a/contracts/test/deploy/Create3.t.sol b/contracts/test/deploy/Create3.t.sol index 0cff576f..d1ca9b4b 100644 --- a/contracts/test/deploy/Create3.t.sol +++ b/contracts/test/deploy/Create3.t.sol @@ -21,24 +21,17 @@ contract Create3Test is Test { bytes32 salt = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; bytes memory creationCode = type(Create3).creationCode; address deployed = create3.deploy(salt, creationCode); - address expected = create3.getDeployed(address(this), salt); + address expected = create3.getDeployed(salt); assertEq(deployed, expected); // Network shall generate the same address for the same deployer and salt. - vm.expectRevert("DEPLOYMENT_FAILED"); + vm.expectRevert(); deployed = create3.deploy(salt, creationCode); - // Network shall generate different addresses for different deployers. - address otherAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); - vm.prank(otherAddr); - deployed = create3.deploy(salt, creationCode); - expected = create3.getDeployed(otherAddr, salt); - assertEq(deployed, expected); - // Network shall generate different addresses for different salts. bytes32 otherSalt = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fedcba; deployed = create3.deploy(otherSalt, creationCode); - expected = create3.getDeployed(address(this), otherSalt); + expected = create3.getDeployed(otherSalt); assertEq(deployed, expected); } } diff --git a/contracts/test/utils/Test.sol b/contracts/test/utils/Test.sol index 28446b72..f8969bca 100644 --- a/contracts/test/utils/Test.sol +++ b/contracts/test/utils/Test.sol @@ -10,9 +10,7 @@ import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/trans import { IPTokenStaking } from "../../src/protocol/IPTokenStaking.sol"; import { IPTokenSlashing } from "../../src/protocol/IPTokenSlashing.sol"; import { UpgradeEntrypoint } from "../../src/protocol/UpgradeEntrypoint.sol"; -import { Predeploys } from "../../src/libraries/Predeploys.sol"; - -import { GenerateAlloc } from "../../script/GenerateAlloc.s.sol"; +import { Create3 } from "../../src/deploy/Create3.sol"; contract Test is ForgeTest { address internal admin = address(0x123); @@ -22,13 +20,55 @@ contract Test is ForgeTest { IPTokenSlashing internal ipTokenSlashing; UpgradeEntrypoint internal upgradeEntrypoint; - function setUp() virtual public { - GenerateAlloc initializer = new GenerateAlloc(); - initializer.disableStateDump(); // Faster tests. Don't call to verify JSON output - initializer.setAdminAddresses(upgradeAdmin, admin); - initializer.run(); - ipTokenStaking = IPTokenStaking(Predeploys.Staking); - ipTokenSlashing = IPTokenSlashing(Predeploys.Slashing); - upgradeEntrypoint = UpgradeEntrypoint(Predeploys.Upgrades); + function setUp() public virtual { + setStaking(); + setSlashing(); + // setUpgrade(); + } + + function setStaking() internal { + address impl = address( + new IPTokenStaking( + 1 gwei, // stakingRounding + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ) + ); + bytes memory initializer = abi.encodeCall(IPTokenStaking.initialize, (admin, 1 ether, 1 ether, 1 ether, 7 days)); + ipTokenStaking = IPTokenStaking(address(new ERC1967Proxy(impl, initializer))); } + + function setSlashing() internal { + require(address(ipTokenStaking) != address(0), "ipTokenStaking not set"); + + Create3 c3Deployer = new Create3(); + + address impl = address(new IPTokenSlashing(address(ipTokenStaking))); + bytes memory initializationData = abi.encodeCall( + IPTokenSlashing.initialize, + ( + admin, + 1 ether // unjailFee + ) + ); + bytes memory creationCode = + abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(impl, initializationData)); + bytes32 salt = keccak256(abi.encode("STORY", type(IPTokenSlashing).name)); + address predicted = c3Deployer.getDeployed(salt); + ipTokenSlashing = IPTokenSlashing(c3Deployer.deploy(salt, creationCode)); + + if (address(ipTokenSlashing) != predicted) { + revert("IPTokenSlashing mismatch"); + } + + } + + function setUpgrade() internal { + address impl = address(new UpgradeEntrypoint()); + + bytes memory initializer = abi.encodeWithSignature("initialize(address)", admin); + upgradeEntrypoint = UpgradeEntrypoint(address(new ERC1967Proxy(impl, initializer))); + } + } From 277a5bf5b0067bad6bd2c04874edb76616312773 Mon Sep 17 00:00:00 2001 From: Leeren Date: Fri, 4 Oct 2024 14:55:07 -0700 Subject: [PATCH 12/14] feat(cli): add unjail validator subcommand (#170) * feat(cli): add unjail validator subcommand * fix: use static predeploy ca in validator logic --- client/cmd/abi/IPTokenSlashing.abi.json | 369 ++++++++++++++++++++++++ client/cmd/flags.go | 12 + client/cmd/transaction.go | 19 ++ client/cmd/validator.go | 130 ++++++++- 4 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 client/cmd/abi/IPTokenSlashing.abi.json diff --git a/client/cmd/abi/IPTokenSlashing.abi.json b/client/cmd/abi/IPTokenSlashing.abi.json new file mode 100644 index 00000000..53b19a05 --- /dev/null +++ b/client/cmd/abi/IPTokenSlashing.abi.json @@ -0,0 +1,369 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "ipTokenStaking", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "IP_TOKEN_STAKING", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IPTokenStaking" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "accessManager", + "type": "address", + "internalType": "address" + }, + { + "name": "newUnjailFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setUnjailFee", + "inputs": [ + { + "name": "newUnjailFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unjail", + "inputs": [ + { + "name": "validatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "unjailFee", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unjailOnBehalf", + "inputs": [ + { + "name": "validatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unjail", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "validatorCmpPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UnjailFeeSet", + "inputs": [ + { + "name": "newUnjailFee", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/client/cmd/flags.go b/client/cmd/flags.go index b0de2471..2e539014 100644 --- a/client/cmd/flags.go +++ b/client/cmd/flags.go @@ -124,6 +124,11 @@ func bindRollbackFlags(cmd *cobra.Command, cfg *config.Config) { cmd.Flags().BoolVar(&cfg.RemoveBlock, "hard", false, "remove last block as well as state") } +func bindValidatorUnjailFlags(cmd *cobra.Command, cfg *unjailConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") +} + // Flag Validation func validateFlags(flags map[string]string) error { @@ -189,3 +194,10 @@ func validateValidatorUnstakeOnBehalfFlags(cfg stakeConfig) error { "unstake": cfg.StakeAmount, }) } + +func validateValidatorUnjailFlags(cfg unjailConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + }) +} diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go index b996350e..7d16f73b 100644 --- a/client/cmd/transaction.go +++ b/client/cmd/transaction.go @@ -16,6 +16,25 @@ import ( "github.com/piplabs/story/lib/errors" ) +func readContract(ctx context.Context, cfg baseConfig, contractAddress common.Address, data []byte) ([]byte, error) { + client, err := ethclient.Dial(cfg.RPC) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to Ethereum client") + } + + callMsg := ethereum.CallMsg{ + To: &contractAddress, + Data: data, + } + + result, err := client.CallContract(ctx, callMsg, nil) + if err != nil { + return nil, errors.Wrap(err, "contract call failed") + } + + return result, nil +} + func prepareAndSendTransaction(ctx context.Context, cfg baseConfig, contractAddress common.Address, value *big.Int, data []byte) error { client, err := ethclient.Dial(cfg.RPC) if err != nil { diff --git a/client/cmd/validator.go b/client/cmd/validator.go index 18e16763..10e47c49 100644 --- a/client/cmd/validator.go +++ b/client/cmd/validator.go @@ -11,24 +11,37 @@ import ( "os" "strings" + "github.com/decred/dcrd/dcrec/secp256k1" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/joho/godotenv" "github.com/spf13/cobra" + "github.com/piplabs/story/client/genutil/evm/predeploys" "github.com/piplabs/story/lib/errors" _ "embed" ) +type ContractType int + const ( - contractAddressHex = "0xCCcCcC0000000000000000000000000000000001" + STAKING ContractType = iota + SLASHING ) +type ContractInfo struct { + AddressHex string + ABI []byte +} + //go:embed abi/IPTokenStaking.abi.json var ipTokenStakingABI []byte +//go:embed abi/IPTokenSlashing.abi.json +var ipTokenSlashingABI []byte + type baseConfig struct { RPC string PrivateKey string @@ -43,6 +56,11 @@ type stakeConfig struct { StakeAmount string } +type unjailConfig struct { + baseConfig + ValidatorPubKey string +} + type operatorConfig struct { baseConfig Operator string @@ -65,6 +83,17 @@ type exportKeyConfig struct { ExportEVMKey bool } +var contracts = map[ContractType]ContractInfo{ + STAKING: { + AddressHex: predeploys.IPTokenStaking, + ABI: ipTokenStakingABI, + }, + SLASHING: { + AddressHex: predeploys.IPTokenSlashing, + ABI: ipTokenSlashingABI, + }, +} + func loadEnv() { err := godotenv.Load() if err != nil { @@ -89,6 +118,7 @@ func newValidatorCmds() *cobra.Command { newValidatorAddOperatorCmd(), newValidatorRemoveOperatorCmd(), newValidatorSetWithdrawalAddressCmd(), + newValidatorUnjailCmd(), ) return cmd @@ -283,6 +313,27 @@ func newValidatorKeyExportCmd() *cobra.Command { return cmd } +func newValidatorUnjailCmd() *cobra.Command { + var cfg unjailConfig + + cmd := &cobra.Command{ + Use: "unjail", + Short: "Unjail the validator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateValidatorUnjailFlags(cfg) }, + func(ctx context.Context) error { return unjail(ctx, cfg) }, + ), + } + + bindValidatorUnjailFlags(cmd, &cfg) + + return cmd +} + func runValidatorCommand( validate func() error, execute func(ctx context.Context) error, @@ -381,7 +432,7 @@ func createValidator(ctx context.Context, cfg createValidatorConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "createValidatorOnBehalf", stakeAmount, uncompressedPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "createValidatorOnBehalf", stakeAmount, uncompressedPubKeyBytes) if err != nil { return err } @@ -399,7 +450,7 @@ func setWithdrawalAddress(ctx context.Context, cfg withdrawalConfig) error { withdrawalAddress := common.HexToAddress(cfg.WithdrawalAddress) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "setWithdrawalAddress", big.NewInt(0), uncompressedPubKey, withdrawalAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "setWithdrawalAddress", big.NewInt(0), uncompressedPubKey, withdrawalAddress) if err != nil { return err } @@ -417,7 +468,7 @@ func addOperator(ctx context.Context, cfg operatorConfig) error { operatorAddress := common.HexToAddress(cfg.Operator) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "addOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "addOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) if err != nil { return err } @@ -435,7 +486,7 @@ func removeOperator(ctx context.Context, cfg operatorConfig) error { operatorAddress := common.HexToAddress(cfg.Operator) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "removeOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "removeOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) if err != nil { return err } @@ -461,7 +512,7 @@ func stake(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedPubKey, validatorPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedPubKey, validatorPubKeyBytes) if err != nil { return err } @@ -491,7 +542,7 @@ func stakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedDelegatorPubKeyBytes, validatorPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedDelegatorPubKeyBytes, validatorPubKeyBytes) if err != nil { return err } @@ -517,7 +568,7 @@ func unstake(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, validatorPubKeyBytes, unstakeAmount) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, validatorPubKeyBytes, unstakeAmount) if err != nil { return err } @@ -543,7 +594,7 @@ func unstakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstakeOnBehalf", big.NewInt(0), delegatorPubKeyBytes, validatorPubKeyBytes, unstakeAmount) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "unstakeOnBehalf", big.NewInt(0), delegatorPubKeyBytes, validatorPubKeyBytes, unstakeAmount) if err != nil { return err } @@ -553,9 +604,64 @@ func unstakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return nil } -func prepareAndExecuteTransaction(ctx context.Context, cfg *baseConfig, methodName string, value *big.Int, args ...any) error { - contractAddress := common.HexToAddress(contractAddressHex) - contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) +func unjail(ctx context.Context, cfg unjailConfig) error { + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) + if err != nil { + return errors.Wrap(err, "failed to decode base64 validator public key") + } + + if len(validatorPubKeyBytes) != secp256k1.PubKeyBytesLenCompressed { + return fmt.Errorf("invalid compressed public key length: %d", len(validatorPubKeyBytes)) + } + + contractABI, err := abi.JSON(strings.NewReader(string(contracts[SLASHING].ABI))) + if err != nil { + return err + } + + result, err := prepareAndReadContract(ctx, SLASHING, &cfg.baseConfig, "unjailFee") + if err != nil { + return err + } + + var unjailFee *big.Int + err = contractABI.UnpackIntoInterface(&unjailFee, "unjailFee", result) + if err != nil { + return errors.Wrap(err, "failed to unpack unjailFee") + } + + fmt.Printf("Unjail fee: %s\n", unjailFee.String()) + + err = prepareAndExecuteTransaction(ctx, SLASHING, &cfg.baseConfig, "unjailOnBehalf", unjailFee, validatorPubKeyBytes) + if err != nil { + return err + } + + fmt.Println("Validator successfully unjailed!") + + return nil +} + +func prepareAndReadContract(ctx context.Context, contractType ContractType, cfg *baseConfig, methodName string, args ...any) ([]byte, error) { + contractInfo := contracts[contractType] + contractAddress := common.HexToAddress(contractInfo.AddressHex) + contractABI, err := abi.JSON(strings.NewReader(string(contractInfo.ABI))) + if err != nil { + return nil, errors.Wrap(err, "failed to parse ABI") + } + + data, err := contractABI.Pack(methodName, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to pack data") + } + + return readContract(ctx, *cfg, contractAddress, data) +} + +func prepareAndExecuteTransaction(ctx context.Context, contractType ContractType, cfg *baseConfig, methodName string, value *big.Int, args ...any) error { + contractInfo := contracts[contractType] + contractAddress := common.HexToAddress(contractInfo.AddressHex) + contractABI, err := abi.JSON(strings.NewReader(string(contractInfo.ABI))) if err != nil { return errors.Wrap(err, "failed to parse ABI") } From 49405489f88efee6171a1355626da4e67d40b50f Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 4 Oct 2024 15:00:03 -0700 Subject: [PATCH 13/14] chore(release): finalize client 0.11.0 stable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 8a8c128f..e93d30dd 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 11 // Minor version component of the current release - VersionPatch = 0 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 11 // Minor version component of the current release + VersionPatch = 0 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From 3636ce5933b387fc661dc1c6173e71c82f0509cc Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 4 Oct 2024 15:02:35 -0700 Subject: [PATCH 14/14] chore(release): begin story client v0.11.1 unstable release --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index e93d30dd..9cad5406 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 11 // Minor version component of the current release - VersionPatch = 0 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 11 // Minor version component of the current release + VersionPatch = 1 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit.