Skip to content

Commit

Permalink
ERC20 token bridge (deposit workflow) (0xPolygon#1242)
Browse files Browse the repository at this point in the history
* Update smart contracts submodule

* Introduce new contracts and generate go bindings

* Update to the latest SC spec

* Add predefined addresses for ChildERC20 and ChildERC20Predicate contracts

* Add NativeERC20 artifact

* Deploy and initialize RootERC20Predicate

* Deploy and init ChildERC20Predicate, ChildERC20 and NativeERC20 contracts on child chain

* Deploy missing contracts, change encoding

* Deposit workflow

* Update TestE2E_Bridge_MultipleCommitmentsPerEpoch assertions

* Update smart contracts

* Rename NativeTokenContract to NativeERC20TokenContract

* Update smart contracts

* - Provide correct receiver address for deposits
- Print sender address in deposit command

* Remove token type value comments

* Move bridge workflow commands to a separate package

* Fix assertions

* SC logging

* Fix assertions 2nd part

* Remove leftover log

* Rename bridge command

* Scientific notation for default mint value

* Address comments

* Rename admin key to depositor key

* Use scientific notation for defaultAllowanceValue

* Fix deposit flags descriptions

* Rebase fix

* Update smart contracts

* Generate function bindings for RootERC20Predicate and RootERC20
Rename RootERC20

* Approve RootERC20Predicate as spender only in test mode
  • Loading branch information
Stefan-Ethernal authored Mar 2, 2023
1 parent 5aea95d commit ee35129
Show file tree
Hide file tree
Showing 30 changed files with 988 additions and 517 deletions.
14 changes: 14 additions & 0 deletions command/bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Bridge helper command

This is a helper command, which allows sending deposits from root to child chain and make withdrawals from child chain to root chain.

## Deposit
This is a helper command which bridges assets from rootchain to the child chain (allows depositing)

```bash
$ polygon-edge bridge deposit
--depositor-key <hex_encoded_depositor_private_key>
--token <token_type>
--receivers <receivers_addresses>
--amounts <amounts>
```
25 changes: 25 additions & 0 deletions command/bridge/bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package bridge

import (
"github.com/0xPolygon/polygon-edge/command/bridge/deposit"
"github.com/spf13/cobra"
)

// GetCommand creates "bridge" helper command
func GetCommand() *cobra.Command {
bridgeCmd := &cobra.Command{
Use: "bridge",
Short: "Top level bridge command.",
}

registerSubcommands(bridgeCmd)

return bridgeCmd
}

func registerSubcommands(baseCmd *cobra.Command) {
baseCmd.AddCommand(
// bridge deposit
deposit.GetCommand(),
)
}
295 changes: 295 additions & 0 deletions command/bridge/deposit/deposit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
package deposit

import (
"fmt"
"math/big"
"strings"

"github.com/spf13/cobra"
"github.com/umbracle/ethgo"
"golang.org/x/sync/errgroup"

"github.com/0xPolygon/polygon-edge/command"
"github.com/0xPolygon/polygon-edge/command/rootchain/helper"
"github.com/0xPolygon/polygon-edge/consensus/polybft"
"github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi"
"github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi/artifact"
"github.com/0xPolygon/polygon-edge/contracts"
"github.com/0xPolygon/polygon-edge/txrelayer"
"github.com/0xPolygon/polygon-edge/types"
)

type TokenType int

const (
ERC20 TokenType = iota
ERC721
ERC1155
)

const (
// defaultMintValue represents amount of tokens which are going to be minted to depositor
defaultMintValue = int64(1e18)
)

var (
params depositParams

manifest *polybft.Manifest

tokenTypesMap = map[string]TokenType{
"erc20": ERC20,
"erc721": ERC721,
"erc1155": ERC1155,
}

configs = map[TokenType]*bridgeConfig{}
)

// GetCommand returns the bridge deposit command
func GetCommand() *cobra.Command {
depositCmd := &cobra.Command{
Use: "deposit",
Short: "Deposits tokens from root chain to child chain",
PreRunE: runPreRun,
Run: runCommand,
}

setFlags(depositCmd)

return depositCmd
}

func setFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(
&params.manifestPath,
manifestPathFlag,
"./manifest.json",
"the manifest file path, which contains genesis metadata",
)

cmd.Flags().StringVar(
&params.tokenTypeRaw,
tokenFlag,
"erc20",
"token type which is being deposited",
)

cmd.Flags().StringSliceVar(
&params.receivers,
receiversFlag,
nil,
"receiving accounts addresses on child chain",
)

cmd.Flags().StringSliceVar(
&params.amounts,
amountsFlag,
nil,
"amounts to send to child chain receiving accounts",
)

cmd.Flags().StringVar(
&params.jsonRPCAddress,
jsonRPCFlag,
"http://127.0.0.1:8545",
"the JSON RPC rootchain IP address (e.g. http://127.0.0.1:8545)",
)

cmd.Flags().StringVar(
&params.depositorKey,
depositorKeyFlag,
helper.DefaultPrivateKeyRaw,
"hex encoded private key of the account which sends rootchain deposit transactions",
)
}

