Skip to content

Commit 33517e9

Browse files
committed
refactor: reduce code duplication in ulxly bridge and claim subcommands
Move shared functions to the common package: - ParseDepositCountFromTransaction and ParseBridgeDepositCount for bridge commands - GetDepositWhenReadyForClaim, GetDeposit, and GetMerkleProofsExitRoots for claim commands - ErrNotReadyForClaim and ErrDepositAlreadyClaimed error sentinels This removes ~355 lines of duplicated code across the bridge/asset, bridge/message, bridge/weth, claim/asset, and claim/message subcommands.
1 parent 1cf6656 commit 33517e9

File tree

11 files changed

+952
-0
lines changed

11 files changed

+952
-0
lines changed

cmd/ulxly/bridge/asset/cmd.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Package asset provides the bridge asset command.
2+
package asset
3+
4+
import (
5+
_ "embed"
6+
"math/big"
7+
"strings"
8+
9+
"github.com/0xPolygon/polygon-cli/bindings/tokens"
10+
ulxlycommon "github.com/0xPolygon/polygon-cli/cmd/ulxly/common"
11+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
12+
"github.com/ethereum/go-ethereum/common"
13+
"github.com/rs/zerolog/log"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
//go:embed usage.md
18+
var usage string
19+
20+
// Cmd represents the bridge asset command.
21+
var Cmd = &cobra.Command{
22+
Use: "asset",
23+
Short: "Move ETH or an ERC20 between to chains.",
24+
Long: usage,
25+
PreRunE: ulxlycommon.PrepInputs,
26+
RunE: bridgeAsset,
27+
SilenceUsage: true,
28+
}
29+
30+
func bridgeAsset(cmd *cobra.Command, _ []string) error {
31+
bridgeAddr := ulxlycommon.InputArgs.BridgeAddress
32+
privateKey := ulxlycommon.InputArgs.PrivateKey
33+
gasLimit := ulxlycommon.InputArgs.GasLimit
34+
destinationAddress := ulxlycommon.InputArgs.DestAddress
35+
chainID := ulxlycommon.InputArgs.ChainID
36+
amount := ulxlycommon.InputArgs.Value
37+
tokenAddr := ulxlycommon.InputArgs.TokenAddress
38+
callDataString := ulxlycommon.InputArgs.CallData
39+
destinationNetwork := ulxlycommon.InputArgs.DestNetwork
40+
isForced := ulxlycommon.InputArgs.ForceUpdate
41+
timeoutTxnReceipt := ulxlycommon.InputArgs.Timeout
42+
rpcURL := ulxlycommon.InputArgs.RPCURL
43+
44+
client, err := ulxlycommon.CreateEthClient(cmd.Context(), rpcURL)
45+
if err != nil {
46+
log.Error().Err(err).Msg("Unable to Dial RPC")
47+
return err
48+
}
49+
defer client.Close()
50+
51+
// Initialize and assign variables required to send transaction payload
52+
bridgeV2, toAddress, auth, err := ulxlycommon.GenerateTransactionPayload(cmd.Context(), client, bridgeAddr, privateKey, gasLimit, destinationAddress, chainID)
53+
if err != nil {
54+
log.Error().Err(err).Msg("error generating transaction payload")
55+
return err
56+
}
57+
58+
bridgeAddress := common.HexToAddress(bridgeAddr)
59+
value, _ := big.NewInt(0).SetString(amount, 0)
60+
tokenAddress := common.HexToAddress(tokenAddr)
61+
callData := common.Hex2Bytes(strings.TrimPrefix(callDataString, "0x"))
62+
63+
if tokenAddress == common.HexToAddress("0x0000000000000000000000000000000000000000") {
64+
auth.Value = value
65+
} else {
66+
// in case it's a token transfer, we need to ensure that the bridge contract
67+
// has enough allowance to transfer the tokens on behalf of the user
68+
tokenContract, iErr := tokens.NewERC20(tokenAddress, client)
69+
if iErr != nil {
70+
log.Error().Err(iErr).Msg("error getting token contract")
71+
return iErr
72+
}
73+
74+
allowance, iErr := tokenContract.Allowance(&bind.CallOpts{Pending: false}, auth.From, bridgeAddress)
75+
if iErr != nil {
76+
log.Error().Err(iErr).Msg("error getting token allowance")
77+
return iErr
78+
}
79+
80+
if allowance.Cmp(value) < 0 {
81+
log.Info().
82+
Str("amount", value.String()).
83+
Str("tokenAddress", tokenAddress.String()).
84+
Str("bridgeAddress", bridgeAddress.String()).
85+
Str("userAddress", auth.From.String()).
86+
Msg("approving bridge contract to spend tokens on behalf of user")
87+
88+
// Approve the bridge contract to spend the tokens on behalf of the user
89+
approveTxn, iErr := tokenContract.Approve(auth, bridgeAddress, value)
90+
if iErr = ulxlycommon.LogAndReturnJSONError(cmd.Context(), client, approveTxn, auth, iErr); iErr != nil {
91+
return iErr
92+
}
93+
log.Info().Msg("approveTxn: " + approveTxn.Hash().String())
94+
if iErr = ulxlycommon.WaitMineTransaction(cmd.Context(), client, approveTxn, timeoutTxnReceipt); iErr != nil {
95+
return iErr
96+
}
97+
}
98+
}
99+
100+
bridgeTxn, err := bridgeV2.BridgeAsset(auth, destinationNetwork, toAddress, value, tokenAddress, isForced, callData)
101+
if err = ulxlycommon.LogAndReturnJSONError(cmd.Context(), client, bridgeTxn, auth, err); err != nil {
102+
log.Info().Err(err).Str("calldata", callDataString).Msg("Bridge transaction failed")
103+
return err
104+
}
105+
log.Info().Msg("bridgeTxn: " + bridgeTxn.Hash().String())
106+
if err = ulxlycommon.WaitMineTransaction(cmd.Context(), client, bridgeTxn, timeoutTxnReceipt); err != nil {
107+
return err
108+
}
109+
depositCount, err := ulxlycommon.ParseDepositCountFromTransaction(cmd.Context(), client, bridgeTxn.Hash(), bridgeV2)
110+
if err != nil {
111+
return err
112+
}
113+
114+
log.Info().Uint32("depositCount", depositCount).Msg("Bridge deposit count parsed from logs")
115+
return nil
116+
}

cmd/ulxly/bridge/asset/usage.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
This command will directly attempt to make a deposit on the uLxLy bridge. This call responds to the method defined below:
2+
3+
```solidity
4+
/**
5+
* @notice Deposit add a new leaf to the merkle tree
6+
* note If this function is called with a reentrant token, it would be possible to `claimTokens` in the same call
7+
* Reducing the supply of tokens on this contract, and actually locking tokens in the contract.
8+
* Therefore we recommend to third parties bridges that if they do implement reentrant call of `beforeTransfer` of some reentrant tokens
9+
* do not call any external address in that case
10+
* note User/UI must be aware of the existing/available networks when choosing the destination network
11+
* @param destinationNetwork Network destination
12+
* @param destinationAddress Address destination
13+
* @param amount Amount of tokens
14+
* @param token Token address, 0 address is reserved for ether
15+
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
16+
* @param permitData Raw data of the call `permit` of the token
17+
*/
18+
function bridgeAsset(
19+
uint32 destinationNetwork,
20+
address destinationAddress,
21+
uint256 amount,
22+
address token,
23+
bool forceUpdateGlobalExitRoot,
24+
bytes calldata permitData
25+
) public payable virtual ifNotEmergencyState nonReentrant {
26+
```
27+
28+
The source of this method is [here](https://github.com/0xPolygonHermez/zkevm-contracts/blob/c8659e6282340de7bdb8fdbf7924a9bd2996bc98/contracts/v2/PolygonZkEVMBridgeV2.sol#L198-L219).
29+
Below is an example of how we would make simple bridge of native ETH from Sepolia (L1) into Cardona (L2).
30+
31+
```bash
32+
polycli ulxly bridge asset \
33+
--bridge-address 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582 \
34+
--private-key 0x32430699cd4f46ab2422f1df4ad6546811be20c9725544e99253a887e971f92b \
35+
--destination-network 1 \
36+
--value 10000000000000000 \
37+
--rpc-url https://sepolia.drpc.org
38+
```
39+
40+
[This](https://sepolia.etherscan.io/tx/0xf57b8171b2f62dce3eedbe3e50d5ee8413d61438af64286b5017ed9d5d154816) is the transaction that was created and mined from running this command.
41+
42+
Here is another example that will bridge a [test ERC20 token](https://sepolia.etherscan.io/address/0xC92AeF5873d058a76685140F3328B0DED79733Af) from Sepolia (L1) into Cardona (L2). In order for this to work, the token would need to have an [approval](https://sepolia.etherscan.io/tx/0x028513b13a2a7899de4db56e60d1dad66c7b7e29f91c54f385fdfdfc8f14b8b4#eventlog) for the bridge to spend tokens for that particular user.
43+
44+
```bash
45+
polycli ulxly bridge asset \
46+
--bridge-address 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582 \
47+
--private-key 0x32430699cd4f46ab2422f1df4ad6546811be20c9725544e99253a887e971f92b \
48+
--destination-network 1 \
49+
--value 10000000000000000 \
50+
--token-address 0xC92AeF5873d058a76685140F3328B0DED79733Af \
51+
--destination-address 0x3878Cff9d621064d393EEF92bF1e12A944c5ba84 \
52+
--rpc-url https://sepolia.drpc.org
53+
```
54+
55+
[This](https://sepolia.etherscan.io/tx/0x8ed1c2c0f2e994c86867f401c86fea3c709a28a18629d473cf683049f176fa93) is the transaction that was created and mined from running this command.
56+
57+
Assuming you have funds on L2, a bridge from L2 to L1 looks pretty much the same.
58+
The command below will bridge `123456` of the native ETH on Cardona (L2) back to network 0 which corresponds to Sepolia (L1).
59+
60+
```bash
61+
polycli ulxly bridge asset \
62+
--bridge-address 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582 \
63+
--private-key 0x32430699cd4f46ab2422f1df4ad6546811be20c9725544e99253a887e971f92b \
64+
--destination-network 0 \
65+
--value 123456 \
66+
--destination-address 0x3878Cff9d621064d393EEF92bF1e12A944c5ba84 \
67+
--rpc-url https://rpc.cardona.zkevm-rpc.com
68+
```
69+
70+
[This](https://cardona-zkevm.polygonscan.com/tx/0x0294dae3cfb26881e5dde9f182531aa5be0818956d029d50e9872543f020df2e) is the transaction that was created and mined from running this command.

cmd/ulxly/bridge/message/cmd.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Package message provides the bridge message command.
2+
package message
3+
4+
import (
5+
_ "embed"
6+
"math/big"
7+
"strings"
8+
9+
ulxlycommon "github.com/0xPolygon/polygon-cli/cmd/ulxly/common"
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/rs/zerolog/log"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
//go:embed usage.md
16+
var usage string
17+
18+
// Cmd represents the bridge message command.
19+
var Cmd = &cobra.Command{
20+
Use: "message",
21+
Short: "Send some ETH along with data from one chain to another chain.",
22+
Long: usage,
23+
PreRunE: ulxlycommon.PrepInputs,
24+
RunE: bridgeMessage,
25+
SilenceUsage: true,
26+
}
27+
28+
func bridgeMessage(cmd *cobra.Command, _ []string) error {
29+
bridgeAddress := ulxlycommon.InputArgs.BridgeAddress
30+
privateKey := ulxlycommon.InputArgs.PrivateKey
31+
gasLimit := ulxlycommon.InputArgs.GasLimit
32+
destinationAddress := ulxlycommon.InputArgs.DestAddress
33+
chainID := ulxlycommon.InputArgs.ChainID
34+
amount := ulxlycommon.InputArgs.Value
35+
tokenAddr := ulxlycommon.InputArgs.TokenAddress
36+
callDataString := ulxlycommon.InputArgs.CallData
37+
destinationNetwork := ulxlycommon.InputArgs.DestNetwork
38+
isForced := ulxlycommon.InputArgs.ForceUpdate
39+
timeoutTxnReceipt := ulxlycommon.InputArgs.Timeout
40+
rpcURL := ulxlycommon.InputArgs.RPCURL
41+
42+
// Dial the Ethereum RPC server.
43+
client, err := ulxlycommon.CreateEthClient(cmd.Context(), rpcURL)
44+
if err != nil {
45+
log.Error().Err(err).Msg("Unable to Dial RPC")
46+
return err
47+
}
48+
defer client.Close()
49+
50+
// Initialize and assign variables required to send transaction payload
51+
bridgeV2, toAddress, auth, err := ulxlycommon.GenerateTransactionPayload(cmd.Context(), client, bridgeAddress, privateKey, gasLimit, destinationAddress, chainID)
52+
if err != nil {
53+
log.Error().Err(err).Msg("error generating transaction payload")
54+
return err
55+
}
56+
57+
value, _ := big.NewInt(0).SetString(amount, 0)
58+
tokenAddress := common.HexToAddress(tokenAddr)
59+
callData := common.Hex2Bytes(strings.TrimPrefix(callDataString, "0x"))
60+
61+
if tokenAddress == common.HexToAddress("0x0000000000000000000000000000000000000000") {
62+
auth.Value = value
63+
}
64+
65+
bridgeTxn, err := bridgeV2.BridgeMessage(auth, destinationNetwork, toAddress, isForced, callData)
66+
if err = ulxlycommon.LogAndReturnJSONError(cmd.Context(), client, bridgeTxn, auth, err); err != nil {
67+
log.Info().Err(err).Str("calldata", callDataString).Msg("Bridge transaction failed")
68+
return err
69+
}
70+
log.Info().Msg("bridgeTxn: " + bridgeTxn.Hash().String())
71+
if err = ulxlycommon.WaitMineTransaction(cmd.Context(), client, bridgeTxn, timeoutTxnReceipt); err != nil {
72+
return err
73+
}
74+
depositCount, err := ulxlycommon.ParseDepositCountFromTransaction(cmd.Context(), client, bridgeTxn.Hash(), bridgeV2)
75+
if err != nil {
76+
return err
77+
}
78+
79+
log.Info().Uint32("depositCount", depositCount).Msg("Bridge deposit count parsed from logs")
80+
return nil
81+
}

cmd/ulxly/bridge/message/usage.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
This command is very similar to `polycli ulxly bridge asset`, but instead this is a more generic interface that can be used to transfer ETH and make a contract call. This is the underlying solidity interface that we're referencing.
2+
3+
```solidity
4+
/**
5+
* @notice Bridge message and send ETH value
6+
* note User/UI must be aware of the existing/available networks when choosing the destination network
7+
* @param destinationNetwork Network destination
8+
* @param destinationAddress Address destination
9+
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
10+
* @param metadata Message metadata
11+
*/
12+
function bridgeMessage(
13+
uint32 destinationNetwork,
14+
address destinationAddress,
15+
bool forceUpdateGlobalExitRoot,
16+
bytes calldata metadata
17+
) external payable ifNotEmergencyState {
18+
```
19+
20+
The source code for this particular method is [here](https://github.com/0xPolygonHermez/zkevm-contracts/blob/c8659e6282340de7bdb8fdbf7924a9bd2996bc98/contracts/v2/PolygonZkEVMBridgeV2.sol#L324-L337).
21+
22+
Below is a simple example of using this command to bridge a small amount of ETH from Sepolia (L1) to Cardona (L2). In this case, we're not including any call data, so it's essentially equivalent to a `bridge asset` call, but the deposit will not be automatically claimed on L2.
23+
24+
```bash
25+
polycli ulxly bridge message \
26+
--bridge-address 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582 \
27+
--private-key 0x32430699cd4f46ab2422f1df4ad6546811be20c9725544e99253a887e971f92b \
28+
--destination-network 1 \
29+
--value 10000000000000000 \
30+
--rpc-url https://sepolia.drpc.org
31+
```
32+
33+
[This](https://sepolia.etherscan.io/tx/0x1a6e2be69fa65e866889d95403b2fe820f08b6a07b96c6afbde646b8092addb2) is the transaction that was generated and mined from this command.
34+
35+
In most cases, you'll want to specify some `call-data` and a `destination-address` in order for a contract to be called on the destination chain. For example:
36+
```bash
37+
polycli ulxly bridge message \
38+
--bridge-address 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582 \
39+
--private-key 0x32430699cd4f46ab2422f1df4ad6546811be20c9725544e99253a887e971f92b \
40+
--destination-network 1 \
41+
--destination-address 0xC92AeF5873d058a76685140F3328B0DED79733Af \
42+
--call-data 0x40c10f190000000000000000000000003878cff9d621064d393eef92bf1e12a944c5ba84000000000000000000000000000000000000000000000000002386f26fc10000 \
43+
--value 0 \
44+
--rpc-url https://sepolia.drpc.org
45+
```
46+
[This](https://sepolia.etherscan.io/tx/0x517b9d827a3a81770d608a6b997e230d992e1e0cabc0fd2797285693b1cc6a9f) is the transaction that was created and mined from running the above command.
47+
48+
In this case, I've configured the destination address to be a test contract I've deployed on L2.
49+
```soldity
50+
// SPDX-License-Identifier: AGPL-3.0
51+
pragma solidity 0.8.20;
52+
53+
contract MessageEmitter {
54+
event MessageReceived (address originAddress, uint32 originNetwork, bytes data);
55+
56+
function onMessageReceived(address originAddress, uint32 originNetwork, bytes memory data) external payable {
57+
emit MessageReceived(originAddress, originNetwork, data);
58+
}
59+
}
60+
```
61+
62+
The idea is to have minimal contract that will meet the expected interface of the bridge contract: https://github.com/0xPolygonHermez/zkevm-contracts/blob/v9.0.0-rc.3-pp/contracts/interfaces/IBridgeMessageReceiver.sol
63+
64+
In this case, I didn't bother implementing the proxy to an ERC20 or extending some ERC20 contract. I'm just emitting an event to know that the transaction actually fired as expected.
65+
The calldata comes from running this command `cast calldata 'mint(address account, uint256 amount)' 0x3878Cff9d621064d393EEF92bF1e12A944c5ba84 10000000000000000`. Again, in this case no ERC20 will be minted because I didn't set it up.
66+

0 commit comments

Comments
 (0)