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
211 changes: 211 additions & 0 deletions contracts/PolygonZkEVM_SpokePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./SpokePool.sol";
import "./external/interfaces/IPolygonZkEVMBridge.sol";

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

/**
* @notice Define interface for PolygonZkEVM Bridge message receiver
* See https://github.com/0xPolygonHermez/zkevm-contracts/blob/53e95f3a236d8bea87c27cb8714a5d21496a3b20/contracts/interfaces/IBridgeMessageReceiver.sol
*/
interface IBridgeMessageReceiver {
/**
* @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool.
* @param originAddress Address of the original message sender on L1.
* @param originNetwork Polygon zkEVM's internal network id of source chain.
* @param data Data to be received and executed on this contract.
*/
function onMessageReceived(
address originAddress,
uint32 originNetwork,
bytes memory data
) external payable;
}

/**
* @notice Polygon zkEVM Spoke pool.
*/
contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver {
using SafeERC20 for IERC20;

// Address of Polygon zkEVM's Canonical Bridge on L2.
IPolygonZkEVMBridge public l2PolygonZkEVMBridge;

// Polygon zkEVM's internal network id for L1.
uint32 public constant l1NetworkId = 0;

// Warning: this variable should _never_ be touched outside of this contract. It is intentionally set to be
// private. Leaving it set to true can permanently disable admin calls.
bool private adminCallValidated;

/**************************************
* ERRORS *
**************************************/
error AdminCallValidatedAlreadySet();
error CallerNotBridge();
error OriginSenderNotCrossDomain();
error SourceChainNotHubChain();
error AdminCallNotValidated();

/**************************************
* EVENTS *
**************************************/
event SetPolygonZkEVMBridge(address indexed newPolygonZkEVMBridge, address indexed oldPolygonZkEVMBridge);
event PolygonZkEVMTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged);
event ReceivedMessageFromL1(address indexed caller, address indexed originAddress);

// Note: validating calls this way ensures that strange calls coming from the onMessageReceived won't be
// misinterpreted. Put differently, just checking that originAddress == crossDomainAdmint is not sufficient.
// All calls that have admin privileges must be fired from within the onMessageReceived method that's gone
// through validation where the sender is checked and the sender from the other chain is also validated.
// This modifier sets the adminCallValidated variable so this condition can be checked in _requireAdminSender().
modifier validateInternalCalls() {
// Make sure adminCallValidated is set to True only once at beginning of onMessageReceived, which prevents
// onMessageReceived from being re-entered.
if (adminCallValidated) {
revert AdminCallValidatedAlreadySet();
}

// This sets a variable indicating that we're now inside a validated call.
// Note: this is used by other methods to ensure that this call has been validated by this method and is not
// spoofed.
adminCallValidated = true;

_;

// Reset adminCallValidated to false to disallow admin calls after this method exits.
adminCallValidated = false;
}

/**
* @notice Construct Polygon zkEVM specific SpokePool.
* @param _wrappedNativeTokenAddress Address of WETH on Polygon zkEVM.
* @param _depositQuoteTimeBuffer Quote timestamps can't be set more than this amount
* into the past from the block time of the deposit.
* @param _fillDeadlineBuffer Fill deadlines can't be set more than this amount
* into the future from the block time of the deposit.
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address _wrappedNativeTokenAddress,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks

/**
* @notice Construct the Polygon zkEVM SpokePool.
* @param _l2PolygonZkEVMBridge Address of Polygon zkEVM's canonical bridge contract on L2.
* @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate
* @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin.
* @param _hubPool Hub pool address to set. Can be changed by admin.
*/
function initialize(
IPolygonZkEVMBridge _l2PolygonZkEVMBridge,
uint32 _initialDepositId,
address _crossDomainAdmin,
address _hubPool
) public initializer {
__SpokePool_init(_initialDepositId, _crossDomainAdmin, _hubPool);
_setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge);
}

/**
* @notice Admin can reset the Polygon zkEVM bridge contract address.
* @param _l2PolygonZkEVMBridge Address of the new canonical bridge.
*/
function setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _l2PolygonZkEVMBridge) external onlyAdmin {
_setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge);
}

