-
Notifications
You must be signed in to change notification settings - Fork 75
feat(scroll): adapter and spokepool #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 34 commits
9c89716
f4b9585
64256be
5e3fe33
3c24d66
08d8642
ab0aa5a
3c205bb
441449a
7be19cc
568f871
543f812
bf20bf2
61fd9dc
e35ae0d
1ed11b6
676b7b5
19ab1c3
272245a
1974f8e
5158600
fecb59f
50e34a2
6f53b8d
c687f6d
a6b537f
ba1cdeb
d5c2afa
9a80eb7
f3fba07
c1bb994
a44f756
71eac94
ebe93d6
bf674fe
300735d
3c52c33
e5f627b
2a4d082
e9b4443
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "./SpokePool.sol"; | ||
| import "@scroll-tech/contracts/L2/gateways/IL2GatewayRouter.sol"; | ||
| import "@scroll-tech/contracts/libraries/IScrollMessenger.sol"; | ||
|
|
||
| /** | ||
| * @title Scroll_SpokePool | ||
| * @notice Modified SpokePool contract deployed on Scroll to facilitate token transfers | ||
| * from Scroll to the HubPool | ||
| */ | ||
| contract Scroll_SpokePool is SpokePool { | ||
| /** | ||
| * @notice The address of the official l2GatewayRouter contract for Scroll for bridging tokens from L2 -> L1 | ||
| * @dev We can find these (main/test)net deployments here: https://docs.scroll.io/en/developers/scroll-contracts/#scroll-contracts | ||
| */ | ||
| IL2GatewayRouter public l2GatewayRouter; | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * @notice The address of the official messenger contract for Scroll from L2 -> L1 | ||
| * @dev We can find these (main/test)net deployments here: https://docs.scroll.io/en/developers/scroll-contracts/#scroll-contracts | ||
| */ | ||
| IScrollMessenger public l2ScrollMessenger; | ||
|
|
||
| /************************************** | ||
| * EVENTS * | ||
| **************************************/ | ||
|
|
||
| event ScrollTokensBridged(address indexed token, address indexed receiver, uint256 amount); | ||
| event SetL2GatewayRouter(address indexed newGatewayRouter, address oldGatewayRouter); | ||
| event SetL2ScrollMessenger(address indexed newScrollMessenger, address oldScrollMessenger); | ||
|
|
||
| /************************************** | ||
| * PUBLIC FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| /// @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 Scroll SpokePool. | ||
| * @param _l2GatewayRouter Standard bridge contract. | ||
| * @param _l2ScrollMessenger Scroll Messenger 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( | ||
| IL2GatewayRouter _l2GatewayRouter, | ||
| IScrollMessenger _l2ScrollMessenger, | ||
| uint32 _initialDepositId, | ||
| address _crossDomainAdmin, | ||
| address _hubPool | ||
| ) public initializer { | ||
| __SpokePool_init(_initialDepositId, _crossDomainAdmin, _hubPool); | ||
| l2GatewayRouter = _l2GatewayRouter; | ||
| l2ScrollMessenger = _l2ScrollMessenger; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Change the L2 Gateway Router. Changed only by admin. | ||
| * @param _l2GatewayRouter New address of L2 gateway router. | ||
| */ | ||
| function setL2GatewayRouter(IL2GatewayRouter _l2GatewayRouter) public onlyAdmin nonReentrant { | ||
| _setL2GatewayRouter(_l2GatewayRouter); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Change L2 message service address. Callable only by admin. | ||
| * @param _l2ScrollMessenger New address of L2 messenger. | ||
| */ | ||
| function setL2ScrollMessenger(IScrollMessenger _l2ScrollMessenger) public onlyAdmin nonReentrant { | ||
| _setL2MessageService(_l2ScrollMessenger); | ||
| } | ||
|
|
||
| /************************************** | ||
| * INTERNAL FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| /** | ||
| * @notice Bridge tokens to the HubPool. | ||
| * @param amountToReturn Amount of tokens to bridge to the HubPool. | ||
| * @param l2TokenAddress Address of the token to bridge. | ||
| */ | ||
| function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal virtual override { | ||
| IL2GatewayRouter _l2GatewayRouter = l2GatewayRouter; | ||
nicholaspai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // The scroll bridge handles arbitrary ERC20 tokens and is mindful of | ||
| // the official WETH address on-chain. We don't need to do anything specific | ||
| // to differentiate between WETH and a separate ERC20. | ||
| // Note: This happens due to the L2GatewayRouter.getERC20Gateway() call | ||
| _l2GatewayRouter.withdrawERC20(l2TokenAddress, hubPool, amountToReturn, 0); | ||
| emit ScrollTokensBridged(l2TokenAddress, hubPool, amountToReturn); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Verifies that calling method is from the cross domain admin. | ||
| */ | ||
| function _requireAdminSender() internal view override { | ||
| // The xdomainMessageSender is set within the Scroll messenger right | ||
| // before the call to this function (and reset afterwards). This represents | ||
| // the address that sent the message from L1 to L2. If the calling contract | ||
| // isn't the Scroll messenger, then the xdomainMessageSender will be the zero | ||
| // address and *NOT* cross domain admin. | ||
| address _xDomainSender = l2ScrollMessenger.xDomainMessageSender(); | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can xDomainMessageSender be set to whatever a malicious contract wants it to or is it always the contract that is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I'm going to go through each call in the stack so this might be longform, but tl;dr I believe we should be fine as long as we trust the chain) Per master on _executeMessagethe last line of this snippet always set to be the DEFAULT_XDOMAIN_MESSAGE_SENDER outside of this specific case. This is a call to ensure that doesn't happen In the above ^ it's only set to be read within the context of relayMessageFrom the above, what we're really looking for is that L2ScrollMessengerWe have an implicit trust that the |
||
| require(_xDomainSender == crossDomainAdmin, "Sender must be admin"); | ||
| } | ||
|
|
||
| function _setL2GatewayRouter(IL2GatewayRouter _l2GatewayRouter) internal { | ||
| address oldL2GatewayRouter = address(l2GatewayRouter); | ||
| l2GatewayRouter = _l2GatewayRouter; | ||
| emit SetL2GatewayRouter(address(_l2GatewayRouter), oldL2GatewayRouter); | ||
| } | ||
|
|
||
| function _setL2MessageService(IScrollMessenger _l2ScrollMessenger) internal { | ||
| address oldL2ScrollMessenger = address(l2ScrollMessenger); | ||
| l2ScrollMessenger = _l2ScrollMessenger; | ||
| emit SetL2ScrollMessenger(address(_l2ScrollMessenger), oldL2ScrollMessenger); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import "@scroll-tech/contracts/L1/gateways/IL1GatewayRouter.sol"; | ||
| import "@scroll-tech/contracts/L1/rollup/IL2GasPriceOracle.sol"; | ||
| import "@scroll-tech/contracts/L1/IL1ScrollMessenger.sol"; | ||
| import "./interfaces/AdapterInterface.sol"; | ||
|
|
||
| /** | ||
| * @title Scroll_Adapter | ||
| * @notice Adapter contract deployed on L1 alongside the HubPool to facilitate token transfers | ||
| * and arbitrary message relaying from L1 to L2. | ||
| */ | ||
| contract Scroll_Adapter is AdapterInterface { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| /** | ||
| * @notice Used as the gas limit for relaying messages to L2. | ||
| */ | ||
| uint32 public constant l2GasLimit = 250_000; | ||
nicholaspai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * @notice The address of the official l1GatewayRouter contract for Scroll for bridging tokens from L1 -> L2 | ||
| * @dev We can find these (main/test)net deployments here: https://docs.scroll.io/en/developers/scroll-contracts/#scroll-contracts | ||
| */ | ||
| IL1GatewayRouter public immutable l1GatewayRouter; | ||
|
|
||
| /** | ||
| * @notice The address of the official messenger contract for Scroll from L1 -> L2 | ||
| * @dev We can find these (main/test)net deployments here: https://docs.scroll.io/en/developers/scroll-contracts/#scroll-contracts | ||
| */ | ||
| IL1ScrollMessenger public immutable l1ScrollMessenger; | ||
|
|
||
| /** | ||
| * @notice The address of the official gas price oracle contract for Scroll for estimating the relayer fee | ||
| * @dev We can find these (main/test)net deployments here: https://docs.scroll.io/en/developers/scroll-contracts/#scroll-contracts | ||
| */ | ||
| IL2GasPriceOracle public immutable l2GasPriceOracle; | ||
|
|
||
| /************************************** | ||
| * PUBLIC FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| /** | ||
| * @notice Constructs new Adapter. | ||
| * @param _l1GatewayRouter Standard bridge contract. | ||
| * @param _l1ScrollMessenger Scroll Messenger contract. | ||
| * @param _l2GasPriceOracle Gas price oracle contract. | ||
| */ | ||
| constructor( | ||
| IL1GatewayRouter _l1GatewayRouter, | ||
| IL1ScrollMessenger _l1ScrollMessenger, | ||
| IL2GasPriceOracle _l2GasPriceOracle | ||
| ) { | ||
| l1GatewayRouter = _l1GatewayRouter; | ||
| l1ScrollMessenger = _l1ScrollMessenger; | ||
| l2GasPriceOracle = _l2GasPriceOracle; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Send message to `target` on Scroll. | ||
| * @dev This message is marked payable because relaying the message will require | ||
| * a fee that needs to be propagated to the Scroll Bridge. It will not send msg.value | ||
| * to the target contract on L2. | ||
| * @param target L2 address to send message to. | ||
| * @param message Message to send to `target`. | ||
| */ | ||
| function relayMessage(address target, bytes calldata message) external payable { | ||
| // We can specifically send a message with 0 value to the Scroll Bridge | ||
| // and it will not forward any ETH to the target contract on L2. However, | ||
| // we need to set the payable value to msg.value to ensure that the Scroll | ||
| // Bridge has enough gas to forward the message to L2. | ||
james-a-morris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| l1ScrollMessenger.sendMessage{ value: _generateRelayerFee() }(target, 0, message, l2GasLimit); | ||
|
||
| emit MessageRelayed(target, message); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Send `amount` of `l1Token` to `to` on Scroll. `l2Token` is the Scroll address equivalent of `l1Token`. | ||
| * @dev This method is marked payable because relaying the message might require a fee | ||
| * to be paid by the sender to forward the message to L2. However, it will not send msg.value | ||
| * to the target contract on L2. | ||
| * @param l1Token L1 token to bridge. | ||
| * @param l2Token L2 token to receive. | ||
| * @param amount Amount of `l1Token` to bridge. | ||
| * @param to Bridge recipient. | ||
| */ | ||
| function relayTokens( | ||
| address l1Token, | ||
| address l2Token, | ||
| uint256 amount, | ||
| address to | ||
| ) external payable { | ||
| IL1GatewayRouter _l1GatewayRouter = l1GatewayRouter; | ||
|
|
||
| // Confirm that the l2Token that we're trying to send is the correct counterpart | ||
| // address | ||
| address _l2Token = _l1GatewayRouter.getL2ERC20Address(l1Token); | ||
| require(_l2Token == l2Token, "l2Token Mismatch"); | ||
|
|
||
| // Bump the allowance | ||
james-a-morris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| IERC20(l1Token).safeIncreaseAllowance(address(_l1GatewayRouter), amount); | ||
|
|
||
| // The scroll bridge handles arbitrary ERC20 tokens and is mindful of | ||
| // the official WETH address on-chain. We don't need to do anything specific | ||
| // to differentiate between WETH and a separate ERC20. | ||
| // Note: This happens due to the L1GatewayRouter.getERC20Gateway() call | ||
| // Note: dev docs: https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge/ | ||
| _l1GatewayRouter.depositERC20{ value: _generateRelayerFee() }(l1Token, to, amount, l2GasLimit); | ||
| emit TokensRelayed(l1Token, l2Token, amount, to); | ||
| } | ||
|
|
||
| /************************************** | ||
| * INTERNAL FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| /** | ||
| * @notice Generates the relayer fee for a message to be sent to L2. | ||
| * @dev Function will revert if the contract does not have enough ETH to pay the fee. | ||
| */ | ||
| function _generateRelayerFee() internal view returns (uint256 l2Fee) { | ||
| l2Fee = l2GasPriceOracle.estimateCrossDomainMessageFee(l2GasLimit); | ||
| require(address(this).balance >= l2Fee, "Insufficient ETH balance"); | ||
| } | ||
| } | ||
james-a-morris marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| 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("Scroll_Adapter", { | ||
| from: deployer, | ||
| log: true, | ||
| skipIfAlreadyDeployed: false, | ||
| args: [ | ||
| L1_ADDRESS_MAP[chainId].scrollERC20GatewayRouter, | ||
| L1_ADDRESS_MAP[chainId].scrollMessengerRelay, | ||
| L1_ADDRESS_MAP[chainId].scrollGasPriceOracle, | ||
| ], | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = func; | ||
| func.tags = ["ScrollAdapter", "mainnet"]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 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()); | ||
|
|
||
| // Initialize deposit counter to very high number of deposits to avoid duplicate deposit ID's | ||
| // with deprecated spoke pool. | ||
| // Set hub pool as cross domain admin since it delegatecalls the Adapter logic. | ||
| const initArgs = [ | ||
| L2_ADDRESS_MAP[chainId].scrollERC20GatewayRouter, | ||
| L2_ADDRESS_MAP[chainId].scrollMessenger, | ||
| 1_000_000, | ||
| hubPool.address, | ||
| hubPool.address, | ||
| ]; | ||
| // Construct this spokepool with a: | ||
| // * A WETH address of the L2 WETH address | ||
| // * A depositQuoteTimeBuffer of 1 hour | ||
| // * A fillDeadlineBuffer of 9 hours | ||
| const constructorArgs = [L2_ADDRESS_MAP[chainId].l2Weth, 3600, 32400]; | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| await deployNewProxy("Scroll_SpokePool", constructorArgs, initArgs); | ||
| }; | ||
| module.exports = func; | ||
| func.tags = ["ScrollSpokePool", "scroll"]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 534351 |
Uh oh!
There was an error while loading. Please reload this page.