func runPreRun(_ *cobra.Command, _ []string) error {
var err error
if err = params.validateFlags(); err != nil {
return err
}

manifest, err = polybft.LoadManifest(params.manifestPath)
if err != nil {
return fmt.Errorf("failed to load manifest file from '%s': %w", params.manifestPath, err)
}

// populate bridge configs based on token types
configs[ERC20] = newBridgeConfig(
contractsapi.RootERC20Predicate,
"depositTo",
contractsapi.RootERC20,
"mint",
manifest.RootchainConfig.RootNativeERC20Address,
manifest.RootchainConfig.RootERC20PredicateAddress,
contracts.ChildERC20PredicateContract)

return nil
}

func runCommand(cmd *cobra.Command, _ []string) {
outputter := command.InitializeOutputter(cmd)
defer outputter.WriteOutput()

if err := helper.InitRootchainPrivateKey(params.depositorKey); err != nil {
outputter.SetError(err)

return
}

tokenType, _ := lookupTokenType(params.tokenTypeRaw)

config, exists := configs[tokenType]
if !exists {
outputter.SetError(fmt.Errorf("not found bridge config for provided token type: %s", params.tokenTypeRaw))

return
}

txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(params.jsonRPCAddress))
if err != nil {
outputter.SetError(fmt.Errorf("could not create rootchain interactor: %w", err))

return
}

g, ctx := errgroup.WithContext(cmd.Context())

for i := range params.receivers {
receiver := params.receivers[i]
amount := params.amounts[i]

g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if helper.IsTestMode(params.depositorKey) {
// mint tokens to depositor
txn, err := createMintTxn(config, big.NewInt(defaultMintValue))
if err != nil {
return fmt.Errorf("mint transaction creation failed: %w", err)
}
receipt, err := txRelayer.SendTransaction(txn, helper.GetRootchainPrivateKey())
if err != nil {
return fmt.Errorf("failed to send mint transaction to depositor %s", helper.GetRootchainPrivateKey().Address())
}

if receipt.Status == uint64(types.ReceiptFailed) {
return fmt.Errorf("failed to mint tokens to depositor %s", helper.GetRootchainPrivateKey().Address())
}
}

// deposit tokens
amountBig, err := types.ParseUint256orHex(&amount)
if err != nil {
return fmt.Errorf("failed to decode provided amount %s: %w", amount, err)
}
txn, err := createDepositTxn(config, ethgo.BytesToAddress([]byte(receiver)), amountBig)
if err != nil {
return fmt.Errorf("failed to create tx input: %w", err)
}

receipt, err := txRelayer.SendTransaction(txn, helper.GetRootchainPrivateKey())
if err != nil {
return fmt.Errorf("receiver: %s, amount: %s, error: %w",
receiver, amount, err)
}

if receipt.Status == uint64(types.ReceiptFailed) {
return fmt.Errorf("receiver: %s, amount: %s",
receiver, amount)
}

return nil
}
})
}

if err = g.Wait(); err != nil {
outputter.SetError(fmt.Errorf("sending transactions to rootchain failed: %w", err))

return
}