/**
* @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool.
* @param _originAddress Address of the original message sender on L1.
* @param _originNetwork Polygon zkEVM's internal network id of source chain.
* @param _data Data to be received and executed on this contract.
*/
function onMessageReceived(
address _originAddress,
uint32 _originNetwork,
bytes memory _data
) external payable override validateInternalCalls {
if (msg.sender != address(l2PolygonZkEVMBridge)) {
revert CallerNotBridge();
}
if (_originAddress != crossDomainAdmin) {
revert OriginSenderNotCrossDomain();
}
if (_originNetwork != l1NetworkId) {
revert SourceChainNotHubChain();
}

/// @custom:oz-upgrades-unsafe-allow delegatecall
(bool success, ) = address(this).delegatecall(_data);
require(success, "delegatecall failed");

emit ReceivedMessageFromL1(msg.sender, _originAddress);
}

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

/**
* @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives
* ETH over the canonical token bridge instead of WETH.
*/
function _preExecuteLeafHook(address l2TokenAddress) internal override {
if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth();
}

// Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because
// this SpokePool will receive ETH from the canonical token bridge instead of WETH. This may not be neccessary
// if ETH on Polygon zkEVM is treated as ETH and the fallback() function is triggered when this contract receives
// ETH. We will have to test this but this function for now allows the contract to safely convert all of its
// held ETH into WETH at the cost of higher gas costs.
function _depositEthToWeth() internal {
//slither-disable-next-line arbitrary-send-eth
if (address(this).balance > 0) wrappedNativeToken.deposit{ value: address(this).balance }();
}

function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
// SpokePool is expected to receive ETH from the L1 HubPool, then we need to first unwrap it to ETH and then
// send ETH directly via the native L2 bridge.
if (l2TokenAddress == address(wrappedNativeToken)) {
WETH9Interface(l2TokenAddress).withdraw(amountToReturn); // Unwrap into ETH.
l2PolygonZkEVMBridge.bridgeAsset{ value: amountToReturn }(
l1NetworkId,
hubPool,
amountToReturn,
address(0),
true, // Indicates if the new global exit root is updated or not, which is true for asset bridges
""
);
} else {
IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2PolygonZkEVMBridge), amountToReturn);
l2PolygonZkEVMBridge.bridgeAsset(
l1NetworkId,
hubPool,
amountToReturn,
l2TokenAddress,
true, // Indicates if the new global exit root is updated or not, which is true for asset bridges
""
);
}
}

// Check that the onMessageReceived method has validated the method to ensure the sender is authenticated.
function _requireAdminSender() internal view override {
if (!adminCallValidated) {
revert AdminCallNotValidated();
}
}

function _setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _newL2PolygonZkEVMBridge) internal {
address oldL2PolygonZkEVMBridge = address(l2PolygonZkEVMBridge);
l2PolygonZkEVMBridge = _newL2PolygonZkEVMBridge;
emit SetPolygonZkEVMBridge(address(_newL2PolygonZkEVMBridge), oldL2PolygonZkEVMBridge);
}
}
68 changes: 68 additions & 0 deletions contracts/chain-adapters/PolygonZkEVM_Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./interfaces/AdapterInterface.sol";
import "../external/interfaces/WETH9Interface.sol";
import "../external/interfaces/IPolygonZkEVMBridge.sol";

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

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

WETH9Interface public immutable l1Weth;
// Address of Polygon zkEVM's Canonical Bridge on L1.
IPolygonZkEVMBridge public immutable l1PolygonZkEVMBridge;

// Polygon's internal network id for zkEVM.
uint32 public constant l2NetworkId = 1;

/**
* @notice Constructs new Adapter.
* @param _l1Weth WETH address on L1.
* @param _l1PolygonZkEVMBridge Canonical token bridge contract on L1.
*/
constructor(WETH9Interface _l1Weth, IPolygonZkEVMBridge _l1PolygonZkEVMBridge) {
l1Weth = _l1Weth;
l1PolygonZkEVMBridge = _l1PolygonZkEVMBridge;
}

/**
* @notice Send cross-chain message to target on Polygon zkEVM.
* @param target Contract on Polygon zkEVM that will receive message.
* @param message Data to send to target.
*/
function relayMessage(address target, bytes calldata message) external payable override {
l1PolygonZkEVMBridge.bridgeMessage(l2NetworkId, target, true, message);
emit MessageRelayed(target, message);
}

