Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ typechain-types
#Hardhat files
cache
artifacts
cache-zk
artifacts-zk
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ yarn lint-fix
NODE_URL_1=https://mainnet.infura.com/xxx yarn hardhat deploy --tags HubPool --network mainnet
ETHERSCAN_API_KEY=XXX yarn hardhat etherscan-verify --network mainnet --license AGPL-3.0 --force-license --solc-input
```

## ZK Sync Adapter

These are special instructions for compiling and deploying contracts on `zksync`. The compile command will create `artifacts-zk` and `cache-zk` directories.

### Compile

This step requires [Docker Desktop](https://www.docker.com/products/docker-desktop/) to be running, as the `solc` docker image is fetched as a prerequisite.

`yarn compile-zksync`
108 changes: 108 additions & 0 deletions contracts/ZkSync_SpokePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;

import "./SpokePool.sol";

interface ZkBridgeLike {
function withdraw(
address _to,
address _l2Token,
uint256 _amount
) external;
}

/**
* @notice ZkSync specific SpokePool, intended to be compiled with `@matterlabs/hardhat-zksync-solc`.
*/
contract ZkSync_SpokePool is SpokePool {
// On Ethereum, avoiding constructor parameters and putting them into constants reduces some of the gas cost
// upon contract deployment. On zkSync the opposite is true: deploying the same bytecode for contracts,
// while changing only constructor parameters can lead to substantial fee savings. So, the following params
// are all set by passing in constructor params where possible.
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't expect to deploy many spoke pool contracts, right?

I think this mostly comes down to reads, since these values will be read for more than they will be deployed. Is reading directly from immutable bytecode cheaper or is reading from mutable storage cheaper? On ethereum mutable storage is far more expensive to read than immutable bytecode.

However, don't we typically make keep these bridge variables mutable to allow upgradability?

I guess I'm just a little confused about which variables this comment applies to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we typically keep these variables mutable but perhaps the comment is misleading. I only included it for now as a consideration but I agree this contract should be rarely re-redeployed so i'll add that note


// However, this contract is expected to be deployed only once to ZkSync. Therefore, we should consider the cost
// of reading mutable vs immutable storage. On Ethereum, mutable storage is more expensive than immutable bytecode.
// But, we also want to be able to upgrade certain state variables.

// Bridge used to withdraw ERC20's to L1: https://github.com/matter-labs/v2-testnet-contracts/blob/3a0651357bb685751c2163e4cc65a240b0f602ef/l2/contracts/bridge/L2ERC20Bridge.sol
ZkBridgeLike public zkErc20Bridge;

// Bridge used to send ETH to L1: https://github.com/matter-labs/v2-testnet-contracts/blob/3a0651357bb685751c2163e4cc65a240b0f602ef/l2/contracts/bridge/L2ETHBridge.sol
ZkBridgeLike public zkEthBridge;

event SetZkBridges(address indexed erc20Bridge, address indexed ethBridge);
event ZkSyncTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged);

/**
* @notice Construct the ZkSync SpokePool.
* @param _zkErc20Bridge Address of L2 ERC20 gateway. Can be reset by admin.
* @param _zkEthBridge Address of L2 ETH gateway. Can be reset by admin.
* @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin.
* @param _hubPool Hub pool address to set. Can be changed by admin.
* @param _wethAddress Weth address for this network to set.
* @param timerAddress Timer address to set.
*/
constructor(
ZkBridgeLike _zkErc20Bridge,
ZkBridgeLike _zkEthBridge,
address _crossDomainAdmin,
address _hubPool,
address _wethAddress,
address timerAddress
) SpokePool(_crossDomainAdmin, _hubPool, _wethAddress, timerAddress) {
_setZkBridges(_zkErc20Bridge, _zkEthBridge);
}

modifier onlyFromCrossDomainAdmin() {
// Formal msg.sender of L1 --> L2 message will be L1 sender.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to be super clear here. The msg.sender that initiates a bridge transaction on L1 will have the transaction on L2 reflect that exact same msg.sender address? There's no aliasing, like on arbitrum?

Consider the following scenario:

  1. Wallet 0x1234 deploys contract A 0x5678 on mainnet in it's transaction with nonce 10.
  2. Wallet 0x1234 deploys a completely different contract (contract B) on Zk-Sync at 0x5678 by deploying it at nonce 10.
  3. Contract A sends a bridge message to Zk-Sync.
  4. On Zk-Sync, this transaction "comes from" contract B despite B having no code to generate that transaction.

Is that possible? My understanding was that Arbitrum created aliases to prevent this scenario. Does Zk-Sync have something to stop that from happening?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confirming with zksync dev support but yes this is my understanding as of now. Also, empirically this authentication has worked for goerli --> zkSync goerli messages

require(msg.sender == crossDomainAdmin, "Invalid sender");
_;
}

/**
* @notice Returns chain ID for this network.
* @dev ZKSync doesn't yet support the CHAIN_ID opcode so we override this, but it will be supported by mainnet
* launch supposedly: https://v2-docs.zksync.io/dev/zksync-v2/temp-limits.html#temporarily-simulated-by-constant-values
*/
function chainId() public pure override returns (uint256) {
return 280;
}

/********************************************************
* ZKSYNC-SPECIFIC CROSS-CHAIN ADMIN FUNCTIONS *
********************************************************/

/**
* @notice Change L2 token bridge addresses. Callable only by admin.
* @param _zkErc20Bridge New address of L2 ERC20 gateway.
* @param _zkEthBridge New address of L2 ETH gateway.
*/
function setZkBridges(ZkBridgeLike _zkErc20Bridge, ZkBridgeLike _zkEthBridge) public onlyAdmin nonReentrant {
_setZkBridges(_zkErc20Bridge, _zkEthBridge);
}

/**************************************
* INTERNAL FUNCTIONS *
**************************************/

function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override {
(relayerRefundLeaf.l2TokenAddress == address(wrappedNativeToken) ? zkEthBridge : zkErc20Bridge).withdraw(
hubPool,
// Note: If ETH, must use 0x0: https://github.com/matter-labs/v2-testnet-contracts/blob/3a0651357bb685751c2163e4cc65a240b0f602ef/l2/contracts/bridge/L2ETHBridge.sol#L57
relayerRefundLeaf.l2TokenAddress == address(wrappedNativeToken)
? address(0)
: relayerRefundLeaf.l2TokenAddress,
relayerRefundLeaf.amountToReturn
);

emit ZkSyncTokensBridged(relayerRefundLeaf.l2TokenAddress, hubPool, relayerRefundLeaf.amountToReturn);
}

function _setZkBridges(ZkBridgeLike _zkErc20Bridge, ZkBridgeLike _zkEthBridge) internal {
zkErc20Bridge = _zkErc20Bridge;
zkEthBridge = _zkEthBridge;
emit SetZkBridges(address(_zkErc20Bridge), address(_zkEthBridge));
}

function _requireAdminSender() internal override onlyFromCrossDomainAdmin {}
}
148 changes: 148 additions & 0 deletions contracts/chain-adapters/ZkSync_Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import "../interfaces/AdapterInterface.sol";
import "../interfaces/WETH9.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Importing `Operations` contract which has the `QueueType` type
import "@matterlabs/zksync-contracts/l1/contracts/zksync/Operations.sol";

interface ZkSyncLike {
function requestL2Transaction(
address _contractAddressL2,
bytes calldata _calldata,
uint256 _ergsLimit,
bytes[] calldata _factoryDeps,
QueueType _queueType
) external payable returns (bytes32 txHash);
}

interface ZkBridgeLike {
function deposit(
address _to,
address _l1Token,
uint256 _amount,
QueueType _queueType
) external payable returns (bytes32 txHash);
}

/**
* @notice Contract containing logic to send messages from L1 to ZkSync.
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
* called via delegatecall, which will execute this contract's logic within the context of the originating contract.
* For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods
* that call this contract's logic guard against reentrancy.
*/

// solhint-disable-next-line contract-name-camelcase
contract ZkSync_Adapter is AdapterInterface {
using SafeERC20 for IERC20;

// We need to pay a fee to submit transactions to the L1 --> L2 priority queue:
// https://v2-docs.zksync.io/dev/zksync-v2/l1-l2-interop.html#priority-queue

// The fee for a transactionis equal to `txBaseCost * gasPrice` where `txBaseCost` depends on the ergsLimit
// (ergs = gas on ZkSync) and the calldata length. More details here:
// https://v2-docs.zksync.io/dev/guide/l1-l2.html#using-contract-interface-in-your-project

// Generally, the ergsLimit and l2GasPrice params are a bit hard to set and may change in the future once ZkSync
// is deployed to mainnet. On testnet, gas price is set to 0 and gas used is 0 so its hard to accurately forecast.
uint256 public immutable l2GasPrice = 1e9;

uint32 public immutable ergsLimit = 1_000_000;

// Hardcode WETH address for L1 since it will not change:
WETH9 public immutable l1Weth = WETH9(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6);

// Hardcode the following ZkSync system contract addresses to save gas on construction. This adapter can be
// redeployed in the event that the following addresses change.

// Main contract used to send L1 --> L2 messages. Fetchable via `zks_getMainContract` method on JSON RPC.
ZkSyncLike public immutable zkSync = ZkSyncLike(0xa0F968EbA6Bbd08F28Dc061C7856C15725983395);
// Bridges to send ERC20 and ETH to L2. Fetchable via `zks_getBridgeContracts` method on JSON RPC.
ZkBridgeLike public immutable zkErc20Bridge = ZkBridgeLike(0x7786255495348c08F82C09C82352019fAdE3BF29);
ZkBridgeLike public immutable zkEthBridge = ZkBridgeLike(0xcbebcD41CeaBBC85Da9bb67527F58d69aD4DfFf5);

event ZkSyncMessageRelayed(bytes32 txHash);

/**
* @notice Send cross-chain message to target on ZkSync.
* @notice This contract must hold at least getL1CallValue() amount of ETH to send a message, or the message
* will get stuck.
* @param target Contract on Arbitrum that will receive message.
* @param message Data to send to target.
*/
function relayMessage(address target, bytes memory message) external payable override {
uint256 txBaseCost = _contractHasSufficientEthBalance();

// Parameters passed to requestL2Transaction:
// _contractAddressL2 is a parameter that defines the address of the contract to be called.
// _calldata is a parameter that contains the calldata of the transaction call. It can be encoded the
// same way as on Ethereum.
// _ergsLimit is a parameter that contains the ergs limit of the transaction call. You can learn more about
// ergs and the zkSync fee system here: https://v2-docs.zksync.io/dev/zksync-v2/fee-model.html
// _factoryDeps is a list of bytecodes. It should contain the bytecode of the contract being deployed.
// If the contract being deployed is a factory contract, i.e. it can deploy other contracts, the array should also contain the bytecodes of the contracts that can be deployed by it.
// _queueType is a parameter required for the priority mode functionality. For the testnet,
// QueueType.Deque should always be supplied.
bytes32 txHash = zkSync.requestL2Transaction{ value: txBaseCost }(
target,
message,
ergsLimit,
new bytes[](0),
QueueType.Deque
);

emit MessageRelayed(target, message);
emit ZkSyncMessageRelayed(txHash);
}

/**
* @notice Bridge tokens to ZkSync.
* @notice This contract must hold at least getL1CallValue() amount of ETH to send a message
* or the message will get stuck.
* @param l1Token L1 token to deposit.
* @param l2Token L2 token to receive.
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
* @param to Bridge recipient.
*/
function relayTokens(
address l1Token,
address l2Token, // l2Token is unused.
uint256 amount,
address to
) external payable override {
uint256 txBaseCost = _contractHasSufficientEthBalance();

// If the l1Token is WETH then unwrap it to ETH then send the ETH to the standard bridge along with the base
// cost.
bytes32 txHash;
if (l1Token == address(l1Weth)) {
l1Weth.withdraw(amount);
// Must set L1Token address to 0x0: https://github.com/matter-labs/v2-testnet-contracts/blob/3a0651357bb685751c2163e4cc65a240b0f602ef/l1/contracts/bridge/L1EthBridge.sol#L78
txHash = zkEthBridge.deposit{ value: txBaseCost + amount }(to, address(0), amount, QueueType.Deque);
} else {
IERC20(l1Token).safeIncreaseAllowance(address(zkErc20Bridge), amount);
txHash = zkErc20Bridge.deposit{ value: txBaseCost }(to, l1Token, amount, QueueType.Deque);
}

emit TokensRelayed(l1Token, l2Token, amount, to);
emit ZkSyncMessageRelayed(txHash);
}

/**
* @notice Returns required amount of ETH to send a message.
* @return amount of ETH that this contract needs to hold in order for relayMessage to succeed.
*/
function getL1CallValue() public pure returns (uint256) {
return l2GasPrice * ergsLimit;
}

function _contractHasSufficientEthBalance() internal view returns (uint256 requiredL1CallValue) {
requiredL1CallValue = getL1CallValue();
require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance");
}
}
20 changes: 20 additions & 0 deletions deploy/015_deploy_zksync_adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "hardhat-deploy";
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime";

const func = async function (hre: HardhatRuntimeEnvironment) {
const { deployments, getNamedAccounts } = hre;

const { deploy } = deployments;

const { deployer } = await getNamedAccounts();

await deploy("ZkSync_Adapter", {
from: deployer,
log: true,
skipIfAlreadyDeployed: true,
args: [],
});
};

module.exports = func;
func.tags = ["ZkSyncAdapter", "mainnet"];
34 changes: 34 additions & 0 deletions deploy/016_deploy_zksync_spokepool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import "hardhat-deploy";
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime";

import { L2_ADDRESS_MAP } from "./consts";

const func = async function (hre: HardhatRuntimeEnvironment) {
const { companionNetworks, getChainId, getNamedAccounts, deployments } = hre;
const { deploy } = deployments;

const { deployer } = await getNamedAccounts();

// Grab L1 addresses:
const { deployments: l1Deployments } = companionNetworks.l1;
const hubPool = await l1Deployments.get("HubPool");
console.log(`Using l1 hub pool @ ${hubPool.address}`);

const chainId = parseInt(await getChainId());

await deploy("ZkSync_SpokePool", {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

from: deployer,
log: true,
skipIfAlreadyDeployed: true,
args: [
L2_ADDRESS_MAP[chainId].zkErc20Bridge,
L2_ADDRESS_MAP[chainId].zkEthBridge,
hubPool.address, // Set hub pool as cross domain admin since it delegatecalls the ZkSync_Adapter logic.
hubPool.address,
L2_ADDRESS_MAP[chainId].l2Weth, // l2Weth
"0x0000000000000000000000000000000000000000", // timer
],
});
};
module.exports = func;
func.tags = ["ZkSyncSpokePool", "zksync"];
5 changes: 5 additions & 0 deletions deploy/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
wMatic: "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889",
fxChild: "0xCf73231F28B7331BBe3124B907840A94851f9f11",
},
280: {
zkErc20Bridge: "0x92131f10c54f9b251a5deaf3c05815f7659bbe02",
zkEthBridge: "0x2c5d8a991f399089f728f1ae40bd0b11acd0fb62",
l2Weth: "0xD3765838f9600Ccff3d01EFA83496599E0984BD2",
},
};

export const POLYGON_CHAIN_IDS: { [l1ChainId: number]: number } = {
Expand Down
Loading