Skip to content

Commit bf08c4d

Browse files
pxrldohakimrice32
authored
feat(polygon-zk-evm): add adapter and spokepool (#6) (#415)
Co-authored-by: Dong-Ha Kim <dongha.kim210@gmail.com> Co-authored-by: Matt Rice <matthewcrice32@gmail.com>
1 parent c854eca commit bf08c4d

File tree

15 files changed

+3952
-0
lines changed

15 files changed

+3952
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import "./SpokePool.sol";
5+
import "./external/interfaces/IPolygonZkEVMBridge.sol";
6+
7+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9+
10+
/**
11+
* @notice Define interface for PolygonZkEVM Bridge message receiver
12+
* See https://github.com/0xPolygonHermez/zkevm-contracts/blob/53e95f3a236d8bea87c27cb8714a5d21496a3b20/contracts/interfaces/IBridgeMessageReceiver.sol
13+
*/
14+
interface IBridgeMessageReceiver {
15+
/**
16+
* @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool.
17+
* @param originAddress Address of the original message sender on L1.
18+
* @param originNetwork Polygon zkEVM's internal network id of source chain.
19+
* @param data Data to be received and executed on this contract.
20+
*/
21+
function onMessageReceived(
22+
address originAddress,
23+
uint32 originNetwork,
24+
bytes memory data
25+
) external payable;
26+
}
27+
28+
/**
29+
* @notice Polygon zkEVM Spoke pool.
30+
*/
31+
contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver {
32+
using SafeERC20 for IERC20;
33+
34+
// Address of Polygon zkEVM's Canonical Bridge on L2.
35+
IPolygonZkEVMBridge public l2PolygonZkEVMBridge;
36+
37+
// Polygon zkEVM's internal network id for L1.
38+
uint32 public constant l1NetworkId = 0;
39+
40+
// Warning: this variable should _never_ be touched outside of this contract. It is intentionally set to be
41+
// private. Leaving it set to true can permanently disable admin calls.
42+
bool private adminCallValidated;
43+
44+
/**************************************
45+
* ERRORS *
46+
**************************************/
47+
error AdminCallValidatedAlreadySet();
48+
error CallerNotBridge();
49+
error OriginSenderNotCrossDomain();
50+
error SourceChainNotHubChain();
51+
error AdminCallNotValidated();
52+
53+
/**************************************
54+
* EVENTS *
55+
**************************************/
56+
event SetPolygonZkEVMBridge(address indexed newPolygonZkEVMBridge, address indexed oldPolygonZkEVMBridge);
57+
event PolygonZkEVMTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged);
58+
event ReceivedMessageFromL1(address indexed caller, address indexed originAddress);
59+
60+
// Note: validating calls this way ensures that strange calls coming from the onMessageReceived won't be
61+
// misinterpreted. Put differently, just checking that originAddress == crossDomainAdmint is not sufficient.
62+
// All calls that have admin privileges must be fired from within the onMessageReceived method that's gone
63+
// through validation where the sender is checked and the sender from the other chain is also validated.
64+
// This modifier sets the adminCallValidated variable so this condition can be checked in _requireAdminSender().
65+
modifier validateInternalCalls() {
66+
// Make sure adminCallValidated is set to True only once at beginning of onMessageReceived, which prevents
67+
// onMessageReceived from being re-entered.
68+
if (adminCallValidated) {
69+
revert AdminCallValidatedAlreadySet();
70+
}
71+
72+
// This sets a variable indicating that we're now inside a validated call.
73+
// Note: this is used by other methods to ensure that this call has been validated by this method and is not
74+
// spoofed.
75+
adminCallValidated = true;
76+
77+
_;
78+
79+
// Reset adminCallValidated to false to disallow admin calls after this method exits.
80+
adminCallValidated = false;
81+
}
82+
83+
/**
84+
* @notice Construct Polygon zkEVM specific SpokePool.
85+
* @param _wrappedNativeTokenAddress Address of WETH on Polygon zkEVM.
86+
* @param _depositQuoteTimeBuffer Quote timestamps can't be set more than this amount
87+
* into the past from the block time of the deposit.
88+
* @param _fillDeadlineBuffer Fill deadlines can't be set more than this amount
89+
* into the future from the block time of the deposit.
90+
*/
91+
/// @custom:oz-upgrades-unsafe-allow constructor
92+
constructor(
93+
address _wrappedNativeTokenAddress,
94+
uint32 _depositQuoteTimeBuffer,
95+
uint32 _fillDeadlineBuffer
96+
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
97+
98+
/**
99+
* @notice Construct the Polygon zkEVM SpokePool.
100+
* @param _l2PolygonZkEVMBridge Address of Polygon zkEVM's canonical bridge contract on L2.
101+
* @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate
102+
* @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin.
103+
* @param _hubPool Hub pool address to set. Can be changed by admin.
104+
*/
105+
function initialize(
106+
IPolygonZkEVMBridge _l2PolygonZkEVMBridge,
107+
uint32 _initialDepositId,
108+
address _crossDomainAdmin,
109+
address _hubPool
110+
) public initializer {
111+
__SpokePool_init(_initialDepositId, _crossDomainAdmin, _hubPool);
112+
_setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge);
113+
}
114+
115+
/**
116+
* @notice Admin can reset the Polygon zkEVM bridge contract address.
117+
* @param _l2PolygonZkEVMBridge Address of the new canonical bridge.
118+
*/
119+
function setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _l2PolygonZkEVMBridge) external onlyAdmin {
120+
_setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge);
121+
}
122+
123+
/**
124+
* @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool.
125+
* @param _originAddress Address of the original message sender on L1.
126+
* @param _originNetwork Polygon zkEVM's internal network id of source chain.
127+
* @param _data Data to be received and executed on this contract.
128+
*/
129+
function onMessageReceived(
130+
address _originAddress,
131+
uint32 _originNetwork,
132+
bytes memory _data
133+
) external payable override validateInternalCalls {
134+
if (msg.sender != address(l2PolygonZkEVMBridge)) {
135+
revert CallerNotBridge();
136+
}
137+
if (_originAddress != crossDomainAdmin) {
138+
revert OriginSenderNotCrossDomain();
139+
}
140+
if (_originNetwork != l1NetworkId) {
141+
revert SourceChainNotHubChain();
142+
}
143+
144+
/// @custom:oz-upgrades-unsafe-allow delegatecall
145+
(bool success, ) = address(this).delegatecall(_data);
146+
require(success, "delegatecall failed");
147+
148+
emit ReceivedMessageFromL1(msg.sender, _originAddress);
149+
}
150+
151+
/**************************************
152+
* INTERNAL FUNCTIONS *
153+
**************************************/
154+
155+
/**
156+
* @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives
157+
* ETH over the canonical token bridge instead of WETH.
158+
*/
159+
function _preExecuteLeafHook(address l2TokenAddress) internal override {
160+
if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth();
161+
}
162+
163+
// Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because
164+
// this SpokePool will receive ETH from the canonical token bridge instead of WETH. This may not be neccessary
165+
// if ETH on Polygon zkEVM is treated as ETH and the fallback() function is triggered when this contract receives
166+
// ETH. We will have to test this but this function for now allows the contract to safely convert all of its
167+
// held ETH into WETH at the cost of higher gas costs.
168+
function _depositEthToWeth() internal {
169+
//slither-disable-next-line arbitrary-send-eth
170+
if (address(this).balance > 0) wrappedNativeToken.deposit{ value: address(this).balance }();
171+
}
172+
173+
function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
174+
// SpokePool is expected to receive ETH from the L1 HubPool, then we need to first unwrap it to ETH and then
175+
// send ETH directly via the native L2 bridge.
176+
if (l2TokenAddress == address(wrappedNativeToken)) {
177+
WETH9Interface(l2TokenAddress).withdraw(amountToReturn); // Unwrap into ETH.
178+
l2PolygonZkEVMBridge.bridgeAsset{ value: amountToReturn }(
179+
l1NetworkId,
180+
hubPool,
181+
amountToReturn,
182+
address(0),
183+
true, // Indicates if the new global exit root is updated or not, which is true for asset bridges
184+
""
185+
);
186+
} else {
187+
IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2PolygonZkEVMBridge), amountToReturn);
188+
l2PolygonZkEVMBridge.bridgeAsset(
189+
l1NetworkId,
190+
hubPool,
191+
amountToReturn,
192+
l2TokenAddress,
193+
true, // Indicates if the new global exit root is updated or not, which is true for asset bridges
194+
""
195+
);
196+
}
197+
}
198+
199+
// Check that the onMessageReceived method has validated the method to ensure the sender is authenticated.
200+
function _requireAdminSender() internal view override {
201+
if (!adminCallValidated) {
202+
revert AdminCallNotValidated();
203+
}
204+
}
205+
206+
function _setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _newL2PolygonZkEVMBridge) internal {
207+
address oldL2PolygonZkEVMBridge = address(l2PolygonZkEVMBridge);
208+
l2PolygonZkEVMBridge = _newL2PolygonZkEVMBridge;
209+
emit SetPolygonZkEVMBridge(address(_newL2PolygonZkEVMBridge), oldL2PolygonZkEVMBridge);
210+
}
211+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
import "../external/interfaces/IPolygonZkEVMBridge.sol";
7+
8+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
10+
11+
// solhint-disable-next-line contract-name-camelcase
12+
contract PolygonZkEVM_Adapter is AdapterInterface {
13+
using SafeERC20 for IERC20;
14+
15+
WETH9Interface public immutable l1Weth;
16+
// Address of Polygon zkEVM's Canonical Bridge on L1.
17+
IPolygonZkEVMBridge public immutable l1PolygonZkEVMBridge;
18+
19+
// Polygon's internal network id for zkEVM.
20+
uint32 public constant l2NetworkId = 1;
21+
22+
/**
23+
* @notice Constructs new Adapter.
24+
* @param _l1Weth WETH address on L1.
25+
* @param _l1PolygonZkEVMBridge Canonical token bridge contract on L1.
26+
*/
27+
constructor(WETH9Interface _l1Weth, IPolygonZkEVMBridge _l1PolygonZkEVMBridge) {
28+
l1Weth = _l1Weth;
29+
l1PolygonZkEVMBridge = _l1PolygonZkEVMBridge;
30+
}
31+
32+
/**
33+
* @notice Send cross-chain message to target on Polygon zkEVM.
34+
* @param target Contract on Polygon zkEVM that will receive message.
35+
* @param message Data to send to target.
36+
*/
37+
function relayMessage(address target, bytes calldata message) external payable override {
38+
l1PolygonZkEVMBridge.bridgeMessage(l2NetworkId, target, true, message);
39+
emit MessageRelayed(target, message);
40+
}
41+
42+
/**
43+
* @notice Bridge tokens to Polygon zkEVM.
44+
* @param l1Token L1 token to deposit.
45+
* @param l2Token L2 token to receive.
46+
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
47+
* @param to Bridge recipient.
48+
*/
49+
function relayTokens(
50+
address l1Token,
51+
address l2Token,
52+
uint256 amount,
53+
address to
54+
) external payable override {
55+
// The mapped WETH address in the native Polygon zkEVM bridge contract does not match
56+
// the official WETH address. Therefore, if the l1Token is WETH then unwrap it to ETH
57+
// and send the ETH directly via as msg.value.
58+
if (l1Token == address(l1Weth)) {
59+
l1Weth.withdraw(amount);
60+
l1PolygonZkEVMBridge.bridgeAsset{ value: amount }(l2NetworkId, to, amount, address(0), true, "");
61+
} else {
62+
IERC20(l1Token).safeIncreaseAllowance(address(l1PolygonZkEVMBridge), amount);
63+
l1PolygonZkEVMBridge.bridgeAsset(l2NetworkId, to, amount, l1Token, true, "");
64+
}
65+
66+
emit TokensRelayed(l1Token, l2Token, amount, to);
67+
}
68+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @notice Interface of Polygon zkEVM's Canonical Bridge
6+
* See https://github.com/0xPolygonHermez/zkevm-contracts/blob/53e95f3a236d8bea87c27cb8714a5d21496a3b20/contracts/interfaces/IPolygonZkEVMBridge.sol
7+
*/
8+
interface IPolygonZkEVMBridge {
9+
/**
10+
* @notice Deposit add a new leaf to the merkle tree
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+
) external payable;
26+
27+
/**
28+
* @notice Bridge message and send ETH value
29+
* @param destinationNetwork Network destination
30+
* @param destinationAddress Address destination
31+
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
32+
* @param metadata Message metadata
33+
*/
34+
function bridgeMessage(
35+
uint32 destinationNetwork,
36+
address destinationAddress,
37+
bool forceUpdateGlobalExitRoot,
38+
bytes calldata metadata
39+
) external payable;
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { L1_ADDRESS_MAP } from "./consts";
2+
import { DeployFunction } from "hardhat-deploy/types";
3+
import { HardhatRuntimeEnvironment } from "hardhat/types";
4+
5+
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
6+
const { deployments, getNamedAccounts, getChainId } = hre;
7+
const { deploy } = deployments;
8+
9+
const { deployer } = await getNamedAccounts();
10+
11+
const chainId = parseInt(await getChainId());
12+
13+
await deploy("PolygonZkEVM_Adapter", {
14+
from: deployer,
15+
log: true,
16+
skipIfAlreadyDeployed: true,
17+
args: [L1_ADDRESS_MAP[chainId].weth, L1_ADDRESS_MAP[chainId].polygonZkEvmBridge],
18+
});
19+
};
20+
21+
module.exports = func;
22+
func.dependencies = ["HubPool"];
23+
func.tags = ["PolygonZkEvmAdapter", "mainnet"];
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { L2_ADDRESS_MAP } from "./consts";
2+
import { deployNewProxy, getSpokePoolDeploymentInfo } from "../utils/utils.hre";
3+
import { DeployFunction } from "hardhat-deploy/types";
4+
import { HardhatRuntimeEnvironment } from "hardhat/types";
5+
6+
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
7+
const { getChainId } = hre;
8+
const { hubPool } = await getSpokePoolDeploymentInfo(hre);
9+
const chainId = parseInt(await getChainId());
10+
11+
const initArgs = [
12+
L2_ADDRESS_MAP[chainId].polygonZkEvmBridge,
13+
// Initialize deposit counter to very high number of deposits to avoid duplicate deposit ID's
14+
// with deprecated spoke pool.
15+
1_000_000,
16+
// Set hub pool as cross domain admin since it delegatecalls the Adapter logic.
17+
hubPool.address,
18+
hubPool.address,
19+
];
20+
const constructorArgs = [L2_ADDRESS_MAP[chainId].l2Weth, 3600, 32400];
21+
22+
await deployNewProxy("PolygonZkEVM_SpokePool", constructorArgs, initArgs);
23+
};
24+
module.exports = func;
25+
func.tags = ["PolygonZkEvmSpokePool", "polygonZkEvm"];

