Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9c89716
feat: include new scroll contracts
james-a-morris Dec 11, 2023
f4b9585
feat: begin working on scroll
james-a-morris Dec 11, 2023
64256be
improve: use scroll adapter contracts
james-a-morris Dec 13, 2023
5e3fe33
improve: sanity checks
james-a-morris Dec 14, 2023
3c24d66
chore: add deployments
james-a-morris Dec 15, 2023
08d8642
improve: send message payable
james-a-morris Dec 15, 2023
ab0aa5a
chore: lint
james-a-morris Dec 15, 2023
3c205bb
improve: initial work on spokepool
james-a-morris Dec 18, 2023
441449a
improve: fn name
james-a-morris Dec 19, 2023
7be19cc
feat: add spokepool deployment
james-a-morris Dec 20, 2023
568f871
improve: remove need for l1 gas
james-a-morris Dec 21, 2023
543f812
improve: remove unneeded solc
james-a-morris Dec 21, 2023
bf20bf2
improve: use specific gas fee
james-a-morris Dec 21, 2023
61fd9dc
improve: custom finalization
james-a-morris Dec 22, 2023
e35ae0d
fix: yarn lock
james-a-morris Dec 26, 2023
1ed11b6
improve: documentation
james-a-morris Dec 27, 2023
676b7b5
improve: new json files
james-a-morris Dec 27, 2023
19ab1c3
nit: public function comments
james-a-morris Dec 27, 2023
272245a
improve: include tech doc links
james-a-morris Dec 27, 2023
1974f8e
improve: remove unneeded solhint
james-a-morris Dec 27, 2023
5158600
improve: addtl comment
james-a-morris Dec 27, 2023
fecb59f
chore: remove unneeded solcInputs
james-a-morris Dec 27, 2023
50e34a2
docs: update README.md
james-a-morris Dec 27, 2023
6f53b8d
test: add adapter tests
james-a-morris Dec 27, 2023
c687f6d
improve: add modifier
james-a-morris Dec 28, 2023
a6b537f
improve: comments
james-a-morris Dec 28, 2023
ba1cdeb
docs: update test name
james-a-morris Dec 28, 2023
d5c2afa
chore: remove testing file
james-a-morris Dec 28, 2023
9a80eb7
improve: revert test file
james-a-morris Dec 28, 2023
f3fba07
docs: add constructor comments
james-a-morris Dec 28, 2023
c1bb994
nit(access-modifier): immutable -> constant
james-a-morris Dec 28, 2023
a44f756
improve(docs): add state varaible natspec docs
james-a-morris Dec 28, 2023
71eac94
improve: fail if not enough balance
james-a-morris Dec 28, 2023
ebe93d6
chore(lint): resolve linter
james-a-morris Dec 28, 2023
bf674fe
improve: use l2GatewayRouter directly.
james-a-morris Dec 30, 2023
300735d
improve: pass l2Gas in constructor
james-a-morris Dec 30, 2023
3c52c33
docs(scroll): add signature explainer
james-a-morris Jan 4, 2024
e5f627b
improve: add second gas limit for arb messages
james-a-morris Jan 5, 2024
2a4d082
improve: remove comment
james-a-morris Jan 5, 2024
e9b4443
improve: use single internal function
james-a-morris Jan 5, 2024
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ NODE_URL_1=https://mainnet.infura.com/xxx yarn hardhat deploy --tags HubPool --n
ETHERSCAN_API_KEY=XXX yarn hardhat etherscan-verify --network mainnet --license AGPL-3.0 --force-license --solc-input
```

## Tasks

##### Finalize Scroll Claims from L2 -> L1 (Mainnet | Sepolia)

```shell
yarn hardhat finalize-scroll-claims --l2-address {operatorAddress}
```

## Slither

[Slither](https://github.com/crytic/slither) is a Solidity static analysis framework written in Python 3. It runs a
Expand Down
124 changes: 124 additions & 0 deletions contracts/Scroll_SpokePool.sol
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;

/**
* @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;
// 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();
Copy link
Member

Choose a reason for hiding this comment

The 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 target of the L1 to L2 message?

Copy link
Contributor Author

@james-a-morris james-a-morris Jan 4, 2024

Choose a reason for hiding this comment

The 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 scroll's official github ( link ), the xDomainMessenger is set to be the _from sender (sender of the L1 txn) here. The transaction stack is l2TokenMessenger calls relayMessage -> _executeMessage.

_executeMessage

function _executeMessage(
        address _from,
        address _to,
        uint256 _value,
        bytes memory _message,
        bytes32 _xDomainCalldataHash
    ) internal {
        // @note check more `_to` address to avoid attack in the future when we add more gateways.
        require(_to != messageQueue, "Forbid to call message queue");
        _validateTargetAddress(_to);

        // @note This usually will never happen, just in case.
        require(_from != xDomainMessageSender, "Invalid message sender"); 

the 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

        xDomainMessageSender = _from;
        // solhint-disable-next-line avoid-low-level-calls
        (bool success, ) = _to.call{value: _value}(_message);
        // reset value to refund gas.
        xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; <--- Immediately resets
        ...
    }

In the above ^ it's only set to be read within the context of _to.call(...) and then is immediately reset

relayMessage

    function relayMessage(
        address _from,
        address _to,
        uint256 _value,
        uint256 _nonce,
        bytes memory _message
    ) external override whenNotPaused {
        // It is impossible to deploy a contract with the same address, reentrance is prevented in nature.
        require(AddressAliasHelper.undoL1ToL2Alias(_msgSender()) == counterpart, "Caller is not L1ScrollMessenger");

        bytes32 _xDomainCalldataHash = keccak256(_encodeXDomainCalldata(_from, _to, _value, _nonce, _message));

        require(!isL1MessageExecuted[_xDomainCalldataHash], "Message was already successfully executed");

        _executeMessage(_from, _to, _value, _message, _xDomainCalldataHash);
    }

From the above, what we're really looking for is that _msgSender() which is the msg.sender is the counterpart domain of the L1ScrollMessenger. Ergo if we pass that require, it must be the case that the calling contract is the trusted L2ScrollMessenger.

L2ScrollMessenger

We have an implicit trust that the L2ScrollMessenger is going to set things right.

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);
}
}
126 changes: 126 additions & 0 deletions contracts/chain-adapters/Scroll_Adapter.sol
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;

/**
* @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.
l1ScrollMessenger.sendMessage{ value: _generateRelayerFee() }(target, 0, message, l2GasLimit);
Copy link
Contributor

Choose a reason for hiding this comment

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

If our gas limit is too high and gas is left unused on the destination, does that money get lost or is it refunded somehow?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per the TG group chat with the Scroll team, our ETH may get burned if we overestimate our gas limit. However, we were told in a previous message that 250,000 is an acceptable amount.

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
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");
}
}
26 changes: 26 additions & 0 deletions deploy/026_deploy_scroll_adapter.ts
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"];
30 changes: 30 additions & 0 deletions deploy/027_deploy_scroll_spokepool.ts
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];

await deployNewProxy("Scroll_SpokePool", constructorArgs, initArgs);
};
module.exports = func;
func.tags = ["ScrollSpokePool", "scroll"];
13 changes: 12 additions & 1 deletion deploy/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
l2WrappedMatic: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
baseCrossDomainMessenger: "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa",
baseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
scrollERC20GatewayRouter: "0xF8B1378579659D8F7EE5f3C929c2f3E332E41Fd6",
scrollMessengerRelay: "0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367",
},
4: {
weth: "0xc778417E063141139Fce010982780140Aa0cD5Ab",
Expand Down Expand Up @@ -71,7 +73,10 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
},
11155111: {
finder: "0xeF684C38F94F48775959ECf2012D7E864ffb9dd4",
weth: "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9",
weth: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
scrollERC20GatewayRouter: "0x13FBE0D0e5552b8c9c4AE9e2435F38f37355998a",
scrollMessengerRelay: "0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A",
scrollGasPriceOracle: "0x247969F4fad93a33d4826046bc3eAE0D36BdE548",
},
};

Expand Down Expand Up @@ -104,6 +109,12 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
l2Weth: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
zkErc20Bridge: "0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102",
},
534351: {
l2Weth: "0x5300000000000000000000000000000000000004",
scrollERC20GatewayRouter: "0x9aD3c5617eCAa556d6E166787A97081907171230",
scrollGasPriceOracle: "0x5300000000000000000000000000000000000002",
scrollMessenger: "0xba50f5340fb9f3bd074bd638c9be13ecb36e603d",
},
};

export const POLYGON_CHAIN_IDS: { [l1ChainId: number]: number } = {
Expand Down
1 change: 1 addition & 0 deletions deployments/scroll-sepolia/.chainId
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
534351
Loading