outputter.SetCommandResult(&result{
TokenType: params.tokenTypeRaw,
Sender: helper.GetRootchainPrivateKey().Address().String(),
Receivers: params.receivers,
Amounts: params.amounts,
})
}

// createDepositTxn encodes parameters for deposit function on rootchain predicate contract
func createDepositTxn(config *bridgeConfig, receiver ethgo.Address, amount *big.Int) (*ethgo.Transaction, error) {
input, err := config.rootPredicate.Abi.Methods[config.depositFnName].Encode([]interface{}{
config.rootTokenAddr,
receiver,
amount,
})
if err != nil {
return nil, fmt.Errorf("failed to encode provided parameters: %w", err)
}

addr := ethgo.Address(config.rootPredicateAddr)

return &ethgo.Transaction{
To: &addr,
Input: input,
}, nil
}

// createMintTxn encodes parameters for mint function on rootchain token contract
func createMintTxn(config *bridgeConfig, amount *big.Int) (*ethgo.Transaction, error) {
input, err := config.rootToken.Abi.Methods[config.mintFnName].Encode([]interface{}{
helper.GetRootchainPrivateKey().Address(),
amount,
})
if err != nil {
return nil, fmt.Errorf("failed to encode provided parameters: %w", err)
}

addr := ethgo.Address(config.rootTokenAddr)

return &ethgo.Transaction{
To: &addr,
Input: input,
}, nil
}

// bridgeConfig contains parameterizable parameters for assets bridging
type bridgeConfig struct {
rootPredicate *artifact.Artifact
depositFnName string
rootToken *artifact.Artifact
mintFnName string
rootTokenAddr types.Address
rootPredicateAddr types.Address
childPredicateAddr types.Address
}

func newBridgeConfig(rootPredicate *artifact.Artifact,
depositFnName string,
rootToken *artifact.Artifact,
mintFnName string,
rootTokenAddr types.Address,
rootPredicateAddr types.Address,
childPredicateAddr types.Address) *bridgeConfig {
return &bridgeConfig{
rootPredicate: rootPredicate,
depositFnName: depositFnName,
rootToken: rootToken,
mintFnName: mintFnName,
rootTokenAddr: rootTokenAddr,
rootPredicateAddr: rootPredicateAddr,
childPredicateAddr: childPredicateAddr,
}
}

// lookupTokenType looks up for provided token type string and returns resolved enum value if found
func lookupTokenType(tokenTypeRaw string) (TokenType, bool) {
tokenType, ok := tokenTypesMap[strings.ToLower(tokenTypeRaw)]

return tokenType, ok
}
61 changes: 61 additions & 0 deletions command/bridge/deposit/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package deposit

import (
"errors"
"fmt"
"os"
)

const (
manifestPathFlag = "manifest"
tokenFlag = "token"
contractFlag = "contract"
receiversFlag = "receivers"
amountsFlag = "amounts"
jsonRPCFlag = "json-rpc"
depositorKeyFlag = "depositor-key"
)

var (
errReceiversMissing = errors.New("receivers flag value is not provided")
errAmountsMissing = errors.New("amount flag value is not provided")
errInconsistentAccounts = errors.New("receivers and amounts must be provided in pairs")
errDepositorKeyMissing = errors.New("depositor private key is not provided")
)

type depositParams struct {
manifestPath string
tokenTypeRaw string
receivers []string
amounts []string
jsonRPCAddress string
depositorKey string
}

func (dp *depositParams) validateFlags() error {
if dp.depositorKey == "" {
return errDepositorKeyMissing
}

if _, err := os.Stat(dp.manifestPath); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("provided manifest path '%s' doesn't exist", dp.manifestPath)
}

if len(dp.receivers) == 0 {
return errReceiversMissing
}

if len(dp.amounts) == 0 {
return errAmountsMissing
}

if len(dp.receivers) != len(dp.amounts) {
return errInconsistentAccounts
}

if _, exists := lookupTokenType(dp.tokenTypeRaw); !exists {
return fmt.Errorf("unrecognized token type provided: %s", dp.tokenTypeRaw)
}

return nil
}
Loading

0 comments on commit ee35129

Please sign in to comment.