deploy/consts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
6060
lineaMessageService: "0x70BaD09280FD342D02fe64119779BC1f0791BAC2",
6161
lineaTokenBridge: "0x5506A3805fB8A58Fa58248CC52d2b06D92cA94e6",
6262
lineaUsdcBridge: "0x32D123756d32d3eD6580935f8edF416e57b940f4",
63+
polygonZkEvmBridge: "0xF6BEEeBB578e214CA9E23B0e9683454Ff88Ed2A7",
6364
},
6465
42: {
6566
l1ArbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", // dummy: Arbitrum's testnet is rinkeby
@@ -170,6 +171,11 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
170171
scrollGasPriceOracle: "0x5300000000000000000000000000000000000002",
171172
scrollMessenger: "0xba50f5340fb9f3bd074bd638c9be13ecb36e603d",
172173
},
174+
1442: {
175+
// Custom WETH for testing because there is no "official" WETH
176+
l2Weth: "0x3ab6C7AEb93A1CFC64AEEa8BF0f00c176EE42A2C",
177+
polygonZkEvmBridge: "0xF6BEEeBB578e214CA9E23B0e9683454Ff88Ed2A7",
178+
},
173179
};
174180

175181
export const POLYGON_CHAIN_IDS: { [l1ChainId: number]: number } = {

0 commit comments

Comments
 (0)