/**
* @notice Bridge tokens to Polygon zkEVM.
* @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,
uint256 amount,
address to
) external payable override {
// The mapped WETH address in the native Polygon zkEVM bridge contract does not match
// the official WETH address. Therefore, if the l1Token is WETH then unwrap it to ETH
// and send the ETH directly via as msg.value.
if (l1Token == address(l1Weth)) {
l1Weth.withdraw(amount);
l1PolygonZkEVMBridge.bridgeAsset{ value: amount }(l2NetworkId, to, amount, address(0), true, "");
} else {
IERC20(l1Token).safeIncreaseAllowance(address(l1PolygonZkEVMBridge), amount);
l1PolygonZkEVMBridge.bridgeAsset(l2NetworkId, to, amount, l1Token, true, "");
}

emit TokensRelayed(l1Token, l2Token, amount, to);
}
}
40 changes: 40 additions & 0 deletions contracts/external/interfaces/IPolygonZkEVMBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/**
* @notice Interface of Polygon zkEVM's Canonical Bridge
* See https://github.com/0xPolygonHermez/zkevm-contracts/blob/53e95f3a236d8bea87c27cb8714a5d21496a3b20/contracts/interfaces/IPolygonZkEVMBridge.sol
*/
interface IPolygonZkEVMBridge {
/**
* @notice Deposit add a new leaf to the merkle tree
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param amount Amount of tokens
* @param token Token address, 0 address is reserved for ether
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
* @param permitData Raw data of the call `permit` of the token
*/
function bridgeAsset(
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
address token,
bool forceUpdateGlobalExitRoot,
bytes calldata permitData
) external payable;

/**
* @notice Bridge message and send ETH value
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
* @param metadata Message metadata
*/
function bridgeMessage(
uint32 destinationNetwork,
address destinationAddress,
bool forceUpdateGlobalExitRoot,
bytes calldata metadata
) external payable;
}
23 changes: 23 additions & 0 deletions deploy/030_deploy_polygon_zk_evm_adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { L1_ADDRESS_MAP } from "./consts";
import { DeployFunction } from "hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";

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

const { deployer } = await getNamedAccounts();

const chainId = parseInt(await getChainId());

await deploy("PolygonZkEVM_Adapter", {
from: deployer,
log: true,
skipIfAlreadyDeployed: true,
args: [L1_ADDRESS_MAP[chainId].weth, L1_ADDRESS_MAP[chainId].polygonZkEvmBridge],
});
};

module.exports = func;
func.dependencies = ["HubPool"];
func.tags = ["PolygonZkEvmAdapter", "mainnet"];
25 changes: 25 additions & 0 deletions deploy/031_deploy_polygon_zk_evm_spokepool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { L2_ADDRESS_MAP } from "./consts";
import { deployNewProxy, getSpokePoolDeploymentInfo } from "../utils/utils.hre";
import { DeployFunction } from "hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { getChainId } = hre;
const { hubPool } = await getSpokePoolDeploymentInfo(hre);
const chainId = parseInt(await getChainId());

const initArgs = [
L2_ADDRESS_MAP[chainId].polygonZkEvmBridge,
// Initialize deposit counter to very high number of deposits to avoid duplicate deposit ID's
// with deprecated spoke pool.
1_000_000,
// Set hub pool as cross domain admin since it delegatecalls the Adapter logic.
hubPool.address,
hubPool.address,
];
const constructorArgs = [L2_ADDRESS_MAP[chainId].l2Weth, 3600, 32400];

await deployNewProxy("PolygonZkEVM_SpokePool", constructorArgs, initArgs);
};
module.exports = func;
func.tags = ["PolygonZkEvmSpokePool", "polygonZkEvm"];
6 changes: 6 additions & 0 deletions deploy/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
lineaMessageService: "0x70BaD09280FD342D02fe64119779BC1f0791BAC2",
lineaTokenBridge: "0x5506A3805fB8A58Fa58248CC52d2b06D92cA94e6",
lineaUsdcBridge: "0x32D123756d32d3eD6580935f8edF416e57b940f4",
polygonZkEvmBridge: "0xF6BEEeBB578e214CA9E23B0e9683454Ff88Ed2A7",
},
42: {
l1ArbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", // dummy: Arbitrum's testnet is rinkeby
Expand Down Expand Up @@ -170,6 +171,11 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
scrollGasPriceOracle: "0x5300000000000000000000000000000000000002",
scrollMessenger: "0xba50f5340fb9f3bd074bd638c9be13ecb36e603d",
},
1442: {
// Custom WETH for testing because there is no "official" WETH
l2Weth: "0x3ab6C7AEb93A1CFC64AEEa8BF0f00c176EE42A2C",
polygonZkEvmBridge: "0xF6BEEeBB578e214CA9E23B0e9683454Ff88Ed2A7",
},
};

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