forked from 0xPolygon/polygon-edge
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ERC20 token bridge (deposit workflow) (0xPolygon#1242)
* 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
1 parent
5aea95d
commit ee35129
Showing
30 changed files
with
988 additions
and
517 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
¶ms.manifestPath, | ||
manifestPathFlag, | ||
"./manifest.json", | ||
"the manifest file path, which contains genesis metadata", | ||
) | ||
|
||
cmd.Flags().StringVar( | ||
¶ms.tokenTypeRaw, | ||
tokenFlag, | ||
"erc20", | ||
"token type which is being deposited", | ||
) | ||
|
||
cmd.Flags().StringSliceVar( | ||
¶ms.receivers, | ||
receiversFlag, | ||
nil, | ||
"receiving accounts addresses on child chain", | ||
) | ||
|
||
cmd.Flags().StringSliceVar( | ||
¶ms.amounts, | ||
amountsFlag, | ||
nil, | ||
"amounts to send to child chain receiving accounts", | ||
) | ||
|
||
cmd.Flags().StringVar( | ||
¶ms.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( | ||
¶ms.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 ðgo.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 ðgo.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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.