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") } diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go index 348e747a..3b374dc3 100644 --- a/client/x/evmstaking/keeper/abci.go +++ b/client/x/evmstaking/keeper/abci.go @@ -84,15 +84,39 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { } for _, entry := range unbondedEntries { + delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(entry.delegatorAddress) + if err != nil { + return nil, errors.Wrap(err, "delegator address from bech32") + } + + 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, + "validator", entry.validatorAddress, + "original_amount", entry.amount.String()) + + continue + } + + // 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, + "requested_amount", entry.amount.String(), + "spendable_amount", spendableAmount.String()) + } + 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") - } // Burn tokens from the delegator _, coins := IPTokenToBondCoin(entry.amount.BigInt()) err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins) diff --git a/client/x/evmstaking/keeper/abci_test.go b/client/x/evmstaking/keeper/abci_test.go index ccf26b94..dcf1dba4 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" @@ -245,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{ @@ -271,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")) @@ -363,6 +367,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 +377,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 } 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..8c59699f --- /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(); +>>>>>>> e522302 (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/Create3.sol b/contracts/src/deploy/Create3.sol deleted file mode 100644 index c7129f2a..00000000 --- a/contracts/src/deploy/Create3.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.23; - -import { CREATE3 } from "solmate/src/utils/CREATE3.sol"; - -/** - * @title Create3 - * @notice Factory for deploying contracts to deterministic addresses via CREATE3 Enables deploying - * contracts using CREATE3. Each deployer (msg.sender) has its own namespace for deployed - * addresses. - * @author zefram.eth - * @custom:attribution zefram.eth (https://github.com/ZeframLou/create3-factory/blob/main/src/CREATE3Factory.sol) - */ -contract Create3 { - /** - * @notice Deploys a contract using CREATE3 - * @dev The provided salt is hashed together with msg.sender to generate the final salt - * @param salt The deployer-specific salt for determining the deployed contract's address - * @param creationCode The creation code of the contract to deploy - * @return deployed The address of the deployed contract - */ - function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address deployed) { - // hash salt with the deployer address to give each deployer its own namespace - salt = keccak256(abi.encodePacked(msg.sender, salt)); - return CREATE3.deploy(salt, creationCode, msg.value); - } - - /** - * @notice Predicts the address of a deployed contract - * @dev The provided salt is hashed together with the deployer address to generate the final salt - * @param deployer The deployer account that will call deploy() - * @param salt The deployer-specific salt for determining the deployed contract's address - * @return deployed The address of the contract that will be deployed - */ - function getDeployed(address deployer, bytes32 salt) external view returns (address deployed) { - // hash salt with the deployer address to give each deployer its own namespace - salt = keccak256(abi.encodePacked(deployer, salt)); - return CREATE3.getDeployed(salt); - } -} 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))); + } + } diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 011b1ce0..9cad5406 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -14,7 +14,7 @@ import ( const ( VersionMajor = 0 // Major version component of the current release - VersionMinor = 10 // Minor 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 )