Building Bridges
Overview
Facet has no canonical bridge, but anyone can build and deploy a bridge. This guide covers the complete process from understanding bridge mechanics to deploying production-ready contracts.
How Bridging Works
Architecture Overview
Bridges on Facet consist of two main components:
L1 Contract: Handles deposits and withdrawals on Ethereum
L2 Contract: Manages the L2 token representation
Depositing Assets (L1 → Facet)
2
3
Withdrawing Assets (Facet → L1)
Withdrawals use Facet's ZK Fault Proof system:
1
2
Implementation Guide
1. L1 Bridge Contract
Start with the core deposit functionality:
import { LibFacet } from "facet-sol/src/utils/LibFacet.sol";
contract L1Bridge {
IERC20 public token;
address public l2Bridge;
function depositERC20(uint256 amount, address to) external {
// Transfer tokens from user
token.safeTransferFrom(msg.sender, address(this), amount);
// Encode the L2 transaction
bytes memory bridgeData = abi.encodeWithSelector(
IL2Bridge.finalizeERC20Deposit.selector,
amount,
to
);
// Send to Facet via 0xface7
LibFacet.sendFacetTransaction({
to: l2Bridge,
gasLimit: 250_000,
data: bridgeData
});
}
}
2. Withdrawal Verification
Integrate with the proof system for withdrawals. Here's a complete example:
interface IRollup {
function getProposal(uint256 id) external view returns (Proposal memory);
function canonicalProposalOf(uint256 block) external view returns (uint32);
}
contract L1Bridge {
IRollup public rollup;
function proveWithdrawal(
uint256 amount,
address to,
uint256 nonce,
uint256 proposalId,
Types.OutputRootProof calldata outputRootProof,
bytes[] calldata withdrawalProof
) external {
// Get proposal and verify it's canonical
IRollup.Proposal memory proposal = rollup.getProposal(proposalId);
require(
rollup.canonicalProposalOf(proposal.l2BlockNumber) == proposalId,
"proposal not canonical"
);
// Verify the output root matches
require(
proposal.rootClaim == Hashing.hashOutputRootProof(outputRootProof),
"invalid output root"
);
// Build withdrawal hash
bytes32 withdrawalHash = _hashWithdrawal(amount, to, nonce);
// Verify merkle proof of withdrawal in L2 state
bytes32 storageKey = keccak256(abi.encode(withdrawalHash, uint256(0)));
bool valid = SecureMerkleTrie.verifyInclusionProof({
_key: abi.encode(storageKey),
_value: hex"01", // value of 1 indicates withdrawal exists
_proof: withdrawalProof,
_root: outputRootProof.messagePasserStorageRoot
});
require(valid, "invalid merkle proof");
// Store proven withdrawal for later finalization
provenWithdrawals[withdrawalHash] = ProvenWithdrawal({
proposalId: uint32(proposalId),
provenAt: uint32(block.timestamp)
});
}
}
3. L2 Bridge Contract
Deploy on Facet to handle the L2 side:
contract L2Bridge is ERC20 {
address public constant MESSAGE_PASSER = 0x4200000000000000000000000000000000000016;
address public l1Bridge;
modifier onlyL1Bridge() {
// Check aliased address from L1
require(
msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Bridge),
"Not L1 bridge"
);
_;
}
function finalizeERC20Deposit(uint256 amount, address to)
external
onlyL1Bridge
{
_mint(to, amount);
}
function initiateERC20Withdrawal(uint256 amount, address to) external {
_burn(msg.sender, amount);
IL2ToL1MessagePasser(MESSAGE_PASSER).initiateWithdrawal(
l1Bridge,
0,
abi.encode(amount, to)
);
}
}
4. Adding Training Wheels
Consider what safety features your users need:
contract L1BridgeWithSafety is L1Bridge, Ownable {
bool public paused;
uint256 public withdrawalDelay = 24 hours;
mapping(bytes32 => bool) public blacklistedRoots;
modifier whenNotPaused() {
require(!paused, "Bridge paused");
_;
}
function finalizeWithdrawal(...) external whenNotPaused {
// Check delay has passed
require(
block.timestamp >= provenAt + withdrawalDelay,
"Delay not met"
);
// Check root not blacklisted
require(!blacklistedRoots[proposal.rootClaim], "Root blacklisted");
// Execute withdrawal
}
// Admin functions
function pause() external onlyOwner {
paused = true;
}
function setWithdrawalDelay(uint256 delay) external onlyOwner {
withdrawalDelay = delay;
}
}
Last updated