diff --git a/command/bridge/bridge.go b/command/bridge/bridge.go index 21fae7b60f..2fd8ef2c30 100644 --- a/command/bridge/bridge.go +++ b/command/bridge/bridge.go @@ -5,9 +5,11 @@ import ( depositERC1155 "github.com/0xPolygon/polygon-edge/command/bridge/deposit/erc1155" depositERC20 "github.com/0xPolygon/polygon-edge/command/bridge/deposit/erc20" + depositERC721 "github.com/0xPolygon/polygon-edge/command/bridge/deposit/erc721" "github.com/0xPolygon/polygon-edge/command/bridge/exit" withdrawERC1155 "github.com/0xPolygon/polygon-edge/command/bridge/withdraw/erc1155" withdrawERC20 "github.com/0xPolygon/polygon-edge/command/bridge/withdraw/erc20" + withdrawERC721 "github.com/0xPolygon/polygon-edge/command/bridge/withdraw/erc721" ) // GetCommand creates "bridge" helper command @@ -26,10 +28,14 @@ func registerSubcommands(baseCmd *cobra.Command) { baseCmd.AddCommand( // bridge deposit-erc20 depositERC20.GetCommand(), + // bridge deposit-erc721 + depositERC721.GetCommand(), // bridge deposit-erc1155 depositERC1155.GetCommand(), // bridge withdraw-erc20 withdrawERC20.GetCommand(), + // bridge withdraw-erc721 + withdrawERC721.GetCommand(), // bridge withdraw-erc1155 withdrawERC1155.GetCommand(), // bridge exit diff --git a/command/bridge/common/params.go b/command/bridge/common/params.go index 013e57e136..5642c3043b 100644 --- a/command/bridge/common/params.go +++ b/command/bridge/common/params.go @@ -55,6 +55,23 @@ func (bp *ERC20BridgeParams) Validate() error { return nil } +type ERC721BridgeParams struct { + *BridgeParams + TokenIDs []string +} + +func NewERC721BridgeParams() *ERC721BridgeParams { + return &ERC721BridgeParams{BridgeParams: &BridgeParams{}} +} + +func (bp *ERC721BridgeParams) Validate() error { + if len(bp.Receivers) != len(bp.TokenIDs) { + return errInconsistentTokenIds + } + + return nil +} + type ERC1155BridgeParams struct { *BridgeParams Amounts []string diff --git a/command/bridge/deposit/erc1155/deposit_erc1155.go b/command/bridge/deposit/erc1155/deposit_erc1155.go index f5d07aaa85..5d6e73e873 100644 --- a/command/bridge/deposit/erc1155/deposit_erc1155.go +++ b/command/bridge/deposit/erc1155/deposit_erc1155.go @@ -82,7 +82,7 @@ func GetCommand() *cobra.Command { depositCmd.Flags().StringVar( &dp.JSONRPCAddr, common.JSONRPCFlag, - "http://127.0.0.1:8545", + txrelayer.DefaultRPCAddress, "the JSON RPC root chain endpoint", ) diff --git a/command/bridge/deposit/erc20/deposit_erc20.go b/command/bridge/deposit/erc20/deposit_erc20.go index 78940c3c1a..62886037d1 100644 --- a/command/bridge/deposit/erc20/deposit_erc20.go +++ b/command/bridge/deposit/erc20/deposit_erc20.go @@ -76,7 +76,7 @@ func GetCommand() *cobra.Command { depositCmd.Flags().StringVar( &dp.JSONRPCAddr, common.JSONRPCFlag, - "http://127.0.0.1:8545", + txrelayer.DefaultRPCAddress, "the JSON RPC root chain endpoint", ) diff --git a/command/bridge/deposit/erc721/deposit_erc721.go b/command/bridge/deposit/erc721/deposit_erc721.go new file mode 100644 index 0000000000..663acadd59 --- /dev/null +++ b/command/bridge/deposit/erc721/deposit_erc721.go @@ -0,0 +1,294 @@ +package deposit + +import ( + "bytes" + "fmt" + "math/big" + "strings" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/bridge/common" + cmdHelper "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/command/rootchain/helper" + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + "github.com/0xPolygon/polygon-edge/txrelayer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/spf13/cobra" + "github.com/umbracle/ethgo" +) + +type depositERC721Params struct { + *common.ERC721BridgeParams + testMode bool +} + +var ( + dp *depositERC721Params = &depositERC721Params{ERC721BridgeParams: common.NewERC721BridgeParams()} +) + +func GetCommand() *cobra.Command { + depositCmd := &cobra.Command{ + Use: "deposit-erc721", + Short: "Deposits ERC721 tokens from the root chain to the child chain", + PreRunE: preRunCommand, + Run: runCommand, + } + + depositCmd.Flags().StringVar( + &dp.SenderKey, + common.SenderKeyFlag, + "", + "hex encoded private key of the account which sends rootchain deposit transactions", + ) + + depositCmd.Flags().StringSliceVar( + &dp.Receivers, + common.ReceiversFlag, + nil, + "receiving accounts addresses on child chain", + ) + + depositCmd.Flags().StringSliceVar( + &dp.TokenIDs, + common.TokenIDsFlag, + nil, + "token ids that are sent to the receivers accounts", + ) + + depositCmd.Flags().StringVar( + &dp.TokenAddr, + common.RootTokenFlag, + "", + "root ERC 721 token address", + ) + + depositCmd.Flags().StringVar( + &dp.PredicateAddr, + common.RootPredicateFlag, + "", + "root ERC 721 token predicate address", + ) + + depositCmd.Flags().StringVar( + &dp.JSONRPCAddr, + common.JSONRPCFlag, + txrelayer.DefaultRPCAddress, + "the JSON RPC root chain endpoint", + ) + + depositCmd.Flags().BoolVar( + &dp.testMode, + helper.TestModeFlag, + false, + "test indicates whether depositor is hardcoded test account "+ + "(in that case tokens are minted to it, so it is able to make deposits)", + ) + + _ = depositCmd.MarkFlagRequired(common.ReceiversFlag) + _ = depositCmd.MarkFlagRequired(common.TokenIDsFlag) + _ = depositCmd.MarkFlagRequired(common.RootTokenFlag) + _ = depositCmd.MarkFlagRequired(common.RootPredicateFlag) + + depositCmd.MarkFlagsMutuallyExclusive(helper.TestModeFlag, common.SenderKeyFlag) + + return depositCmd +} + +func preRunCommand(_ *cobra.Command, _ []string) error { + return dp.Validate() +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + depositorKey, err := helper.GetRootchainPrivateKey(dp.SenderKey) + if err != nil { + outputter.SetError(fmt.Errorf("failed to initialize depositor private key: %w", err)) + } + + depositorAddr := depositorKey.Address() + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(dp.JSONRPCAddr)) + if err != nil { + outputter.SetError(fmt.Errorf("failed to initialize rootchain tx relayer: %w", err)) + + return + } + + receivers := make([]ethgo.Address, len(dp.Receivers)) + tokenIDs := make([]*big.Int, len(dp.Receivers)) + + for i, tokenIDRaw := range dp.TokenIDs { + tokenIDRaw := tokenIDRaw + + tokenID, err := types.ParseUint256orHex(&tokenIDRaw) + if err != nil { + outputter.SetError(fmt.Errorf("failed to decode provided token id %s: %w", tokenIDRaw, err)) + + return + } + + receivers[i] = ethgo.Address(types.StringToAddress(dp.Receivers[i])) + tokenIDs[i] = tokenID + } + + if dp.testMode { + for i := 0; i < len(tokenIDs); i++ { + mintTxn, err := createMintTxn(types.Address(depositorAddr), types.Address(depositorAddr)) + if err != nil { + outputter.SetError(fmt.Errorf("mint transaction creation failed: %w", err)) + + return + } + + receipt, err := txRelayer.SendTransaction(mintTxn, depositorKey) + if err != nil { + outputter.SetError(fmt.Errorf("failed to send mint transaction to depositor %s", depositorAddr)) + + return + } + + if receipt.Status == uint64(types.ReceiptFailed) { + outputter.SetError(fmt.Errorf("failed to mint tokens to depositor %s", depositorAddr)) + + return + } + } + } + + approveTxn, err := createApproveERC721PredicateTxn(types.StringToAddress(dp.PredicateAddr), + types.StringToAddress(dp.TokenAddr)) + if err != nil { + outputter.SetError(fmt.Errorf("failed to approve predicate: %w", err)) + + return + } + + receipt, err := txRelayer.SendTransaction(approveTxn, depositorKey) + if err != nil { + outputter.SetError(fmt.Errorf("failed to send root erc 721 approve transaction")) + + return + } + + if receipt.Status == uint64(types.ReceiptFailed) { + outputter.SetError(fmt.Errorf("failed to approve root erc 721 predicate")) + + return + } + + // deposit tokens + depositTxn, err := createDepositTxn(depositorAddr, receivers, tokenIDs) + if err != nil { + outputter.SetError(fmt.Errorf("failed to create tx input: %w", err)) + + return + } + + receipt, err = txRelayer.SendTransaction(depositTxn, depositorKey) + if err != nil { + outputter.SetError(fmt.Errorf("sending deposit transactions failed (receivers: %s, tokenIDs: %s): %w", + strings.Join(dp.Receivers, ", "), strings.Join(dp.TokenIDs, ", "), err)) + + return + } + + if receipt.Status == uint64(types.ReceiptFailed) { + outputter.SetError(fmt.Errorf("sending deposit transactions failed (receivers: %s, tokenIDs: %s)", + strings.Join(dp.Receivers, ", "), strings.Join(dp.TokenIDs, ", "))) + + return + } + + outputter.SetCommandResult( + &depositERC721Result{ + Sender: depositorAddr.String(), + Receivers: dp.Receivers, + TokenIDs: dp.TokenIDs, + }) +} + +// createDepositTxn encodes parameters for deposit fnction on rootchain predicate contract +func createDepositTxn(sender ethgo.Address, + receivers []ethgo.Address, tokenIDs []*big.Int) (*ethgo.Transaction, error) { + depositToRoot := &contractsapi.DepositBatchRootERC721PredicateFn{ + RootToken: types.StringToAddress(dp.TokenAddr), + Receivers: receivers, + TokenIDs: tokenIDs, + } + + input, err := depositToRoot.EncodeAbi() + if err != nil { + return nil, fmt.Errorf("failed to encode provided parameters: %w", err) + } + + addr := ethgo.Address(types.StringToAddress(dp.PredicateAddr)) + + return ðgo.Transaction{ + From: sender, + To: &addr, + Input: input, + }, nil +} + +// createMintTxn encodes parameters for mint function on rootchain token contract +func createMintTxn(sender, receiver types.Address) (*ethgo.Transaction, error) { + mintFn := &contractsapi.MintRootERC721Fn{ + To: receiver, + } + + input, err := mintFn.EncodeAbi() + if err != nil { + return nil, fmt.Errorf("failed to encode provided parameters: %w", err) + } + + addr := ethgo.Address(types.StringToAddress(dp.TokenAddr)) + + return ðgo.Transaction{ + From: ethgo.Address(sender), + To: &addr, + Input: input, + }, nil +} + +// createApproveERC721PredicateTxn sends approve transaction +func createApproveERC721PredicateTxn(rootERC721Predicate, rootERC721Token types.Address) (*ethgo.Transaction, error) { + approveFnParams := &contractsapi.SetApprovalForAllRootERC721Fn{ + Operator: rootERC721Predicate, + Approved: true, + } + + input, err := approveFnParams.EncodeAbi() + if err != nil { + return nil, fmt.Errorf("failed to encode parameters for RootERC721.approve. error: %w", err) + } + + addr := ethgo.Address(rootERC721Token) + + return ðgo.Transaction{ + To: &addr, + Input: input, + }, nil +} + +type depositERC721Result struct { + Sender string `json:"sender"` + Receivers []string `json:"receivers"` + TokenIDs []string `json:"tokenIDs"` +} + +func (r *depositERC721Result) GetOutput() string { + var buffer bytes.Buffer + + vals := make([]string, 0, 3) + vals = append(vals, fmt.Sprintf("Sender|%s", r.Sender)) + vals = append(vals, fmt.Sprintf("Receivers|%s", strings.Join(r.Receivers, ", "))) + vals = append(vals, fmt.Sprintf("TokenIDs|%s", strings.Join(r.TokenIDs, ", "))) + + buffer.WriteString("\n[DEPOSIT ERC 721]\n") + buffer.WriteString(cmdHelper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/bridge/exit/exit.go b/command/bridge/exit/exit.go index 6b5b0fe659..5527f44d49 100644 --- a/command/bridge/exit/exit.go +++ b/command/bridge/exit/exit.go @@ -79,7 +79,7 @@ func GetCommand() *cobra.Command { exitCmd.Flags().StringVar( &ep.rootJSONRPCAddr, rootJSONRPCFlag, - "http://127.0.0.1:8545", + txrelayer.DefaultRPCAddress, "the JSON RPC root chain endpoint", ) diff --git a/command/bridge/withdraw/erc721/withdraw_erc721.go b/command/bridge/withdraw/erc721/withdraw_erc721.go new file mode 100644 index 0000000000..18bd3cd96f --- /dev/null +++ b/command/bridge/withdraw/erc721/withdraw_erc721.go @@ -0,0 +1,236 @@ +package withdraw + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/bridge/common" + cmdHelper "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/txrelayer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/spf13/cobra" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/wallet" +) + +var ( + wp *common.ERC721BridgeParams = common.NewERC721BridgeParams() +) + +func GetCommand() *cobra.Command { + withdrawCmd := &cobra.Command{ + Use: "withdraw-erc721", + Short: "Withdraws ERC 721 tokens from the child chain to the root chain", + PreRunE: preRun, + Run: run, + } + + withdrawCmd.Flags().StringVar( + &wp.SenderKey, + common.SenderKeyFlag, + "", + "withdraw transaction sender hex-encoded private key", + ) + + withdrawCmd.Flags().StringSliceVar( + &wp.Receivers, + common.ReceiversFlag, + nil, + "receiving accounts addresses on the root chain", + ) + + withdrawCmd.Flags().StringSliceVar( + &wp.TokenIDs, + common.TokenIDsFlag, + nil, + "tokens ids to send to receiving accounts", + ) + + withdrawCmd.Flags().StringVar( + &wp.PredicateAddr, + common.ChildPredicateFlag, + contracts.ChildERC721PredicateContract.String(), + "ERC 721 child chain predicate address", + ) + + withdrawCmd.Flags().StringVar( + &wp.TokenAddr, + common.ChildTokenFlag, + contracts.ChildERC721Contract.String(), + "ERC 721 child chain token address", + ) + + withdrawCmd.Flags().StringVar( + &wp.JSONRPCAddr, + common.JSONRPCFlag, + "http://127.0.0.1:9545", + "the JSON RPC child chain endpoint", + ) + + _ = withdrawCmd.MarkFlagRequired(common.SenderKeyFlag) + _ = withdrawCmd.MarkFlagRequired(common.ReceiversFlag) + _ = withdrawCmd.MarkFlagRequired(common.TokenIDsFlag) + + return withdrawCmd +} + +func preRun(cmd *cobra.Command, _ []string) error { + return wp.Validate() +} + +func run(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + senderKeyRaw, err := hex.DecodeString(wp.SenderKey) + if err != nil { + outputter.SetError(fmt.Errorf("failed to decode sender private key: %w", err)) + + return + } + + senderAccount, err := wallet.NewWalletFromPrivKey(senderKeyRaw) + if err != nil { + outputter.SetError(err) + + return + } + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(wp.JSONRPCAddr)) + if err != nil { + outputter.SetError(fmt.Errorf("could not create child chain tx relayer: %w", err)) + + return + } + + receivers := make([]ethgo.Address, len(wp.Receivers)) + tokenIDs := make([]*big.Int, len(wp.Receivers)) + + for i, tokenIDRaw := range wp.TokenIDs { + tokenIDRaw := tokenIDRaw + + tokenID, err := types.ParseUint256orHex(&tokenIDRaw) + if err != nil { + outputter.SetError(fmt.Errorf("failed to decode provided token id %s: %w", tokenIDRaw, err)) + + return + } + + receivers[i] = ethgo.Address(types.StringToAddress(wp.Receivers[i])) + tokenIDs[i] = tokenID + } + + txn, err := createWithdrawTxn(receivers, tokenIDs) + if err != nil { + outputter.SetError(fmt.Errorf("failed to create tx input: %w", err)) + + return + } + + receipt, err := txRelayer.SendTransaction(txn, senderAccount) + if err != nil { + outputter.SetError(fmt.Errorf("failed to send withdrawal transaction (receivers: %s, token ids: %s): %w", + strings.Join(wp.Receivers, ", "), strings.Join(wp.TokenIDs, ", "), err)) + + return + } + + if receipt.Status == uint64(types.ReceiptFailed) { + outputter.SetError(fmt.Errorf("failed to execute withdrawal transaction (receivers: %s, token ids: %s)", + strings.Join(wp.Receivers, ", "), strings.Join(wp.TokenIDs, ", "))) + + return + } + + exitEventIDRaw, err := extractExitEventID(receipt) + if err != nil { + outputter.SetError(fmt.Errorf("failed to extract exit event: %w", err)) + + return + } + + exitEventID := strconv.FormatUint(exitEventIDRaw.Uint64(), 10) + blockNumber := strconv.FormatUint(receipt.BlockNumber, 10) + + outputter.SetCommandResult( + &withdrawERC721Result{ + Sender: senderAccount.Address().String(), + Receivers: wp.Receivers, + TokenIDs: wp.TokenIDs, + ExitEventID: exitEventID, + BlockNumber: blockNumber, + }) +} + +// createWithdrawTxn encodes parameters for withdraw function on child chain predicate contract +func createWithdrawTxn(receivers []ethgo.Address, tokenIDs []*big.Int) (*ethgo.Transaction, error) { + withdrawToFn := &contractsapi.WithdrawBatchChildERC721PredicateFn{ + ChildToken: types.StringToAddress(wp.TokenAddr), + Receivers: receivers, + TokenIDs: tokenIDs, + } + + input, err := withdrawToFn.EncodeAbi() + if err != nil { + return nil, fmt.Errorf("failed to encode provided parameters: %w", err) + } + + addr := ethgo.Address(types.StringToAddress(wp.PredicateAddr)) + + return ðgo.Transaction{ + To: &addr, + Input: input, + }, nil +} + +// extractExitEventID tries to extract exit event id from provided receipt +func extractExitEventID(receipt *ethgo.Receipt) (*big.Int, error) { + var exitEvent contractsapi.L2StateSyncedEvent + for _, log := range receipt.Logs { + doesMatch, err := exitEvent.ParseLog(log) + if err != nil { + return nil, err + } + + if !doesMatch { + continue + } + + return exitEvent.ID, nil + } + + return nil, errors.New("failed to find exit event log") +} + +type withdrawERC721Result struct { + Sender string `json:"sender"` + Receivers []string `json:"receivers"` + TokenIDs []string `json:"tokenIDs"` + ExitEventID string `json:"exitEventIDs"` + BlockNumber string `json:"blockNumbers"` +} + +func (r *withdrawERC721Result) GetOutput() string { + var buffer bytes.Buffer + + vals := make([]string, 0, 5) + vals = append(vals, fmt.Sprintf("Sender|%s", r.Sender)) + vals = append(vals, fmt.Sprintf("Receivers|%s", strings.Join(r.Receivers, ", "))) + vals = append(vals, fmt.Sprintf("TokenIDs|%s", strings.Join(r.TokenIDs, ", "))) + vals = append(vals, fmt.Sprintf("Exit Event IDs|%s", r.ExitEventID)) + vals = append(vals, fmt.Sprintf("Inclusion Block Numbers|%s", r.BlockNumber)) + + buffer.WriteString("\n[WITHDRAW ERC 721]\n") + buffer.WriteString(cmdHelper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/rootchain/deploy/deploy.go b/command/rootchain/deploy/deploy.go index aad6b8d841..6b700abd28 100644 --- a/command/rootchain/deploy/deploy.go +++ b/command/rootchain/deploy/deploy.go @@ -33,6 +33,7 @@ const ( erc20TemplateName = "ERC20Template" rootERC721PredicateName = "RootERC721Predicate" rootERC721Name = "RootERC721" + erc721TemplateName = "ERC721Template" rootERC1155PredicateName = "RootERC1155Predicate" rootERC1155Name = "RootERC1155" erc1155TemplateName = "ERC1155Template" @@ -74,6 +75,9 @@ var ( rootERC721Name: func(rootchainConfig *polybft.RootchainConfig, addr types.Address) { rootchainConfig.RootERC721Address = addr }, + erc721TemplateName: func(rootchainConfig *polybft.RootchainConfig, addr types.Address) { + rootchainConfig.RootERC721TemplateAddress = addr + }, rootERC1155PredicateName: func(rootchainConfig *polybft.RootchainConfig, addr types.Address) { rootchainConfig.RootERC1155PredicateAddress = addr }, @@ -297,6 +301,10 @@ func deployContracts(outputter command.OutputFormatter, client *jsonrpc.Client, name: rootERC721PredicateName, artifact: contractsapi.RootERC721Predicate, }, + { + name: erc721TemplateName, + artifact: contractsapi.ChildERC721, + }, { name: rootERC1155PredicateName, artifact: contractsapi.RootERC1155Predicate, @@ -389,6 +397,11 @@ func deployContracts(outputter command.OutputFormatter, client *jsonrpc.Client, return nil, err } + // init RootERC721Predicate + if err := initializeRootERC721Predicate(outputter, txRelayer, rootchainConfig, deployerKey); err != nil { + return nil, err + } + // init RootERC1155Predicate if err := initializeRootERC1155Predicate(outputter, txRelayer, rootchainConfig, deployerKey); err != nil { return rootchainConfig, err @@ -509,6 +522,32 @@ func initializeRootERC20Predicate(cmdOutput command.OutputFormatter, txRelayer t return nil } +func initializeRootERC721Predicate(cmdOutput command.OutputFormatter, txRelayer txrelayer.TxRelayer, + rootchainConfig *polybft.RootchainConfig, deployerKey ethgo.Key) error { + rootERC721PredicateParams := contractsapi.InitializeRootERC721PredicateFn{ + NewStateSender: rootchainConfig.StateSenderAddress, + NewExitHelper: rootchainConfig.ExitHelperAddress, + NewChildERC721Predicate: contracts.ChildERC721PredicateContract, + NewChildTokenTemplate: rootchainConfig.RootERC721TemplateAddress, + } + + input, err := rootERC721PredicateParams.EncodeAbi() + if err != nil { + return fmt.Errorf("failed to encode parameters for RootERC721Predicate.initialize. error: %w", err) + } + + if err := sendTransaction(txRelayer, ethgo.Address(rootchainConfig.RootERC721PredicateAddress), + input, rootERC721PredicateName, deployerKey); err != nil { + return err + } + + cmdOutput.WriteCommandResult(&messageResult{ + Message: fmt.Sprintf("%s %s contract is initialized", contractsDeploymentTitle, rootERC721PredicateName), + }) + + return nil +} + // initializeRootERC1155Predicate invokes initialize function on RootERC1155Predicate smart contract func initializeRootERC1155Predicate(cmdOutput command.OutputFormatter, txRelayer txrelayer.TxRelayer, rootchainConfig *polybft.RootchainConfig, deployerKey ethgo.Key) error { diff --git a/command/rootchain/fund/fund.go b/command/rootchain/fund/fund.go index e197a7bd68..957f0e24d1 100644 --- a/command/rootchain/fund/fund.go +++ b/command/rootchain/fund/fund.go @@ -59,8 +59,8 @@ func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( &jsonRPCAddress, jsonRPCFlag, - "http://127.0.0.1:8545", - "the JSON RPC rootchain IP address (e.g. http://127.0.0.1:8545)", + txrelayer.DefaultRPCAddress, + "the rootchain JSON RPC endpoint", ) // Don't accept data-dir and config flags because they are related to different secrets managers. diff --git a/consensus/polybft/contracts_initializer.go b/consensus/polybft/contracts_initializer.go index fc03cf9cf1..ff9b4e2a6b 100644 --- a/consensus/polybft/contracts_initializer.go +++ b/consensus/polybft/contracts_initializer.go @@ -71,6 +71,24 @@ func getInitChildERC20PredicateInput(config *BridgeConfig) ([]byte, error) { return params.EncodeAbi() } +// getInitChildERC721PredicateInput builds input parameters for ChildERC721Predicate SC initialization +func getInitChildERC721PredicateInput(config *BridgeConfig) ([]byte, error) { + rootERC721PredicateAddr := types.StringToAddress(disabledBridgeRootPredicateAddr) + + if config != nil { + rootERC721PredicateAddr = config.RootERC721PredicateAddr + } + + params := &contractsapi.InitializeChildERC721PredicateFn{ + NewL2StateSender: contracts.L2StateSenderContract, + NewStateReceiver: contracts.StateReceiverContract, + NewRootERC721Predicate: rootERC721PredicateAddr, + NewChildTokenTemplate: contracts.ChildERC721Contract, + } + + return params.EncodeAbi() +} + // getInitChildERC1155PredicateInput builds input parameters for ChildERC1155Predicate SC initialization func getInitChildERC1155PredicateInput(config *BridgeConfig) ([]byte, error) { rootERC1155PredicateAddr := types.StringToAddress(disabledBridgeRootPredicateAddr) diff --git a/consensus/polybft/contractsapi/bindings-gen/main.go b/consensus/polybft/contractsapi/bindings-gen/main.go index 3bf9c7c712..7f5b722866 100644 --- a/consensus/polybft/contractsapi/bindings-gen/main.go +++ b/consensus/polybft/contractsapi/bindings-gen/main.go @@ -183,6 +183,42 @@ func main() { }, []string{}, }, + { + "RootERC721Predicate", + gensc.RootERC721Predicate, + []string{ + "initialize", + "depositBatch", + }, + []string{}, + }, + { + "RootERC721", + gensc.RootERC721, + []string{ + "setApprovalForAll", + "mint", + }, + []string{}, + }, + { + "ChildERC721Predicate", + gensc.ChildERC721Predicate, + []string{ + "initialize", + "withdrawBatch", + }, + []string{}, + }, + { + "ChildERC721", + gensc.ChildERC721, + []string{ + "initialize", + "ownerOf", + }, + []string{}, + }, } generatedData := &generatedData{} diff --git a/consensus/polybft/contractsapi/contractsapi.go b/consensus/polybft/contractsapi/contractsapi.go index ef5203dbd1..e1b4ad0d18 100644 --- a/consensus/polybft/contractsapi/contractsapi.go +++ b/consensus/polybft/contractsapi/contractsapi.go @@ -951,3 +951,144 @@ func (b *BalanceOfChildERC1155Fn) EncodeAbi() ([]byte, error) { func (b *BalanceOfChildERC1155Fn) DecodeAbi(buf []byte) error { return decodeMethod(ChildERC1155.Abi.Methods["balanceOf"], buf, b) } + +type InitializeRootERC721PredicateFn struct { + NewStateSender types.Address `abi:"newStateSender"` + NewExitHelper types.Address `abi:"newExitHelper"` + NewChildERC721Predicate types.Address `abi:"newChildERC721Predicate"` + NewChildTokenTemplate types.Address `abi:"newChildTokenTemplate"` +} + +func (i *InitializeRootERC721PredicateFn) Sig() []byte { + return RootERC721Predicate.Abi.Methods["initialize"].ID() +} + +func (i *InitializeRootERC721PredicateFn) EncodeAbi() ([]byte, error) { + return RootERC721Predicate.Abi.Methods["initialize"].Encode(i) +} + +func (i *InitializeRootERC721PredicateFn) DecodeAbi(buf []byte) error { + return decodeMethod(RootERC721Predicate.Abi.Methods["initialize"], buf, i) +} + +type DepositBatchRootERC721PredicateFn struct { + RootToken types.Address `abi:"rootToken"` + Receivers []ethgo.Address `abi:"receivers"` + TokenIDs []*big.Int `abi:"tokenIds"` +} + +func (d *DepositBatchRootERC721PredicateFn) Sig() []byte { + return RootERC721Predicate.Abi.Methods["depositBatch"].ID() +} + +func (d *DepositBatchRootERC721PredicateFn) EncodeAbi() ([]byte, error) { + return RootERC721Predicate.Abi.Methods["depositBatch"].Encode(d) +} + +func (d *DepositBatchRootERC721PredicateFn) DecodeAbi(buf []byte) error { + return decodeMethod(RootERC721Predicate.Abi.Methods["depositBatch"], buf, d) +} + +type SetApprovalForAllRootERC721Fn struct { + Operator types.Address `abi:"operator"` + Approved bool `abi:"approved"` +} + +func (s *SetApprovalForAllRootERC721Fn) Sig() []byte { + return RootERC721.Abi.Methods["setApprovalForAll"].ID() +} + +func (s *SetApprovalForAllRootERC721Fn) EncodeAbi() ([]byte, error) { + return RootERC721.Abi.Methods["setApprovalForAll"].Encode(s) +} + +func (s *SetApprovalForAllRootERC721Fn) DecodeAbi(buf []byte) error { + return decodeMethod(RootERC721.Abi.Methods["setApprovalForAll"], buf, s) +} + +type MintRootERC721Fn struct { + To types.Address `abi:"to"` +} + +func (m *MintRootERC721Fn) Sig() []byte { + return RootERC721.Abi.Methods["mint"].ID() +} + +func (m *MintRootERC721Fn) EncodeAbi() ([]byte, error) { + return RootERC721.Abi.Methods["mint"].Encode(m) +} + +func (m *MintRootERC721Fn) DecodeAbi(buf []byte) error { + return decodeMethod(RootERC721.Abi.Methods["mint"], buf, m) +} + +type InitializeChildERC721PredicateFn struct { + NewL2StateSender types.Address `abi:"newL2StateSender"` + NewStateReceiver types.Address `abi:"newStateReceiver"` + NewRootERC721Predicate types.Address `abi:"newRootERC721Predicate"` + NewChildTokenTemplate types.Address `abi:"newChildTokenTemplate"` +} + +func (i *InitializeChildERC721PredicateFn) Sig() []byte { + return ChildERC721Predicate.Abi.Methods["initialize"].ID() +} + +func (i *InitializeChildERC721PredicateFn) EncodeAbi() ([]byte, error) { + return ChildERC721Predicate.Abi.Methods["initialize"].Encode(i) +} + +func (i *InitializeChildERC721PredicateFn) DecodeAbi(buf []byte) error { + return decodeMethod(ChildERC721Predicate.Abi.Methods["initialize"], buf, i) +} + +type WithdrawBatchChildERC721PredicateFn struct { + ChildToken types.Address `abi:"childToken"` + Receivers []ethgo.Address `abi:"receivers"` + TokenIDs []*big.Int `abi:"tokenIds"` +} + +func (w *WithdrawBatchChildERC721PredicateFn) Sig() []byte { + return ChildERC721Predicate.Abi.Methods["withdrawBatch"].ID() +} + +func (w *WithdrawBatchChildERC721PredicateFn) EncodeAbi() ([]byte, error) { + return ChildERC721Predicate.Abi.Methods["withdrawBatch"].Encode(w) +} + +func (w *WithdrawBatchChildERC721PredicateFn) DecodeAbi(buf []byte) error { + return decodeMethod(ChildERC721Predicate.Abi.Methods["withdrawBatch"], buf, w) +} + +type InitializeChildERC721Fn struct { + RootToken_ types.Address `abi:"rootToken_"` + Name_ string `abi:"name_"` + Symbol_ string `abi:"symbol_"` +} + +func (i *InitializeChildERC721Fn) Sig() []byte { + return ChildERC721.Abi.Methods["initialize"].ID() +} + +func (i *InitializeChildERC721Fn) EncodeAbi() ([]byte, error) { + return ChildERC721.Abi.Methods["initialize"].Encode(i) +} + +func (i *InitializeChildERC721Fn) DecodeAbi(buf []byte) error { + return decodeMethod(ChildERC721.Abi.Methods["initialize"], buf, i) +} + +type OwnerOfChildERC721Fn struct { + TokenID *big.Int `abi:"tokenId"` +} + +func (o *OwnerOfChildERC721Fn) Sig() []byte { + return ChildERC721.Abi.Methods["ownerOf"].ID() +} + +func (o *OwnerOfChildERC721Fn) EncodeAbi() ([]byte, error) { + return ChildERC721.Abi.Methods["ownerOf"].Encode(o) +} + +func (o *OwnerOfChildERC721Fn) DecodeAbi(buf []byte) error { + return decodeMethod(ChildERC721.Abi.Methods["ownerOf"], buf, o) +} diff --git a/consensus/polybft/polybft.go b/consensus/polybft/polybft.go index 5174c79d8d..3d8830f9db 100644 --- a/consensus/polybft/polybft.go +++ b/consensus/polybft/polybft.go @@ -182,6 +182,17 @@ func GenesisPostHookFactory(config *chain.Chain, engineName string) func(txn *st } } + // initialize ChildERC721Predicate SC + input, err = getInitChildERC721PredicateInput(polyBFTConfig.Bridge) + if err != nil { + return err + } + + if err = initContract(contracts.ChildERC721PredicateContract, input, + "ChildERC721Predicate", transition); err != nil { + return err + } + // initialize ChildERC1155Predicate SC input, err = getInitChildERC1155PredicateInput(polyBFTConfig.Bridge) if err != nil { diff --git a/consensus/polybft/polybft_config.go b/consensus/polybft/polybft_config.go index 796d00c5d2..ecc0db23e8 100644 --- a/consensus/polybft/polybft_config.go +++ b/consensus/polybft/polybft_config.go @@ -79,6 +79,7 @@ type BridgeConfig struct { ExitHelperAddr types.Address `json:"exitHelperAddress"` RootERC20PredicateAddr types.Address `json:"erc20PredicateAddress"` RootNativeERC20Addr types.Address `json:"nativeERC20Address"` + RootERC721Addr types.Address `json:"erc721Address"` RootERC721PredicateAddr types.Address `json:"erc721PredicateAddress"` RootERC1155Addr types.Address `json:"erc1155Address"` RootERC1155PredicateAddr types.Address `json:"erc1155PredicateAddress"` @@ -229,6 +230,7 @@ type RootchainConfig struct { ERC20TemplateAddress types.Address RootERC721PredicateAddress types.Address RootERC721Address types.Address + RootERC721TemplateAddress types.Address RootERC1155PredicateAddress types.Address RootERC1155Address types.Address ERC1155TemplateAddress types.Address @@ -244,6 +246,7 @@ func (r *RootchainConfig) ToBridgeConfig() *BridgeConfig { ExitHelperAddr: r.ExitHelperAddress, RootERC20PredicateAddr: r.RootERC20PredicateAddress, RootNativeERC20Addr: r.RootNativeERC20Address, + RootERC721Addr: r.RootERC721Address, RootERC721PredicateAddr: r.RootERC721PredicateAddress, RootERC1155Addr: r.RootERC1155Address, RootERC1155PredicateAddr: r.RootERC1155PredicateAddress, diff --git a/consensus/polybft/statesyncrelayer/state_sync_relayer.go b/consensus/polybft/statesyncrelayer/state_sync_relayer.go index e1c0cc33b8..b9f15764a0 100644 --- a/consensus/polybft/statesyncrelayer/state_sync_relayer.go +++ b/consensus/polybft/statesyncrelayer/state_sync_relayer.go @@ -38,7 +38,7 @@ func sanitizeRPCEndpoint(rpcEndpoint string) string { if err == nil { rpcEndpoint = fmt.Sprintf("http://%s:%s", "127.0.0.1", port) } else { - rpcEndpoint = "http://127.0.0.1:8545" + rpcEndpoint = txrelayer.DefaultRPCAddress } } diff --git a/consensus/polybft/statesyncrelayer/state_sync_relayer_test.go b/consensus/polybft/statesyncrelayer/state_sync_relayer_test.go index e2a2bc26f1..2c709477e6 100644 --- a/consensus/polybft/statesyncrelayer/state_sync_relayer_test.go +++ b/consensus/polybft/statesyncrelayer/state_sync_relayer_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/txrelayer" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/mock" @@ -93,7 +94,7 @@ func Test_sanitizeRPCEndpoint(t *testing.T) { { "empty endpoint", "", - "http://127.0.0.1:8545", + txrelayer.DefaultRPCAddress, }, } @@ -115,7 +116,7 @@ func TestStateSyncRelayer_Stop(t *testing.T) { key, err := wallet.GenerateKey() require.NoError(t, err) - r := NewRelayer("test-chain-1", "http://127.0.0.1:8545", ethgo.Address(contracts.StateReceiverContract), 0, hclog.NewNullLogger(), key) + r := NewRelayer("test-chain-1", txrelayer.DefaultRPCAddress, ethgo.Address(contracts.StateReceiverContract), 0, hclog.NewNullLogger(), key) require.NotPanics(t, func() { r.Stop() }) } diff --git a/e2e-polybft/e2e/bridge_test.go b/e2e-polybft/e2e/bridge_test.go index 0fbb92431e..99fa9b13de 100644 --- a/e2e-polybft/e2e/bridge_test.go +++ b/e2e-polybft/e2e/bridge_test.go @@ -274,6 +274,172 @@ func TestE2E_Bridge_Transfers(t *testing.T) { }) } +func TestE2E_Bridge_DepositAndWithdrawERC721(t *testing.T) { + const ( + txnCount = 4 + epochSize = 5 + ) + + receiverKeys := make([]string, txnCount) + receivers := make([]string, txnCount) + receiversAddrs := make([]types.Address, txnCount) + tokenIDs := make([]string, txnCount) + + for i := 0; i < txnCount; i++ { + key, err := ethgow.GenerateKey() + require.NoError(t, err) + + rawKey, err := key.MarshallPrivateKey() + require.NoError(t, err) + + receiverKeys[i] = hex.EncodeToString(rawKey) + receivers[i] = types.Address(key.Address()).String() + receiversAddrs[i] = types.Address(key.Address()) + tokenIDs[i] = fmt.Sprintf("%d", i) + + t.Logf("Receiver#%d=%s\n", i+1, receivers[i]) + } + + cluster := framework.NewTestCluster(t, 5, + framework.WithBridge(), + framework.WithEpochSize(epochSize), + framework.WithPremine(receiversAddrs...)) + defer cluster.Stop() + + cluster.WaitForReady(t) + + polybftCfg, err := polybft.LoadPolyBFTConfig(path.Join(cluster.Config.TmpDir, chainConfigFileName)) + require.NoError(t, err) + + // DEPOSIT ERC721 TOKENS + // send a few transactions to the bridge + require.NoError( + t, + cluster.Bridge.Deposit( + common.ERC721, + polybftCfg.Bridge.RootERC721Addr, + polybftCfg.Bridge.RootERC721PredicateAddr, + strings.Join(receivers[:], ","), + "", + strings.Join(tokenIDs[:], ","), + ), + ) + + // wait for a few more sprints + require.NoError(t, cluster.WaitForBlock(25, 2*time.Minute)) + + // the transactions are processed and there should be a success events + var stateSyncedResult contractsapi.StateSyncResultEvent + + id := stateSyncedResult.Sig() + filter := ðgo.LogFilter{ + Topics: [][]*ethgo.Hash{ + {&id}, + }, + } + + filter.SetFromUint64(0) + filter.SetToUint64(100) + + validatorSrv := cluster.Servers[0] + childEthEndpoint := validatorSrv.JSONRPC().Eth() + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithClient(validatorSrv.JSONRPC())) + require.NoError(t, err) + + logs, err := childEthEndpoint.GetLogs(filter) + require.NoError(t, err) + + // assert that all deposits are executed successfully. + // All deposits are sent using a single transaction, so arbitrary message bridge emits two state sync events: + // MAP_TOKEN_SIG and DEPOSIT_BATCH_SIG state sync events + checkStateSyncResultLogs(t, logs, 2) + + // retrieve child token address + rootToChildTokenFn := contractsapi.ChildERC721Predicate.Abi.Methods["rootTokenToChildToken"] + input, err := rootToChildTokenFn.Encode([]interface{}{polybftCfg.Bridge.RootERC721Addr}) + require.NoError(t, err) + + childTokenRaw, err := txRelayer.Call(ethgo.ZeroAddress, ethgo.Address(contracts.ChildERC721PredicateContract), input) + require.NoError(t, err) + + childTokenAddr := types.StringToAddress(childTokenRaw) + + for i, receiver := range receiversAddrs { + ownerOfFn := &contractsapi.OwnerOfChildERC721Fn{ + TokenID: big.NewInt(int64(i)), + } + + ownerInput, err := ownerOfFn.EncodeAbi() + require.NoError(t, err) + + addressRaw, err := txRelayer.Call(ethgo.ZeroAddress, ethgo.Address(childTokenAddr), ownerInput) + require.NoError(t, err) + + require.Equal(t, receiver, types.StringToAddress(addressRaw)) + } + + t.Log("Deposits were successfully processed") + + // WITHDRAW ERC721 TOKENS + rootchainTxRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(cluster.Bridge.JSONRPCAddr())) + require.NoError(t, err) + + for i, receiverKey := range receiverKeys { + // send withdraw transactions + err = cluster.Bridge.Withdraw( + common.ERC721, + receiverKey, + receivers[i], + "", + tokenIDs[i], + validatorSrv.JSONRPCAddr(), + childTokenAddr) + require.NoError(t, err) + } + + currentBlock, err := childEthEndpoint.GetBlockByNumber(ethgo.Latest, false) + require.NoError(t, err) + + currentExtra, err := polybft.GetIbftExtra(currentBlock.ExtraData) + require.NoError(t, err) + + t.Logf("Latest block number: %d, epoch number: %d\n", currentBlock.Number, currentExtra.Checkpoint.EpochNumber) + + currentEpoch := currentExtra.Checkpoint.EpochNumber + require.NoError(t, waitForRootchainEpoch(currentEpoch, 3*time.Minute, rootchainTxRelayer, polybftCfg.Bridge.CheckpointManagerAddr)) + + exitHelper := polybftCfg.Bridge.ExitHelperAddr + rootJSONRPC := cluster.Bridge.JSONRPCAddr() + childJSONRPC := validatorSrv.JSONRPCAddr() + + for i := uint64(1); i <= txnCount; i++ { + // send exit transaction to exit helper + err = cluster.Bridge.SendExitTransaction(exitHelper, i, rootJSONRPC, childJSONRPC) + require.NoError(t, err) + + // make sure exit event is processed successfully + isProcessed, err := isExitEventProcessed(i, ethgo.Address(exitHelper), rootchainTxRelayer) + require.NoError(t, err) + require.True(t, isProcessed, fmt.Sprintf("exit event with ID %d was not processed", i)) + } + + // assert that owners of given token ids are the accounts on the root chain ERC 721 token + for i, receiver := range receiversAddrs { + ownerOfFn := &contractsapi.OwnerOfChildERC721Fn{ + TokenID: big.NewInt(int64(i)), + } + + ownerInput, err := ownerOfFn.EncodeAbi() + require.NoError(t, err) + + addressRaw, err := rootchainTxRelayer.Call(ethgo.ZeroAddress, ethgo.Address(polybftCfg.Bridge.RootERC721Addr), ownerInput) + require.NoError(t, err) + + require.Equal(t, receiver, types.StringToAddress(addressRaw)) + } +} + func TestE2E_Bridge_DepositAndWithdrawERC1155(t *testing.T) { const ( txnCount = 5 diff --git a/e2e-polybft/framework/test-bridge.go b/e2e-polybft/framework/test-bridge.go index 3fdfb6358f..a9b3a99ed7 100644 --- a/e2e-polybft/framework/test-bridge.go +++ b/e2e-polybft/framework/test-bridge.go @@ -123,8 +123,18 @@ func (t *TestBridge) Deposit(token bridgeCommon.TokenType, rootTokenAddr, rootPr "--amounts", amounts) case bridgeCommon.ERC721: - //nolint:godox - // TODO: Implement ERC721 deposits + if tokenIDs == "" { + return errors.New("provide at least one token id value") + } + + args = append(args, + "bridge", + "deposit-erc721", + "--test", + "--root-token", rootTokenAddr.String(), + "--root-predicate", rootPredicateAddr.String(), + "--receivers", receivers, + "--token-ids", tokenIDs) case bridgeCommon.ERC1155: if amounts == "" { @@ -187,8 +197,18 @@ func (t *TestBridge) Withdraw(token bridgeCommon.TokenType, "--json-rpc", jsonRPCEndpoint) case bridgeCommon.ERC721: - //nolint:godox - // TODO: Implement ERC721 withdrawal + if tokenIDs == "" { + return errors.New("provide at least one token id value") + } + + args = append(args, + "bridge", + "withdraw-erc721", + "--sender-key", senderKey, + "--receivers", receivers, + "--token-ids", tokenIDs, + "--json-rpc", jsonRPCEndpoint, + "--child-token", childToken.String()) case bridgeCommon.ERC1155: if amounts == "" { diff --git a/txrelayer/txrelayer.go b/txrelayer/txrelayer.go index e812dae19d..fec5b7b132 100644 --- a/txrelayer/txrelayer.go +++ b/txrelayer/txrelayer.go @@ -44,7 +44,7 @@ type TxRelayerImpl struct { func NewTxRelayer(opts ...TxRelayerOption) (TxRelayer, error) { t := &TxRelayerImpl{ - ipAddress: "http://127.0.0.1:8545", + ipAddress: DefaultRPCAddress, receiptTimeout: 50 * time.Millisecond, } for _, opt := range opts {