Skip to content
Draft
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: 1 addition & 1 deletion .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
run: forge test
- name: Coverage
working-directory: contracts
run: forge coverage --exclude-tests --report=lcov
run: forge coverage --ir-minimum --exclude-tests --report=lcov
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
Expand Down
5 changes: 4 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
solc_version = "0.8.33"
optimizer = true
optimizer_runs = 200
via_ir = false
via_ir = true
test = 'test'
script = 'scripts'
fs_permissions = [
Expand All @@ -21,6 +21,9 @@ ignored_error_codes = [

no_match_test = "testRegenerate*"

# Coverage: use --ir-minimum to avoid "stack too deep" when running forge coverage
# Example: forge coverage --ir-minimum --exclude-tests --report=lcov

# Production profile: via_ir enabled for deployment contracts
[profile.production]
via_ir = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
pragma solidity 0.8.33;

import {Script, console} from "forge-std/Script.sol";
import {SPOKE_POOL as SEPOLIA_SPOKE_POOL, WETH9 as SEPOLIA_WETH9} from "../constants/Sepolia.sol";
import {SPOKE_POOL as MAINNET_SPOKE_POOL, WETH9 as MAINNET_WETH9} from "../constants/Mainnet.sol";
import {SPOKE_POOL as SEPOLIA_SPOKE_POOL, WETH9 as SEPOLIA_WETH9, GATEWAY as SEPOLIA_GATEWAY} from "../constants/Sepolia.sol";
import {SPOKE_POOL as MAINNET_SPOKE_POOL, WETH9 as MAINNET_WETH9, GATEWAY as MAINNET_GATEWAY} from "../constants/Mainnet.sol";
import {SnowbridgeL1Adaptor} from "../../../../src/l2-integration/SnowbridgeL1Adaptor.sol";

contract DeploySnowbridgeL1Adaptor is Script {
Expand All @@ -16,18 +16,21 @@ contract DeploySnowbridgeL1Adaptor is Script {
address SPOKE_POOL_ADDRESS;
address BASE_MULTI_CALL_HANDLER_ADDRESS;
address WETH9_ADDRESS;
address GATEWAY_ADDRESS;

if (keccak256(bytes(vm.envString("L1_NETWORK"))) == keccak256(bytes("mainnet"))) {
SPOKE_POOL_ADDRESS = MAINNET_SPOKE_POOL;
WETH9_ADDRESS = MAINNET_WETH9;
GATEWAY_ADDRESS = MAINNET_GATEWAY;
} else if (keccak256(bytes(vm.envString("L1_NETWORK"))) == keccak256(bytes("sepolia"))) {
SPOKE_POOL_ADDRESS = SEPOLIA_SPOKE_POOL;
WETH9_ADDRESS = SEPOLIA_WETH9;
GATEWAY_ADDRESS = SEPOLIA_GATEWAY;
} else {
revert("Unsupported L1 network");
}

snowbridgeL1Adaptor = new SnowbridgeL1Adaptor(SPOKE_POOL_ADDRESS, WETH9_ADDRESS);
snowbridgeL1Adaptor = new SnowbridgeL1Adaptor(SPOKE_POOL_ADDRESS, WETH9_ADDRESS, GATEWAY_ADDRESS);
console.log("Snowbridge L1 Adaptor deployed at:", address(snowbridgeL1Adaptor));
return;
}
Expand Down
19 changes: 17 additions & 2 deletions contracts/src/l2-integration/SnowbridgeL1Adaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {WETH9} from "canonical-weth/WETH9.sol";
import {ISpokePool, IMessageHandler} from "./interfaces/ISpokePool.sol";
import {DepositParams, Instructions, Call} from "./Types.sol";
import {Agent} from "../Agent.sol";

contract SnowbridgeL1Adaptor {
using SafeERC20 for IERC20;
ISpokePool public immutable SPOKE_POOL;
WETH9 public immutable L1_WETH9;
address public immutable GATEWAY;

/**************************************
* EVENTS *
Expand All @@ -19,15 +21,27 @@ contract SnowbridgeL1Adaptor {
event DepositCallInvoked(bytes32 topic, uint256 depositId);
event DepositCallFailed(bytes32 topic);

constructor(address _spokePool, address _l1weth9) {
constructor(address _spokePool, address _l1weth9, address _gateway) {
SPOKE_POOL = ISpokePool(_spokePool);
L1_WETH9 = WETH9(payable(_l1weth9));
GATEWAY = _gateway;
}

modifier onlyRegisteredGatewayAgent() {
require(
Agent(payable(msg.sender)).GATEWAY() == GATEWAY,
"Caller is not a registered Gateway Agent"
Comment on lines +32 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Agent(payable(msg.sender)).GATEWAY() == GATEWAY,
"Caller is not a registered Gateway Agent"
IGatewayV2(GATEWAY).isAgent(msg.sender),
"Caller is not a registered Gateway Agent"

Otherwise someone can deploy a contract that returns the expected GATEWAY address from a GATEWAY() function without actually being a real Agent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Added test in #1733 to demonstrate.

Copy link
Contributor Author

@yrong yrong Mar 3, 2026

Choose a reason for hiding this comment

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

Nice catch!

It seems we don't have IGatewayV2(GATEWAY).isAgent(msg.sender) implemented yet? To achieve this, I assume we would need an additional mapping from agent address to ID in storage?

mapping(bytes32 agentID => address) agents;

Copy link
Contributor

@alistair-singh alistair-singh Mar 3, 2026

Choose a reason for hiding this comment

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

I feel this is valid flaw, but if someone went to all that trouble to deploy a Fake Agent contract which claimed to be a valid Agent with our Gateway, they would still only affect their own fake agent right?

This is only some extra hardening so make sure average users don't misuse the L1 Wrapper and hurt themselves. My understanding is that there is no threat to valid agents or the gateway here.

);
_;
}

// Send ERC20 token on L1 to L2, the fee (params.inputAmount - params.outputAmount) should be calculated off-chain
// following https://docs.across.to/reference/api-reference#get-swap-approval
// The call requires pre-funding of the contract with the input tokens.
function depositToken(DepositParams calldata params, address recipient, bytes32 topic) public {
function depositToken(DepositParams calldata params, address recipient, bytes32 topic)
public
onlyRegisteredGatewayAgent
{
require(params.inputToken != address(0), "Input token cannot be zero address");
checkInputs(params, recipient);
IERC20(params.inputToken).forceApprove(address(SPOKE_POOL), params.inputAmount);
Expand All @@ -54,6 +68,7 @@ contract SnowbridgeL1Adaptor {
// The call requires pre-funding of the contract with native Ether.
function depositNativeEther(DepositParams calldata params, address recipient, bytes32 topic)
public
onlyRegisteredGatewayAgent
{
require(
params.inputToken == address(0),
Expand Down
1 change: 1 addition & 0 deletions contracts/src/l2-integration/SnowbridgeL2Adaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ contract SnowbridgeL2Adaptor {
);
L2_WETH9.deposit{value: params.inputAmount}();
} else {
require(msg.value == 0, "Sent value must be zero for WETH deposits");
// Deposit WETH
IERC20(address(L2_WETH9))
.safeTransferFrom(msg.sender, address(this), params.inputAmount);
Expand Down
Loading
Loading