Skip to content

Commit c0917a2

Browse files
mrice32nicholaspai
andauthored
feat: ZkStack Adapters (#628)
* WIP Signed-off-by: Matt Rice <[email protected]> * WIP Signed-off-by: Matt Rice <[email protected]> * fix Signed-off-by: Matt Rice <[email protected]> * Update contracts/chain-adapters/ZkStack_Adapter.sol Co-authored-by: nicholaspai <[email protected]> --------- Signed-off-by: Matt Rice <[email protected]> Co-authored-by: nicholaspai <[email protected]>
1 parent c376d5c commit c0917a2

File tree

3 files changed

+590
-1
lines changed

3 files changed

+590
-1
lines changed

contracts/chain-adapters/DonationBox.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
77

88
/**
99
* @notice Users can donate tokens to this contract that only the owner can withdraw.
10-
* @dev This contract is designed to be used as a convience for the owner to store funds to pay for
10+
* @dev This contract is designed to be used as a convenience for the owner to store funds to pay for
1111
* future transactions, such as donating custom gas tokens to pay for future retryable ticket messages
1212
* to be sent via the Arbitrum_Adapter.
1313
* @custom:security-contact [email protected]
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import "./interfaces/AdapterInterface.sol";
5+
import "../external/interfaces/WETH9Interface.sol";
6+
7+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9+
10+
// The BridgeHub is the main interaction point for bridging into ZkStack chains.
11+
interface BridgeHubInterface {
12+
struct L2TransactionRequestDirect {
13+
uint256 chainId;
14+
uint256 mintValue;
15+
address l2Contract;
16+
uint256 l2Value;
17+
bytes l2Calldata;
18+
uint256 l2GasLimit;
19+
uint256 l2GasPerPubdataByteLimit;
20+
bytes[] factoryDeps;
21+
address refundRecipient;
22+
}
23+
24+
/**
25+
* @notice the mailbox is called directly after the sharedBridge received the deposit.
26+
* This assumes that either ether is the base token or the msg.sender has approved mintValue allowance for the
27+
* sharedBridge. This means this is not ideal for contract calls, as the contract would have to handle token
28+
* allowance of the base Token.
29+
* @param _request the direct request.
30+
*/
31+
function requestL2TransactionDirect(L2TransactionRequestDirect calldata _request)
32+
external
33+
payable
34+
returns (bytes32 canonicalTxHash);
35+
36+
struct L2TransactionRequestTwoBridgesOuter {
37+
uint256 chainId;
38+
uint256 mintValue;
39+
uint256 l2Value;
40+
uint256 l2GasLimit;
41+
uint256 l2GasPerPubdataByteLimit;
42+
address refundRecipient;
43+
address secondBridgeAddress;
44+
uint256 secondBridgeValue;
45+
bytes secondBridgeCalldata;
46+
}
47+
48+
/**
49+
* @notice After depositing funds to the sharedBridge, the secondBridge is called to return the actual L2 message
50+
* which is sent to the Mailbox. This assumes that either ether is the base token or the msg.sender has approved
51+
* the sharedBridge with the mintValue, and also the necessary approvals are given for the second bridge. The logic
52+
* of this bridge is to allow easy depositing for bridges. Each contract that handles the users ERC20 tokens needs
53+
* approvals from the user, this contract allows the user to approve for each token only its respective bridge.
54+
* This function is great for contract calls to L2, the secondBridge can be any contract.
55+
* @param _request the two bridges request.
56+
*/
57+
function requestL2TransactionTwoBridges(L2TransactionRequestTwoBridgesOuter calldata _request)
58+
external
59+
payable
60+
returns (bytes32 canonicalTxHash);
61+
62+
/**
63+
* @notice Gets the shared bridge.
64+
* @dev The shared bridge manages ERC20 tokens.
65+
*/
66+
function sharedBridge() external view returns (address);
67+
68+
/**
69+
* @notice Gets the base token for a chain.
70+
* @dev Base token == native token.
71+
*/
72+
function baseToken(uint256 _chainId) external view returns (address);
73+
74+
/**
75+
* @notice Computes the base transaction cost for a transaction.
76+
* @param _chainId the chain the transaction is being sent to.
77+
* @param _gasPrice the l1 gas price at time of execution.
78+
* @param _l2GasLimit the gas limit for the l2 transaction.
79+
* @param _l2GasPerPubdataByteLimit configuration value that changes infrequently.
80+
*/
81+
function l2TransactionBaseCost(
82+
uint256 _chainId,
83+
uint256 _gasPrice,
84+
uint256 _l2GasLimit,
85+
uint256 _l2GasPerPubdataByteLimit
86+
) external view returns (uint256);
87+
}
88+
89+
/**
90+
* @notice Contract containing logic to send messages from L1 to ZkStack with ETH as the gas token.
91+
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
92+
* called via delegatecall, which will execute this contract's logic within the context of the originating contract.
93+
* For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods
94+
* that call this contract's logic guard against reentrancy.
95+
* @custom:security-contact [email protected]
96+
*/
97+
98+
// solhint-disable-next-line contract-name-camelcase
99+
contract ZkStack_Adapter is AdapterInterface {
100+
using SafeERC20 for IERC20;
101+
102+
// We need to pay a base fee to the operator to include our L1 --> L2 transaction.
103+
// https://era.zksync.io/docs/dev/developer-guides/bridging/l1-l2.html#getting-the-base-cost
104+
105+
// Limit on L2 gas to spend.
106+
uint256 public immutable L2_GAS_LIMIT; // typically 2_000_000
107+
108+
// How much gas is required to publish a byte of data from L1 to L2. 800 is the required value
109+
// as set here https://github.com/matter-labs/era-contracts/blob/6391c0d7bf6184d7f6718060e3991ba6f0efe4a7/ethereum/contracts/zksync/facets/Mailbox.sol#L226
110+
// Note, this value can change and will require an updated adapter.
111+
uint256 public immutable L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT; // Typically 800
112+
113+
// This address receives any remaining fee after an L1 to L2 transaction completes.
114+
// If refund recipient = address(0) then L2 msg.sender is used, unless msg.sender is a contract then its address
115+
// gets aliased.
116+
address public immutable L2_REFUND_ADDRESS;
117+
118+
// L2 chain id
119+
uint256 public immutable CHAIN_ID;
120+
121+
// BridgeHub address
122+
BridgeHubInterface public immutable BRIDGE_HUB;
123+
124+
// Set l1Weth at construction time to make testing easier.
125+
WETH9Interface public immutable L1_WETH;
126+
127+
// SharedBridge address, which is read from the BridgeHub at construction.
128+
address public immutable SHARED_BRIDGE;
129+
130+
event ZkStackMessageRelayed(bytes32 canonicalTxHash);
131+
error ETHGasTokenRequired();
132+
133+
/**
134+
* @notice Constructs new Adapter.
135+
* @param _l1Weth WETH address on L1.
136+
* @param _l2RefundAddress address that recieves excess gas refunds on L2.
137+
*/
138+
constructor(
139+
uint256 _chainId,
140+
BridgeHubInterface _bridgeHub,
141+
WETH9Interface _l1Weth,
142+
address _l2RefundAddress,
143+
uint256 _l2GasLimit,
144+
uint256 _l1GasToL2GasPerPubDataLimit
145+
) {
146+
CHAIN_ID = _chainId;
147+
BRIDGE_HUB = _bridgeHub;
148+
L1_WETH = _l1Weth;
149+
L2_REFUND_ADDRESS = _l2RefundAddress;
150+
L2_GAS_LIMIT = _l2GasLimit;
151+
L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT = _l1GasToL2GasPerPubDataLimit;
152+
SHARED_BRIDGE = BRIDGE_HUB.sharedBridge();
153+
address gasToken = BRIDGE_HUB.baseToken(CHAIN_ID);
154+
if (gasToken != address(1)) {
155+
revert ETHGasTokenRequired();
156+
}
157+
}
158+
159+
/**
160+
* @notice Send cross-chain message to target on ZkStack.
161+
* @dev The HubPool must hold enough ETH to pay for the L2 txn.
162+
* @param target Contract on L2 that will receive message.
163+
* @param message Data to send to target.
164+
*/
165+
function relayMessage(address target, bytes memory message) external payable override {
166+
uint256 txBaseCost = _computeETHTxCost(L2_GAS_LIMIT);
167+
168+
// Returns the hash of the requested L2 transaction. This hash can be used to follow the transaction status.
169+
bytes32 canonicalTxHash = BRIDGE_HUB.requestL2TransactionDirect{ value: txBaseCost }(
170+
BridgeHubInterface.L2TransactionRequestDirect({
171+
chainId: CHAIN_ID,
172+
mintValue: txBaseCost,
173+
l2Contract: target,
174+
l2Value: 0,
175+
l2Calldata: message,
176+
l2GasLimit: L2_GAS_LIMIT,
177+
l2GasPerPubdataByteLimit: L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT,
178+
factoryDeps: new bytes[](0),
179+
refundRecipient: L2_REFUND_ADDRESS
180+
})
181+
);
182+
183+
emit MessageRelayed(target, message);
184+
emit ZkStackMessageRelayed(canonicalTxHash);
185+
}
186+
187+
/**
188+
* @notice Bridge tokens to ZkStack.
189+
* @dev The HubPool must hold enough ETH to pay for the L2 txn.
190+
* @param l1Token L1 token to deposit.
191+
* @param l2Token L2 token to receive.
192+
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
193+
* @param to Bridge recipient.
194+
*/
195+
function relayTokens(
196+
address l1Token,
197+
address l2Token, // l2Token is unused.
198+
uint256 amount,
199+
address to
200+
) external payable override {
201+
// A bypass proxy seems to no longer be needed to avoid deposit limits. The tracking of these limits seems to be deprecated.
202+
// See: https://github.com/matter-labs/era-contracts/blob/bce4b2d0f34bd87f1aaadd291772935afb1c3bd6/l1-contracts/contracts/bridge/L1ERC20Bridge.sol#L54-L55
203+
uint256 txBaseCost = _computeETHTxCost(L2_GAS_LIMIT);
204+
205+
bytes32 txHash;
206+
if (l1Token == address(L1_WETH)) {
207+
// If the l1Token is WETH then unwrap it to ETH then send the ETH to the standard bridge along with the base
208+
// cost.
209+
L1_WETH.withdraw(amount);
210+
txHash = BRIDGE_HUB.requestL2TransactionDirect{ value: amount + txBaseCost }(
211+
BridgeHubInterface.L2TransactionRequestDirect({
212+
chainId: CHAIN_ID,
213+
mintValue: txBaseCost,
214+
l2Contract: to,
215+
l2Value: 0,
216+
l2Calldata: "",
217+
l2GasLimit: L2_GAS_LIMIT,
218+
l2GasPerPubdataByteLimit: L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT,
219+
factoryDeps: new bytes[](0),
220+
refundRecipient: L2_REFUND_ADDRESS
221+
})
222+
);
223+
} else {
224+
// An ERC20 that is not WETH.
225+
IERC20(l1Token).safeIncreaseAllowance(SHARED_BRIDGE, amount);
226+
txHash = BRIDGE_HUB.requestL2TransactionTwoBridges{ value: txBaseCost }(
227+
BridgeHubInterface.L2TransactionRequestTwoBridgesOuter({
228+
chainId: CHAIN_ID,
229+
mintValue: txBaseCost,
230+
l2Value: 0,
231+
l2GasLimit: L2_GAS_LIMIT,
232+
l2GasPerPubdataByteLimit: L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT,
233+
refundRecipient: L2_REFUND_ADDRESS,
234+
secondBridgeAddress: BRIDGE_HUB.sharedBridge(),
235+
secondBridgeValue: 0,
236+
secondBridgeCalldata: _secondBridgeCalldata(to, l1Token, amount)
237+
})
238+
);
239+
}
240+
241+
emit TokensRelayed(l1Token, l2Token, amount, to);
242+
emit ZkStackMessageRelayed(txHash);
243+
}
244+
245+
/**
246+
* @notice Computes the calldata for the "second bridge", which handles sending non native tokens.
247+
* @param l2Recipient recipient of the tokens.
248+
* @param l1Token the l1 address of the token. Note: ETH is encoded as address(1).
249+
* @param amount number of tokens to send.
250+
* @return abi encoded bytes.
251+
*/
252+
function _secondBridgeCalldata(
253+
address l2Recipient,
254+
address l1Token,
255+
uint256 amount
256+
) internal pure returns (bytes memory) {
257+
return abi.encode(l1Token, amount, l2Recipient);
258+
}
259+
260+
/**
261+
* @notice For a given l2 gas limit, this computes the amount of ETH needed and
262+
* returns the amount.
263+
* @param l2GasLimit L2 gas limit for the message.
264+
* @return amount of ETH that this contract needs to provide in order for the l2 transaction to succeed.
265+
*/
266+
function _computeETHTxCost(uint256 l2GasLimit) internal view returns (uint256) {
267+
return BRIDGE_HUB.l2TransactionBaseCost(CHAIN_ID, tx.gasprice, l2GasLimit, L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT);
268+
}
269+
}

0 commit comments

Comments
 (0)