diff --git a/cmd/migration/main.go b/cmd/migration/main.go index 2d6f77d5..6dd16854 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -14,9 +14,13 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ethereumClient "github.com/multiversx/mx-bridge-eth-go/clients/ethereum" "github.com/multiversx/mx-bridge-eth-go/clients/ethereum/contract" + "github.com/multiversx/mx-bridge-eth-go/clients/ethereum/wrappers" + "github.com/multiversx/mx-bridge-eth-go/clients/gasManagement" + "github.com/multiversx/mx-bridge-eth-go/clients/gasManagement/factory" "github.com/multiversx/mx-bridge-eth-go/clients/multiversx" "github.com/multiversx/mx-bridge-eth-go/cmd/migration/disabled" "github.com/multiversx/mx-bridge-eth-go/config" + "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/executors/ethereum" chainCore "github.com/multiversx/mx-chain-core-go/core" logger "github.com/multiversx/mx-chain-logger-go" @@ -37,6 +41,12 @@ const ( var log = logger.GetOrCreate("main") +type internalComponents struct { + batch *ethereum.BatchInfo + cryptoHandler ethereumClient.CryptoHandler + ethClient *ethclient.Client +} + func main() { app := cli.NewApp() app.Name = "Funds migration CLI tool" @@ -80,15 +90,17 @@ func execute(ctx *cli.Context) error { operationMode := strings.ToLower(ctx.GlobalString(mode.Name)) switch operationMode { case signMode: - return generateAndSign(ctx, cfg) + + _, err = generateAndSign(ctx, cfg) + return err case executeMode: - //TODO: implement + return executeTransfer(ctx, cfg) } return fmt.Errorf("unknown execution mode: %s", operationMode) } -func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { +func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*internalComponents, error) { argsProxy := blockchain.ArgsProxy{ ProxyURL: cfg.MultiversX.NetworkAddress, SameScState: false, @@ -100,18 +112,18 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { } proxy, err := blockchain.NewProxy(argsProxy) if err != nil { - return err + return nil, err } dummyAddress := data.NewAddressFromBytes(bytes.Repeat([]byte{0x1}, 32)) multisigAddress, err := data.NewAddressFromBech32String(cfg.MultiversX.MultisigContractAddress) if err != nil { - return err + return nil, err } safeAddress, err := data.NewAddressFromBech32String(cfg.MultiversX.SafeContractAddress) if err != nil { - return err + return nil, err } argsMXClientDataGetter := multiversx.ArgsMXClientDataGetter{ @@ -123,12 +135,12 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { } mxDataGetter, err := multiversx.NewMXClientDataGetter(argsMXClientDataGetter) if err != nil { - return err + return nil, err } ethClient, err := ethclient.Dial(cfg.Eth.NetworkAddress) if err != nil { - return err + return nil, err } argsContractsHolder := ethereumClient.ArgsErc20SafeContractsHolder{ @@ -137,13 +149,13 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { } erc20ContractsHolder, err := ethereumClient.NewErc20SafeContractsHolder(argsContractsHolder) if err != nil { - return err + return nil, err } safeEthAddress := common.HexToAddress(cfg.Eth.SafeContractAddress) safeInstance, err := contract.NewERC20Safe(safeEthAddress, ethClient) if err != nil { - return err + return nil, err } argsCreator := ethereum.ArgsMigrationBatchCreator{ @@ -156,28 +168,28 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { creator, err := ethereum.NewMigrationBatchCreator(argsCreator) if err != nil { - return err + return nil, err } newSafeAddressString := ctx.GlobalString(newSafeAddress.Name) if len(newSafeAddressString) == 0 { - return fmt.Errorf("invalid new safe address for Ethereum") + return nil, fmt.Errorf("invalid new safe address for Ethereum") } newSafeAddressValue := common.HexToAddress(ctx.GlobalString(newSafeAddress.Name)) batchInfo, err := creator.CreateBatchInfo(context.Background(), newSafeAddressValue) if err != nil { - return err + return nil, err } val, err := json.MarshalIndent(batchInfo, "", " ") if err != nil { - return err + return nil, err } cryptoHandler, err := ethereumClient.NewCryptoHandler(cfg.Eth.PrivateKeyFile) if err != nil { - return err + return nil, err } log.Info("signing batch", "message hash", batchInfo.MessageHash.String(), @@ -185,7 +197,7 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { signature, err := cryptoHandler.Sign(batchInfo.MessageHash) if err != nil { - return err + return nil, err } log.Info("Migration .json file contents: \n" + string(val)) @@ -194,26 +206,99 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) error { jsonFilename = applyTimestamp(jsonFilename) err = os.WriteFile(jsonFilename, val, os.ModePerm) if err != nil { - return err + return nil, err } sigInfo := ðereum.SignatureInfo{ - PublicKey: cryptoHandler.GetAddress().String(), + Address: cryptoHandler.GetAddress().String(), MessageHash: batchInfo.MessageHash.String(), Signature: hex.EncodeToString(signature), } sigFilename := ctx.GlobalString(signatureJsonFile.Name) sigFilename = applyTimestamp(sigFilename) - sigFilename = applyPublicKey(sigFilename, sigInfo.PublicKey) + sigFilename = applyPublicKey(sigFilename, sigInfo.Address) val, err = json.MarshalIndent(sigInfo, "", " ") if err != nil { - return err + return nil, err } log.Info("Signature .json file contents: \n" + string(val)) - return os.WriteFile(sigFilename, val, os.ModePerm) + err = os.WriteFile(sigFilename, val, os.ModePerm) + if err != nil { + return nil, err + } + + return &internalComponents{ + batch: batchInfo, + cryptoHandler: cryptoHandler, + ethClient: ethClient, + }, nil +} + +func executeTransfer(ctx *cli.Context, cfg config.MigrationToolConfig) error { + components, err := generateAndSign(ctx, cfg) + if err != nil { + return err + } + + bridgeEthAddress := common.HexToAddress(cfg.Eth.MultisigContractAddress) + multiSigInstance, err := contract.NewBridge(bridgeEthAddress, components.ethClient) + if err != nil { + return err + } + + safeEthAddress := common.HexToAddress(cfg.Eth.SafeContractAddress) + safeInstance, err := contract.NewERC20Safe(safeEthAddress, components.ethClient) + if err != nil { + return err + } + + argsClientWrapper := wrappers.ArgsEthereumChainWrapper{ + StatusHandler: &disabled.StatusHandler{}, + MultiSigContract: multiSigInstance, + SafeContract: safeInstance, + BlockchainClient: components.ethClient, + } + ethereumChainWrapper, err := wrappers.NewEthereumChainWrapper(argsClientWrapper) + if err != nil { + return err + } + + gasStationConfig := cfg.Eth.GasStation + argsGasStation := gasManagement.ArgsGasStation{ + RequestURL: gasStationConfig.URL, + RequestPollingInterval: time.Duration(gasStationConfig.PollingIntervalInSeconds) * time.Second, + RequestRetryDelay: time.Duration(gasStationConfig.RequestRetryDelayInSeconds) * time.Second, + MaximumFetchRetries: gasStationConfig.MaxFetchRetries, + RequestTime: time.Duration(gasStationConfig.RequestTimeInSeconds) * time.Second, + MaximumGasPrice: gasStationConfig.MaximumAllowedGasPrice, + GasPriceSelector: core.EthGasPriceSelector(gasStationConfig.GasPriceSelector), + GasPriceMultiplier: gasStationConfig.GasPriceMultiplier, + } + gs, err := factory.CreateGasStation(argsGasStation, gasStationConfig.Enabled) + if err != nil { + return err + } + + args := ethereum.ArgsMigrationBatchExecutor{ + EthereumChainWrapper: ethereumChainWrapper, + CryptoHandler: components.cryptoHandler, + Batch: *components.batch, + Signatures: ethereum.LoadAllSignatures(log, configPath), + Logger: log, + GasHandler: gs, + TransferGasLimitBase: cfg.Eth.GasLimitBase, + TransferGasLimitForEach: cfg.Eth.GasLimitForEach, + } + + executor, err := ethereum.NewMigrationBatchExecutor(args) + if err != nil { + return err + } + + return executor.ExecuteTransfer(context.Background()) } func loadConfig(filepath string) (config.MigrationToolConfig, error) { diff --git a/executors/ethereum/common.go b/executors/ethereum/common.go index 8805664d..dcbb6923 100644 --- a/executors/ethereum/common.go +++ b/executors/ethereum/common.go @@ -27,7 +27,7 @@ type BatchInfo struct { // SignatureInfo is the struct holding signature info type SignatureInfo struct { - PublicKey string `json:"PublicKey"` + Address string `json:"Address"` MessageHash string `json:"MessageHash"` Signature string `json:"Signature"` } diff --git a/executors/ethereum/errors.go b/executors/ethereum/errors.go index 2706fde9..09d6b8a7 100644 --- a/executors/ethereum/errors.go +++ b/executors/ethereum/errors.go @@ -9,4 +9,10 @@ var ( errNilSafeContractWrapper = errors.New("nil safe contract wrapper") errWrongERC20AddressResponse = errors.New("wrong ERC20 address response") errNilLogger = errors.New("nil logger") + errNilCryptoHandler = errors.New("nil crypto handler") + errNilEthereumChainWrapper = errors.New("nil Ethereum chain wrapper") + errQuorumNotReached = errors.New("quorum not reached") + errInvalidSignature = errors.New("invalid signature") + errMultisigContractPaused = errors.New("multisig contract paused") + errNilGasHandler = errors.New("nil gas handler") ) diff --git a/executors/ethereum/interface.go b/executors/ethereum/interface.go index 5c3bc0f4..bf2a7a2f 100644 --- a/executors/ethereum/interface.go +++ b/executors/ethereum/interface.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" ) // TokensMapper can convert a token bytes from one chain to another @@ -32,3 +33,30 @@ type MvxDataGetter interface { GetERC20AddressForTokenId(ctx context.Context, tokenId []byte) ([][]byte, error) IsInterfaceNil() bool } + +// EthereumChainWrapper defines the operations of the Ethereum wrapper +type EthereumChainWrapper interface { + ExecuteTransfer(opts *bind.TransactOpts, tokens []common.Address, + recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, + signatures [][]byte) (*types.Transaction, error) + ChainID(ctx context.Context) (*big.Int, error) + BlockNumber(ctx context.Context) (uint64, error) + NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) + Quorum(ctx context.Context) (*big.Int, error) + GetRelayers(ctx context.Context) ([]common.Address, error) + IsPaused(ctx context.Context) (bool, error) +} + +// CryptoHandler defines the operations for a component that expose some crypto primitives +type CryptoHandler interface { + Sign(msgHash common.Hash) ([]byte, error) + GetAddress() common.Address + CreateKeyedTransactor(chainId *big.Int) (*bind.TransactOpts, error) + IsInterfaceNil() bool +} + +// GasHandler defines the component able to fetch the current gas price +type GasHandler interface { + GetCurrentGasPrice() (*big.Int, error) + IsInterfaceNil() bool +} diff --git a/executors/ethereum/migrationBatchExecutor.go b/executors/ethereum/migrationBatchExecutor.go new file mode 100644 index 00000000..adddc767 --- /dev/null +++ b/executors/ethereum/migrationBatchExecutor.go @@ -0,0 +1,294 @@ +package ethereum + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/multiversx/mx-chain-core-go/core/check" + logger "github.com/multiversx/mx-chain-logger-go" +) + +const ethSignatureSize = 64 + +// ArgsMigrationBatchExecutor is the argument for the NewMigrationBatchExecutor constructor +type ArgsMigrationBatchExecutor struct { + EthereumChainWrapper EthereumChainWrapper + CryptoHandler CryptoHandler + Batch BatchInfo + Signatures []SignatureInfo + Logger logger.Logger + GasHandler GasHandler + TransferGasLimitBase uint64 + TransferGasLimitForEach uint64 +} + +type migrationBatchExecutor struct { + ethereumChainWrapper EthereumChainWrapper + cryptoHandler CryptoHandler + batch BatchInfo + signatures []SignatureInfo + logger logger.Logger + gasHandler GasHandler + transferGasLimitBase uint64 + transferGasLimitForEach uint64 +} + +// NewMigrationBatchExecutor creates a new instance of type migrationBatchCreator that is able to execute the multisig transfer +func NewMigrationBatchExecutor(args ArgsMigrationBatchExecutor) (*migrationBatchExecutor, error) { + if check.IfNilReflect(args.EthereumChainWrapper) { + return nil, errNilEthereumChainWrapper + } + if check.IfNil(args.CryptoHandler) { + return nil, errNilCryptoHandler + } + if check.IfNil(args.Logger) { + return nil, errNilLogger + } + if check.IfNil(args.GasHandler) { + return nil, errNilGasHandler + } + + return &migrationBatchExecutor{ + ethereumChainWrapper: args.EthereumChainWrapper, + cryptoHandler: args.CryptoHandler, + batch: args.Batch, + signatures: args.Signatures, + logger: args.Logger, + gasHandler: args.GasHandler, + transferGasLimitBase: args.TransferGasLimitBase, + transferGasLimitForEach: args.TransferGasLimitForEach, + }, nil +} + +// ExecuteTransfer will try to execute the transfer +func (executor *migrationBatchExecutor) ExecuteTransfer(ctx context.Context) error { + isPaused, err := executor.ethereumChainWrapper.IsPaused(ctx) + if err != nil { + return fmt.Errorf("%w in executor.ExecuteTransfer", err) + } + if isPaused { + return fmt.Errorf("%w in executor.ExecuteTransfer", errMultisigContractPaused) + } + + relayers, err := executor.ethereumChainWrapper.GetRelayers(ctx) + if err != nil { + return err + } + + quorum, err := executor.ethereumChainWrapper.Quorum(ctx) + if err != nil { + return err + } + + signatures, err := executor.checkRelayersSigsAndQuorum(relayers, quorum) + if err != nil { + return err + } + + nonce, err := executor.getNonce(ctx, executor.cryptoHandler.GetAddress()) + if err != nil { + return err + } + + chainId, err := executor.ethereumChainWrapper.ChainID(ctx) + if err != nil { + return err + } + + auth, err := executor.cryptoHandler.CreateKeyedTransactor(chainId) + if err != nil { + return err + } + + gasPrice, err := executor.gasHandler.GetCurrentGasPrice() + if err != nil { + return err + } + + tokens, recipients, amounts, depositNonces, batchNonce := executor.extractArgumentsFromBatch() + + auth.Nonce = big.NewInt(nonce) + auth.Value = big.NewInt(0) + auth.GasLimit = executor.transferGasLimitBase + uint64(len(tokens))*executor.transferGasLimitForEach + auth.Context = ctx + auth.GasPrice = gasPrice + + tx, err := executor.ethereumChainWrapper.ExecuteTransfer(auth, tokens, recipients, amounts, depositNonces, batchNonce, signatures) + if err != nil { + return err + } + + txHash := tx.Hash().String() + executor.logger.Info("Executed transfer transaction", "batchID", executor.batch.BatchID, "hash", txHash) + + return nil +} + +func (executor *migrationBatchExecutor) getNonce(ctx context.Context, fromAddress common.Address) (int64, error) { + blockNonce, err := executor.ethereumChainWrapper.BlockNumber(ctx) + if err != nil { + return 0, fmt.Errorf("%w in getNonce, BlockNumber call", err) + } + + nonce, err := executor.ethereumChainWrapper.NonceAt(ctx, fromAddress, big.NewInt(int64(blockNonce))) + + return int64(nonce), err +} + +func (executor *migrationBatchExecutor) extractArgumentsFromBatch() ( + tokens []common.Address, + recipients []common.Address, + amounts []*big.Int, + nonces []*big.Int, + batchNonce *big.Int, +) { + tokens = make([]common.Address, 0, len(executor.batch.DepositsInfo)) + recipients = make([]common.Address, 0, len(executor.batch.DepositsInfo)) + amounts = make([]*big.Int, 0, len(executor.batch.DepositsInfo)) + nonces = make([]*big.Int, 0, len(executor.batch.DepositsInfo)) + batchNonce = big.NewInt(0).SetUint64(executor.batch.BatchID) + + newSafeContractAddress := common.HexToAddress(executor.batch.NewSafeContractAddress) + for _, deposit := range executor.batch.DepositsInfo { + tokens = append(tokens, deposit.ContractAddress) + recipients = append(recipients, newSafeContractAddress) + amounts = append(amounts, deposit.Amount) + nonces = append(nonces, big.NewInt(0).SetUint64(deposit.DepositNonce)) + } + + return +} + +func (executor *migrationBatchExecutor) checkRelayersSigsAndQuorum(relayers []common.Address, quorum *big.Int) ([][]byte, error) { + sameMessageHashSignatures := executor.getSameMessageHashSignatures() + validSignatures := executor.getValidSignatures(sameMessageHashSignatures) + return executor.checkQuorum(relayers, quorum, validSignatures) +} + +func (executor *migrationBatchExecutor) getSameMessageHashSignatures() []SignatureInfo { + filtered := make([]SignatureInfo, 0, len(executor.signatures)) + expectedMessageHash := executor.batch.MessageHash.String() + for _, sigInfo := range executor.signatures { + if sigInfo.MessageHash != expectedMessageHash { + executor.logger.Warn("found a signature info that was not carried on the same message hash", + "local message hash", executor.batch.MessageHash.String(), + "address", sigInfo.Address, "message hash", sigInfo.MessageHash) + + continue + } + + filtered = append(filtered, sigInfo) + } + + return filtered +} + +func (executor *migrationBatchExecutor) getValidSignatures(provided []SignatureInfo) []SignatureInfo { + filtered := make([]SignatureInfo, 0, len(provided)) + for _, sigInfo := range provided { + hash := common.HexToHash(sigInfo.MessageHash) + sig, err := hex.DecodeString(sigInfo.Signature) + if err != nil { + executor.logger.Warn("found a non valid signature info (can not unhex the signature)", + "address", sigInfo.Address, "message hash", sigInfo.MessageHash, "signature", sigInfo.Signature, "error", err) + continue + } + + err = verifySignature(hash, sig, common.HexToAddress(sigInfo.Address)) + if err != nil { + executor.logger.Warn("found a non valid signature info", + "address", sigInfo.Address, "message hash", sigInfo.MessageHash, "signature", sigInfo.Signature, "error", err) + continue + } + + filtered = append(filtered, sigInfo) + } + + return filtered +} + +func verifySignature(messageHash common.Hash, signature []byte, address common.Address) error { + pkBytes, err := crypto.Ecrecover(messageHash.Bytes(), signature) + if err != nil { + return err + } + + pk, err := crypto.UnmarshalPubkey(pkBytes) + if err != nil { + return err + } + + addressFromPk := crypto.PubkeyToAddress(*pk) + if addressFromPk.String() != address.String() { + // we need to check that the recovered public key matched the one provided in order to make sure + // that the signature, hash and public key match + return errInvalidSignature + } + + if len(signature) > ethSignatureSize { + // signatures might contain the recovery byte + signature = signature[:ethSignatureSize] + } + + sigOk := crypto.VerifySignature(pkBytes, messageHash.Bytes(), signature) + if !sigOk { + return errInvalidSignature + } + + return nil +} + +func (executor *migrationBatchExecutor) checkQuorum(relayers []common.Address, quorum *big.Int, signatures []SignatureInfo) ([][]byte, error) { + whitelistedRelayers := make(map[common.Address]SignatureInfo) + + for _, sigInfo := range signatures { + if !isWhitelistedRelayer(sigInfo, relayers) { + executor.logger.Warn("found a non whitelisted relayer", + "address", sigInfo.Address) + continue + } + + relayerAddress := common.HexToAddress(sigInfo.Address) + _, found := whitelistedRelayers[relayerAddress] + if found { + executor.logger.Warn("found a multiple relayer sig info, ignoring", + "address", sigInfo.Address) + continue + } + + whitelistedRelayers[relayerAddress] = sigInfo + } + + validSignatures := make([][]byte, 0, len(whitelistedRelayers)) + for _, sigInfo := range whitelistedRelayers { + sig, err := hex.DecodeString(sigInfo.Signature) + if err != nil { + return nil, fmt.Errorf("internal error: %w while decoding this string %s that should have been hexed encoded", err, sigInfo.Signature) + } + + validSignatures = append(validSignatures, sig) + executor.logger.Info("valid signature recorded for whitelisted relayer", "relayer", sigInfo.Address) + } + + if uint64(len(validSignatures)) < quorum.Uint64() { + return nil, fmt.Errorf("%w: minimum %d, got %d", errQuorumNotReached, quorum.Uint64(), len(validSignatures)) + } + + return validSignatures, nil +} + +func isWhitelistedRelayer(sigInfo SignatureInfo, relayers []common.Address) bool { + relayerAddress := common.HexToAddress(sigInfo.Address) + for _, relayer := range relayers { + if bytes.Equal(relayer.Bytes(), relayerAddress.Bytes()) { + return true + } + } + + return false +} diff --git a/executors/ethereum/migrationBatchExecutor_test.go b/executors/ethereum/migrationBatchExecutor_test.go new file mode 100644 index 00000000..dcc89094 --- /dev/null +++ b/executors/ethereum/migrationBatchExecutor_test.go @@ -0,0 +1,711 @@ +package ethereum + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "errors" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/multiversx/mx-bridge-eth-go/testsCommon" + "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var log = logger.GetOrCreate("executors/ethereum_test") + +func createMockArgsMigrationBatchExecutor() ArgsMigrationBatchExecutor { + return ArgsMigrationBatchExecutor{ + EthereumChainWrapper: &bridge.EthereumClientWrapperStub{}, + CryptoHandler: &bridge.CryptoHandlerStub{}, + Batch: BatchInfo{}, + Signatures: make([]SignatureInfo, 0), + Logger: log, + GasHandler: &testsCommon.GasHandlerStub{}, + TransferGasLimitBase: 100, + TransferGasLimitForEach: 10, + } +} + +func createPrivateKeys(tb testing.TB, num int) []*ecdsa.PrivateKey { + keys := make([]*ecdsa.PrivateKey, 0, num) + + for i := 0; i < num; i++ { + skBytes := make([]byte, 32) + _, _ = rand.Read(skBytes) + + privateKey, err := ethCrypto.HexToECDSA(hex.EncodeToString(skBytes)) + require.Nil(tb, err) + + keys = append(keys, privateKey) + } + + return keys +} + +func sign(tb testing.TB, sk *ecdsa.PrivateKey, msgHash common.Hash) []byte { + sig, err := ethCrypto.Sign(msgHash.Bytes(), sk) + require.Nil(tb, err) + + return sig +} + +func TestNewMigrationBatchExecutor(t *testing.T) { + t.Parallel() + + t.Run("nil Ethereum chain wrapper should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.EthereumChainWrapper = nil + + executor, err := NewMigrationBatchExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilEthereumChainWrapper, err) + }) + t.Run("nil crypto handler should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.CryptoHandler = nil + + executor, err := NewMigrationBatchExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilCryptoHandler, err) + }) + t.Run("nil logger should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Logger = nil + + executor, err := NewMigrationBatchExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilLogger, err) + }) + t.Run("nil gas handler should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.GasHandler = nil + + executor, err := NewMigrationBatchExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilGasHandler, err) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + executor, err := NewMigrationBatchExecutor(args) + assert.NotNil(t, executor) + assert.Nil(t, err) + }) +} + +func TestMigrationBatchExecutor_checkRelayersSigsAndQuorum(t *testing.T) { + t.Parallel() + + t.Run("quorum not satisfied should error", func(t *testing.T) { + t.Parallel() + + privateKeys := createPrivateKeys(t, 3) + + testMsgHash := common.HexToHash(strings.Repeat("1", 64)) + + signatures := []SignatureInfo{ + { + Address: ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[0], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[1], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[2], testMsgHash)), + }, + } + + args := createMockArgsMigrationBatchExecutor() + args.Batch = BatchInfo{ + MessageHash: testMsgHash, + } + args.Signatures = signatures + + executor, _ := NewMigrationBatchExecutor(args) + whitelistedRelayers := []common.Address{ + ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey), + } + + verifiedSigs, err := executor.checkRelayersSigsAndQuorum(whitelistedRelayers, big.NewInt(4)) + assert.ErrorIs(t, err, errQuorumNotReached) + assert.Contains(t, err.Error(), "minimum 4, got 3") + assert.Empty(t, verifiedSigs) + }) + t.Run("should work with wrong sig info elements", func(t *testing.T) { + t.Parallel() + + privateKeys := createPrivateKeys(t, 6) + + testMsgHash := common.HexToHash(strings.Repeat("1", 64)) + wrongMsgHash := common.HexToHash(strings.Repeat("2", 64)) + + correctSigForFifthElement := hex.EncodeToString(sign(t, privateKeys[5], testMsgHash)) + signatures := []SignatureInfo{ + // wrong message hash + { + Address: ethCrypto.PubkeyToAddress(privateKeys[4].PublicKey).String(), + MessageHash: wrongMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[4], wrongMsgHash)), + }, + // wrong signature: another message hash + { + Address: ethCrypto.PubkeyToAddress(privateKeys[5].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[5], wrongMsgHash)), + }, + // wrong signature: not a hex string + { + Address: ethCrypto.PubkeyToAddress(privateKeys[5].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: "not a hex string", + }, + // wrong signature: malformed signature + { + Address: ethCrypto.PubkeyToAddress(privateKeys[5].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: strings.Replace(correctSigForFifthElement, "1", "2", -1), + }, + // repeated good sig[1] + { + Address: ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[1], testMsgHash)), + }, + // good sigs + { + Address: ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[0], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[1], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[2], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[3].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[3], testMsgHash)), + }, + } + + args := createMockArgsMigrationBatchExecutor() + args.Batch = BatchInfo{ + MessageHash: testMsgHash, + } + args.Signatures = signatures + + executor, _ := NewMigrationBatchExecutor(args) + whitelistedRelayers := []common.Address{ + // all but private key[3] are whitelisted + ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[4].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[5].PublicKey), + } + + verifiedSigs, err := executor.checkRelayersSigsAndQuorum(whitelistedRelayers, big.NewInt(3)) + assert.Nil(t, err) + assert.Equal(t, 3, len(verifiedSigs)) + }) + t.Run("should work with correct sig elements", func(t *testing.T) { + t.Parallel() + + privateKeys := createPrivateKeys(t, 3) + + testMsgHash := common.HexToHash(strings.Repeat("1", 64)) + + signatures := []SignatureInfo{ + { + Address: ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[0], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[1], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[2], testMsgHash)), + }, + } + + args := createMockArgsMigrationBatchExecutor() + args.Batch = BatchInfo{ + MessageHash: testMsgHash, + } + args.Signatures = signatures + + executor, _ := NewMigrationBatchExecutor(args) + whitelistedRelayers := []common.Address{ + ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey), + } + + verifiedSigs, err := executor.checkRelayersSigsAndQuorum(whitelistedRelayers, big.NewInt(3)) + assert.Nil(t, err) + assert.Equal(t, 3, len(verifiedSigs)) + }) +} + +func TestMigrationBatchExecutor_ExecuteTransfer(t *testing.T) { + t.Parallel() + + testMsgHash := common.HexToHash(strings.Repeat("1", 64)) + newSafeContractAddress := common.HexToAddress("A6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c") + batchInfo := BatchInfo{ + OldSafeContractAddress: "3009d97FfeD62E57d444e552A9eDF9Ee6Bc8644c", + NewSafeContractAddress: newSafeContractAddress.String(), + BatchID: 4432, + MessageHash: testMsgHash, + DepositsInfo: []*DepositInfo{ + { + DepositNonce: 37, + Token: "tkn1", + ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn1Erc20Address), + Amount: big.NewInt(112), + AmountString: "112", + }, + { + DepositNonce: 38, + Token: "tkn2", + ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn2Erc20Address), + Amount: big.NewInt(113), + AmountString: "113", + }, + }, + } + + privateKeys := createPrivateKeys(t, 3) + signatures := []SignatureInfo{ + { + Address: ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[0], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[1], testMsgHash)), + }, + { + Address: ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey).String(), + MessageHash: testMsgHash.String(), + Signature: hex.EncodeToString(sign(t, privateKeys[2], testMsgHash)), + }, + } + + whitelistedRelayers := []common.Address{ + ethCrypto.PubkeyToAddress(privateKeys[0].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[1].PublicKey), + ethCrypto.PubkeyToAddress(privateKeys[2].PublicKey), + } + testBlockNumber := uint64(1000000) + senderNonce := uint64(3377) + testChainId := big.NewInt(2222) + testGasPrice := big.NewInt(112233) + + expectedTokens := []common.Address{ + common.BytesToAddress(tkn1Erc20Address), + common.BytesToAddress(tkn2Erc20Address), + } + expectedRecipients := []common.Address{ + newSafeContractAddress, + newSafeContractAddress, + } + expectedAmounts := []*big.Int{ + big.NewInt(112), + big.NewInt(113), + } + expectedNonces := []*big.Int{ + big.NewInt(37), + big.NewInt(38), + } + expectedSignatures := make([][]byte, 0, len(signatures)) + for _, sigInfo := range signatures { + sig, err := hex.DecodeString(sigInfo.Signature) + require.Nil(t, err) + expectedSignatures = append(expectedSignatures, sig) + } + expectedErr := errors.New("expected error") + + t.Run("is paused query errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + IsPausedCalled: func(ctx context.Context) (bool, error) { + return false, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("is paused should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + IsPausedCalled: func(ctx context.Context) (bool, error) { + return true, nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, errMultisigContractPaused) + }) + t.Run("get relayers errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return nil, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.Equal(t, expectedErr, err) + }) + t.Run("get quorum errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return nil, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.Equal(t, expectedErr, err) + }) + t.Run("checking the signatures and relayers errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures // no whitelisted relayers + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, errQuorumNotReached) + }) + t.Run("get block errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + BlockNumberCalled: func(ctx context.Context) (uint64, error) { + return 0, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("get nonce errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + NonceAtCalled: func(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + return 0, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("chain ID errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + ChainIDCalled: func(ctx context.Context) (*big.Int, error) { + return nil, expectedErr + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("create keyed transactor errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + args.CryptoHandler = &bridge.CryptoHandlerStub{ + CreateKeyedTransactorCalled: func(chainId *big.Int) (*bind.TransactOpts, error) { + return nil, expectedErr + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("get gas price errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Fail(t, "should have not called execute transfer") + + return nil, nil + }, + } + args.CryptoHandler = &bridge.CryptoHandlerStub{ + CreateKeyedTransactorCalled: func(chainId *big.Int) (*bind.TransactOpts, error) { + return &bind.TransactOpts{}, nil + }, + } + args.GasHandler = &testsCommon.GasHandlerStub{ + GetCurrentGasPriceCalled: func() (*big.Int, error) { + return nil, expectedErr + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("execute transfer errors should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + return nil, expectedErr + }, + } + args.CryptoHandler = &bridge.CryptoHandlerStub{ + CreateKeyedTransactorCalled: func(chainId *big.Int) (*bind.TransactOpts, error) { + return &bind.TransactOpts{}, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + err := executor.ExecuteTransfer(context.Background()) + assert.ErrorIs(t, err, expectedErr) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockArgsMigrationBatchExecutor() + args.Batch = batchInfo + args.Signatures = signatures + executeWasCalled := false + args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ + GetRelayersCalled: func(ctx context.Context) ([]common.Address, error) { + return whitelistedRelayers, nil + }, + BlockNumberCalled: func(ctx context.Context) (uint64, error) { + return testBlockNumber, nil + }, + NonceAtCalled: func(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + assert.Equal(t, big.NewInt(0).SetUint64(testBlockNumber), blockNumber) + return senderNonce, nil + }, + ChainIDCalled: func(ctx context.Context) (*big.Int, error) { + return testChainId, nil + }, + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + ExecuteTransferCalled: func(opts *bind.TransactOpts, tokens []common.Address, recipients []common.Address, amounts []*big.Int, nonces []*big.Int, batchNonce *big.Int, signatures [][]byte) (*types.Transaction, error) { + assert.Equal(t, big.NewInt(0).SetUint64(senderNonce), opts.Nonce) + assert.Equal(t, big.NewInt(0), opts.Value) + assert.Equal(t, uint64(100+10+10), opts.GasLimit) // base + 2 deposits + assert.Equal(t, testGasPrice, opts.GasPrice) + assert.Equal(t, expectedTokens, tokens) + assert.Equal(t, expectedRecipients, recipients) + assert.Equal(t, expectedAmounts, amounts) + assert.Equal(t, expectedNonces, nonces) + assert.Equal(t, big.NewInt(4432), batchNonce) + assert.ElementsMatch(t, expectedSignatures, signatures) + executeWasCalled = true + + txData := &types.LegacyTx{ + Nonce: 0, + Data: []byte("mocked data"), + } + tx := types.NewTx(txData) + + return tx, nil + }, + } + args.GasHandler = &testsCommon.GasHandlerStub{ + GetCurrentGasPriceCalled: func() (*big.Int, error) { + return testGasPrice, nil + }, + } + args.CryptoHandler = &bridge.CryptoHandlerStub{ + CreateKeyedTransactorCalled: func(chainId *big.Int) (*bind.TransactOpts, error) { + assert.Equal(t, testChainId, chainId) + return &bind.TransactOpts{}, nil + }, + } + + executor, _ := NewMigrationBatchExecutor(args) + + err := executor.ExecuteTransfer(context.Background()) + assert.Nil(t, err) + assert.True(t, executeWasCalled) + }) +} diff --git a/executors/ethereum/signatures.go b/executors/ethereum/signatures.go new file mode 100644 index 00000000..54fadf75 --- /dev/null +++ b/executors/ethereum/signatures.go @@ -0,0 +1,63 @@ +package ethereum + +import ( + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + + logger "github.com/multiversx/mx-chain-logger-go" +) + +const filesPattern = "0x*.json" + +// LoadAllSignatures can load all valid signatures from the specified directory +func LoadAllSignatures(logger logger.Logger, path string) []SignatureInfo { + filesContents, err := getAllFilesContents(path) + if err != nil { + logger.Warn(err.Error()) + return make([]SignatureInfo, 0) + } + + signatures := make([]SignatureInfo, 0, len(filesContents)) + for _, buff := range filesContents { + sigInfo := &SignatureInfo{} + err = json.Unmarshal(buff, sigInfo) + if err != nil { + logger.Warn("error unmarshalling to json", "error", err) + continue + } + + signatures = append(signatures, *sigInfo) + } + + return signatures +} + +func getAllFilesContents(dirPath string) ([][]byte, error) { + dirInfo, err := os.ReadDir(dirPath) + if err != nil { + return nil, fmt.Errorf("%w while fetching %s directory contents", err, dirPath) + } + + data := make([][]byte, 0, len(dirInfo)) + for _, di := range dirInfo { + if di.IsDir() { + continue + } + matched, errMatched := filepath.Match(filesPattern, di.Name()) + if errMatched != nil || !matched { + continue + } + + buff, errRead := os.ReadFile(path.Join(dirPath, di.Name())) + if errRead != nil { + continue + } + + data = append(data, buff) + } + + return data, nil +} diff --git a/executors/ethereum/signatures_test.go b/executors/ethereum/signatures_test.go new file mode 100644 index 00000000..de284867 --- /dev/null +++ b/executors/ethereum/signatures_test.go @@ -0,0 +1,29 @@ +package ethereum + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadAllSignatures(t *testing.T) { + t.Parallel() + + dirPath := "testdata" + sigs := LoadAllSignatures(log, dirPath) + + expectedSigs := []SignatureInfo{ + { + Address: "0x3FE464Ac5aa562F7948322F92020F2b668D543d8", + MessageHash: "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + Signature: "74a91b07c796d1fcb18517994f4b71fe5f1c10317e95c609eabac9e7dbfc517c3e9c402585774a7129e4b5bbfade40647afc52bb38cb2a4b63163cbe2577eee201", + }, + { + Address: "0xA6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c", + MessageHash: "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + Signature: "111222333", + }, + } + + assert.Equal(t, expectedSigs, sigs) +} diff --git a/executors/ethereum/testdata/0x3FE464Ac5aa562F7948322F92020F2b668D543d8-2024-09-05-15-34-44.json b/executors/ethereum/testdata/0x3FE464Ac5aa562F7948322F92020F2b668D543d8-2024-09-05-15-34-44.json new file mode 100755 index 00000000..9cc62245 --- /dev/null +++ b/executors/ethereum/testdata/0x3FE464Ac5aa562F7948322F92020F2b668D543d8-2024-09-05-15-34-44.json @@ -0,0 +1,5 @@ +{ + "Address": "0x3FE464Ac5aa562F7948322F92020F2b668D543d8", + "MessageHash": "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + "Signature": "74a91b07c796d1fcb18517994f4b71fe5f1c10317e95c609eabac9e7dbfc517c3e9c402585774a7129e4b5bbfade40647afc52bb38cb2a4b63163cbe2577eee201" +} diff --git a/executors/ethereum/testdata/0xA6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c-2024-09-05-15-34-44.json b/executors/ethereum/testdata/0xA6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c-2024-09-05-15-34-44.json new file mode 100755 index 00000000..b446e750 --- /dev/null +++ b/executors/ethereum/testdata/0xA6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c-2024-09-05-15-34-44.json @@ -0,0 +1,5 @@ +{ + "Address": "0xA6504Cc508889bbDBd4B748aFf6EA6b5D0d2684c", + "MessageHash": "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + "Signature": "111222333" +} diff --git a/executors/ethereum/testdata/0xDirectory/dummy b/executors/ethereum/testdata/0xDirectory/dummy new file mode 100644 index 00000000..e977405b --- /dev/null +++ b/executors/ethereum/testdata/0xDirectory/dummy @@ -0,0 +1 @@ +dummy, empty file diff --git a/executors/ethereum/testdata/0xbad-2024-09-05-15-34-44.json b/executors/ethereum/testdata/0xbad-2024-09-05-15-34-44.json new file mode 100755 index 00000000..eab6c718 --- /dev/null +++ b/executors/ethereum/testdata/0xbad-2024-09-05-15-34-44.json @@ -0,0 +1,4 @@ +{ + "Address": "0x3F", + "MessageHash": "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + "Signature": "74a91b07c796d1fcb18517994f4b71fe5f1c10317e95c609eabac9e7dbfc517c3e9c402585774a7129e4b5bbfade40647afc52bb38cb2a4b63163cbe2577eee201 diff --git a/executors/ethereum/testdata/migration-2024-09-05-15-34-44.json b/executors/ethereum/testdata/migration-2024-09-05-15-34-44.json new file mode 100755 index 00000000..450dae06 --- /dev/null +++ b/executors/ethereum/testdata/migration-2024-09-05-15-34-44.json @@ -0,0 +1,14 @@ +{ + "OldSafeContractAddress": "0x92A26975433A61CF1134802586aa669bAB8B69f3", + "NewSafeContractAddress": "0x1Ff78EB04d44a803E73c44FEf8790c5cAbD14596", + "BatchID": 3548, + "MessageHash": "0xc5b805c73d01e35e10a27a4cab86f096c976f0910ae23f5c6b307a823f0c49fb", + "DepositsInfo": [ + { + "DepositNonce": 4652, + "Token": "ETHUSDC-220753", + "ContractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Amount": "7086984513581" + } + ] +}