Skip to content

[2/2] Decouple ETH Bridge from signal service #113

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

Merged
merged 50 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
807fa56
remove authorize in the commitment store
LeoPatOZ Apr 30, 2025
717b414
update to get commitment from a 'trusted' source
LeoPatOZ Apr 30, 2025
82cf729
allow genesis to start at the latest publication id
LeoPatOZ Apr 30, 2025
aa153b4
dissallow state proofs when verifying signal
LeoPatOZ Apr 30, 2025
f025453
start
LeoPatOZ Apr 30, 2025
1b51d2d
add commitment publisher to SS
LeoPatOZ Apr 30, 2025
fd2a26c
remove namespace and decouple bridge
LeoPatOZ Apr 30, 2025
207dd3f
trusted commitment store
LeoPatOZ Apr 30, 2025
9e42c20
fix test
LeoPatOZ Apr 30, 2025
d89d44b
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ Apr 30, 2025
a2b9d2d
update test
LeoPatOZ Apr 30, 2025
e5c3a4d
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ Apr 30, 2025
09e908b
update test
LeoPatOZ Apr 30, 2025
6c2fdd8
inter
LeoPatOZ Apr 30, 2025
6626f47
update test
LeoPatOZ Apr 30, 2025
9eae9dc
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ Apr 30, 2025
290de34
update test
LeoPatOZ Apr 30, 2025
44e8e97
fix deployment issues in test
LeoPatOZ Apr 30, 2025
ac12b20
fix test 1/n
LeoPatOZ Apr 30, 2025
bad54e4
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ Apr 30, 2025
cccab08
fix bug in libsignal
LeoPatOZ May 1, 2025
7f14514
broken
LeoPatOZ May 1, 2025
2608edb
Remove unnecessary import
nikeshnazareth May 5, 2025
df6f9e4
Rename storage commitment error message and change comment
nikeshnazareth May 5, 2025
931c8b0
Remove stray "ff" in comment
nikeshnazareth May 6, 2025
a22949e
Fix typo
nikeshnazareth May 6, 2025
0187564
Replace commitmentId with publicationId when describing commitment "h…
nikeshnazareth May 6, 2025
1b47fde
Remove obsolete "namespace" from a comment
nikeshnazareth May 6, 2025
61cfd98
Add comment explaining address(this) in verifySignal
nikeshnazareth May 6, 2025
bc686f2
Fix typo
nikeshnazareth May 6, 2025
f0c4b48
Remove comment about native bridging from SignalService
nikeshnazareth May 6, 2025
d9340f6
fix comment about state proof in lib signal
LeoPatOZ May 6, 2025
4528794
improve natspec and remove unnecesary imports
ggonzalez94 May 6, 2025
9cdac70
update comments
LeoPatOZ May 6, 2025
5b58aee
update comments
LeoPatOZ May 6, 2025
35ea231
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ May 6, 2025
7448d95
update comments
LeoPatOZ May 6, 2025
252b904
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ May 6, 2025
0a64605
revert changes
LeoPatOZ May 6, 2025
070991f
pure
LeoPatOZ May 6, 2025
01b3266
Merge branch 'simplify-commitment-store' into decouple-eth-bridge
LeoPatOZ May 6, 2025
d543b43
Remove obsolete comment
nikeshnazareth May 7, 2025
1b77c8f
Remove obsolete reference to SIGNAL_NAMESPACE
nikeshnazareth May 7, 2025
417f198
Bug fix: Use counterpart bridge address to derive signal slot
nikeshnazareth May 7, 2025
0d025ca
add nonReentrant using ReentrancyGuardTransient
ggonzalez94 May 7, 2025
6a6b292
better spacing
LeoPatOZ May 7, 2025
1c717f9
Update src/protocol/ETHBridge.sol
LeoPatOZ May 7, 2025
dfe6667
Merge branch 'decouple-eth-bridge' of github.com:OpenZeppelin/minimal…
LeoPatOZ May 7, 2025
2114335
fix rust
LeoPatOZ May 7, 2025
f2853c6
Merge branch 'signal-service' into decouple-eth-bridge
LeoPatOZ May 8, 2025
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
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ gas_reports_ignore = ["MockDelayedInclusionStore", "MockCheckpointTracker", ""]

remappings = [
"@optimism/=lib/optimism/",
"@vendor/=src/vendor/"
"@vendor/=src/vendor/",
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"
]

[fmt]
Expand Down
22 changes: 16 additions & 6 deletions offchain/deposit_signal_proof.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use alloy::primitives::{address, bytes, U256};
use alloy::primitives::{address, bytes, B256, U256};
use eyre::Result;

mod utils;
use utils::{deploy_signal_service, get_proofs, get_provider};
use utils::{deploy_eth_bridge, deploy_signal_service, get_proofs, get_provider};

mod signal_slot;
use signal_slot::{get_signal_slot, NameSpaceConst};
use signal_slot::get_signal_slot;

#[tokio::main]
async fn main() -> Result<()> {
Expand All @@ -23,6 +23,9 @@ async fn main() -> Result<()> {
let sender = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
let amount = U256::from(4000000000000000000_u128);

// This is the checkpoint tracker
let trusted_publisher = address!("0xCafac3dD18aC6c6e92c921884f9E4176737C052c");

let (provider, _anvil) = get_provider()?;

let signal_service = deploy_signal_service(&provider).await?;
Expand All @@ -32,16 +35,23 @@ async fn main() -> Result<()> {
signal_service.address()
);

println!("Sending ETH deposit signal...");
let builder = signal_service.deposit(sender, data).value(amount);
let eth_bridge =
deploy_eth_bridge(&provider, *signal_service.address(), trusted_publisher).await?;

println!("Deployed ETH bridge at address: {}", eth_bridge.address());

println!("Sending ETH deposit...");
let builder = eth_bridge.deposit(sender, data).value(amount);
let tx = builder.send().await?.get_receipt().await?;

// Get deposit ID from the transaction receipt logs
// possibly a better way to do this, but this works :)
let receipt_logs = tx.logs().get(0).unwrap().topics();
let deposit_id = receipt_logs.get(1).unwrap();
let depid: B256 =
"0xf9c183d2de58fbeb1a8917170139e980fa1b6e5a358ec83721e11c9f6e25eb18".parse()?;

let slot = get_signal_slot(deposit_id, &sender, NameSpaceConst::ETHBridge);
let slot = get_signal_slot(&depid, &eth_bridge.address());
get_proofs(&provider, slot, &signal_service).await?;

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions offchain/generic_signal_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use alloy::primitives::{Address, B256};
use eyre::{eyre, Result};

mod signal_slot;
use signal_slot::{get_signal_slot, NameSpaceConst};
use signal_slot::get_signal_slot;

mod utils;
use utils::{deploy_signal_service, get_proofs, get_provider};
Expand Down Expand Up @@ -48,7 +48,7 @@ async fn main() -> Result<()> {
let builder = signal_service.sendSignal(signal);
builder.send().await?.watch().await?;

let slot = get_signal_slot(&signal, &sender, NameSpaceConst::Signal);
let slot = get_signal_slot(&signal, &sender);
get_proofs(&provider, slot, &signal_service).await?;

Ok(())
Expand Down
35 changes: 4 additions & 31 deletions offchain/signal_slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,6 @@ use alloy::{
};
use eyre::{eyre, Result};

pub enum NameSpaceConst {
Signal,
ETHBridge,
}

impl NameSpaceConst {
pub fn value(&self) -> B256 {
match self {
NameSpaceConst::Signal => keccak256("generic-signal"),
NameSpaceConst::ETHBridge => keccak256("eth-bridge"),
}
}

pub fn from_arg(arg: &str) -> Result<Self> {
match arg {
"1" => Ok(NameSpaceConst::Signal),
"2" => Ok(NameSpaceConst::ETHBridge),
_ => Err(eyre!("Invalid namespace selection: must be '1' for normal signals or '2' for eth deposits")),
}
}
}

pub fn erc7201_slot(namespace: &Vec<u8>) -> B256 {
let namespace_hash = keccak256(namespace);

Expand All @@ -41,8 +19,8 @@ pub fn erc7201_slot(namespace: &Vec<u8>) -> B256 {
aligned_slot
}

pub fn get_signal_slot(signal: &B256, sender: &Address, namespace: NameSpaceConst) -> B256 {
let namespace = (signal, sender, namespace.value()).abi_encode_packed();
pub fn get_signal_slot(signal: &B256, sender: &Address) -> B256 {
let namespace = (signal, sender).abi_encode_packed();
return erc7201_slot(namespace.as_ref());
}

Expand All @@ -51,7 +29,7 @@ fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
return Err(eyre!(
"Usage: cargo run --bin signal_slot <signal (0x...)> <sender (0x...)> <namespace (1 or 2)>"
"Usage: cargo run --bin signal_slot <signal (0x...)> <sender (0x...)>"
));
}

Expand All @@ -65,11 +43,6 @@ fn main() -> Result<()> {
.parse()
.map_err(|_| eyre!("Invalid sender format: {}", sender_str))?;

let namespace = NameSpaceConst::from_arg(&args[3])?;

println!(
"Signal Slot: {:?}",
get_signal_slot(&signal, &sender, namespace)
);
println!("Signal Slot: {:?}", get_signal_slot(&signal, &sender));
Ok(())
}
30 changes: 25 additions & 5 deletions offchain/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,28 @@ use alloy::{
sol,
};

use ETHBridge::ETHBridgeInstance;
use SignalService::SignalServiceInstance;

use eyre::Result;
use serde_json::to_string_pretty;

const BLOCK_TIME: u64 = 5;

// NOTE: This needs to match the address of the rollup operator in the tests
// to ensure the signal service is deployed to the same address.
const ROLLUP_OPERATOR: Address = address!("0xCf03Dd0a894Ef79CB5b601A43C4b25E3Ae4c67eD");

sol!(
#[allow(missing_docs)]
#[sol(rpc)]
SignalService,
"./out/SignalService.sol/SignalService.json",
);

sol!(
#[allow(missing_docs)]
#[sol(rpc)]
ETHBridge,
"./out/ETHBridge.sol/ETHBridge.json"
);

pub fn get_provider() -> Result<(impl Provider, AnvilInstance)> {
let anvil = Anvil::new()
.block_time(BLOCK_TIME)
Expand All @@ -40,7 +44,23 @@ pub fn get_provider() -> Result<(impl Provider, AnvilInstance)> {
pub async fn deploy_signal_service(
provider: &impl Provider,
) -> Result<SignalServiceInstance<(), &impl Provider>> {
let contract = SignalService::deploy(provider, ROLLUP_OPERATOR).await?;
let contract = SignalService::deploy(provider).await?;
Ok(contract)
}

pub async fn deploy_eth_bridge(
provider: &impl Provider,
signal_service: Address,
trusted_publisher: Address,
) -> Result<ETHBridgeInstance<(), &impl Provider>> {
// L2 Eth bridge address
let counterpart = address!("0xDC9e4C83bDe3912E9B63A9BF9cE263F3309aB5d4");
// WARN: This is a slight hack for now to make sure the contract is deployed on the correct address
ETHBridge::deploy(provider, signal_service, trusted_publisher, counterpart).await?;

let contract =
ETHBridge::deploy(provider, signal_service, trusted_publisher, counterpart).await?;

Ok(contract)
}

Expand Down
49 changes: 20 additions & 29 deletions src/libs/LibSignal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,57 @@ library LibSignal {
using SlotDerivation for string;
using SafeCast for uint256;

bytes32 constant SIGNAL_NAMESPACE = keccak256("generic-signal");

/// @dev A `value` was signaled at a namespaced slot for the current `msg.sender`.
function signaled(bytes32 value) internal view returns (bool) {
return signaled(value, SIGNAL_NAMESPACE);
}

/// @dev A `value` was signaled at a namespaced slot for the current `msg.sender` and
/// namespace.
function signaled(bytes32 value, bytes32 namespace) internal view returns (bool) {
return signaled(value, msg.sender, namespace);
return signaled(value, msg.sender);
}

/// @dev A `value` was signaled at a namespaced slot. See `deriveSlot`.
function signaled(bytes32 value, address account, bytes32 namespace) internal view returns (bool) {
bytes32 slot = deriveSlot(value, account, namespace);
function signaled(bytes32 value, address account) internal view returns (bool) {
bytes32 slot = deriveSlot(value, account);
return slot.getBooleanSlot().value == true;
}

/// @dev Signal a `value` at a namespaced slot for the current `msg.sender` and namespace.
/// @dev Signal a `value` at a namespaced slot for the current `msg.sender`.
function signal(bytes32 value) internal returns (bytes32) {
return signal(value, msg.sender, SIGNAL_NAMESPACE);
return signal(value, msg.sender);
}

/// @dev Signal a `value` at a namespaced slot. See `deriveSlot`.
function signal(bytes32 value, address account, bytes32 namespace) internal returns (bytes32) {
bytes32 slot = deriveSlot(value, account, namespace);
function signal(bytes32 value, address account) internal returns (bytes32) {
bytes32 slot = deriveSlot(value, account);
slot.getBooleanSlot().value = true;
return slot;
}

/// @dev Returns the storage slot for a signal. Namespaced to the msg.sender, value and namespace.
function deriveSlot(bytes32 value, bytes32 namespace) internal view returns (bytes32) {
return deriveSlot(value, msg.sender, namespace);
/// @dev Returns the storage slot for a signal. Namespaced to the msg.sender and value
function deriveSlot(bytes32 value) internal view returns (bytes32) {
return deriveSlot(value, msg.sender);
}

/// @dev Returns the storage slot for a signal. Namespaced to the current account and value and namespace.
function deriveSlot(bytes32 value, address account, bytes32 namespace) internal pure returns (bytes32) {
return string(abi.encodePacked(value, account, namespace)).erc7201Slot();
/// @dev Returns the storage slot for a signal. Namespaced to the current account and value.
function deriveSlot(bytes32 value, address account) internal pure returns (bytes32) {
return string(abi.encodePacked(value, account)).erc7201Slot();
}

/// @dev Performs a storage proof verification for a signal stored on the contract using this library
/// @param value The signal value to verify
/// @param namespace The namespace of the signal
/// @param sender The address that originally sent the signal on the source chain
/// @param root The state root or storage root from the source chain to verify against
/// @param accountProof Merkle proof for the contract's account against the state root. Empty if we are using a
/// storage root.
/// @param storageProof Merkle proof for the derived storage slot against the account's storage root
/// @param root The state root from the source chain to verify against
/// @param accountProof Merkle proof for the SignalService account against the state root.
/// @param storageProof Merkle proof for the derived storage slot against the SignalService's storage root
/// @dev We can use `address(this)` as the SignalService address, even when `root` refers to a different chain,
/// because we assume the SignalService is deployed at the same address on every chain.
function verifySignal(
bytes32 value,
bytes32 namespace,
address sender,
bytes32 root,
bytes[] memory accountProof,
bytes[] memory storageProof
) internal pure {
) internal view {
bytes32 encodedBool = bytes32(uint256(1));
LibTrieProof.verifyMerkleProof(
root, sender, deriveSlot(value, sender, namespace), encodedBool, accountProof, storageProof
root, address(this), deriveSlot(value, sender), encodedBool, accountProof, storageProof
);
}
}
54 changes: 35 additions & 19 deletions src/protocol/ETHBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,78 @@
pragma solidity ^0.8.28;

import {IETHBridge} from "./IETHBridge.sol";
import {ISignalService} from "./ISignalService.sol";
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";

/// @dev Abstract ETH bridging contract to send native ETH between L1 <-> L2 using storage proofs.
/// @dev ETH bridging contract to send native ETH between L1 <-> L2 using storage proofs.
///
/// IMPORTANT: No recovery mechanism is implemented in case an account creates a deposit that can't be claimed.
contract ETHBridge is IETHBridge {
contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
mapping(bytes32 id => bool claimed) private _claimed;

/// Incremental nonce to generate unique deposit IDs.
uint256 private _globalDepositNonce;

/// Namespace for the ETH bridge.
bytes32 internal constant ETH_BRIDGE_NAMESPACE = keccak256("eth-bridge");
ISignalService public immutable signalService;

/// @dev Trusted source of commitments in the `CommitmentStore` that the bridge will use to validate withdrawals
/// @dev This is the Anchor on L2 and the Checkpoint Tracker on the L1
address public immutable trustedCommitmentPublisher;

/// @dev The counterpart bridge contract on the other chain.
/// This is used to locate deposit signals inside the other chain's state root.
/// WARN: This address has no significance (and may be untrustworthy) on this chain.
address public immutable counterpart;

constructor(address _signalService, address _trustedCommitmentPublisher, address _counterpart) {
require(_signalService != address(0), "Empty signal service");
require(_trustedCommitmentPublisher != address(0), "Empty trusted publisher");

signalService = ISignalService(_signalService);
trustedCommitmentPublisher = _trustedCommitmentPublisher;
counterpart = _counterpart;
}

/// @inheritdoc IETHBridge
function claimed(bytes32 id) public view virtual returns (bool) {
function claimed(bytes32 id) public view returns (bool) {
return _claimed[id];
}

/// @inheritdoc IETHBridge
function getDepositId(ETHDeposit memory ethDeposit) public view virtual returns (bytes32 id) {
function getDepositId(ETHDeposit memory ethDeposit) public pure returns (bytes32 id) {
return _generateId(ethDeposit);
}

/// @inheritdoc IETHBridge
function deposit(address to, bytes memory data) public payable virtual returns (bytes32 id) {
function deposit(address to, bytes memory data) public payable returns (bytes32 id) {
ETHDeposit memory ethDeposit = ETHDeposit(_globalDepositNonce, msg.sender, to, msg.value, data);
id = _generateId(ethDeposit);
unchecked {
++_globalDepositNonce;
}

signalService.sendSignal(id);
emit DepositMade(id, ethDeposit);
}

/// @inheritdoc IETHBridge
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof)
external
returns (bytes32 id)
{
id = _generateId(ethDeposit);
}

/// @dev Processes deposit claim by id.
/// @param id Identifier of the deposit
/// @param ethDeposit Deposit to process
function _processClaimDepositWithId(bytes32 id, ETHDeposit memory ethDeposit) internal virtual {
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof) external nonReentrant {
bytes32 id = _generateId(ethDeposit);
require(!claimed(id), AlreadyClaimed());

signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof);

_claimed[id] = true;
_sendETH(ethDeposit.to, ethDeposit.amount, ethDeposit.data);

emit DepositClaimed(id, ethDeposit);
}

/// @dev Function to transfer ETH to the receiver but ignoring the returndata.
/// @param to Address to send the ETH to
/// @param value Amount of ETH to send
/// @param data Data to send to the receiver
function _sendETH(address to, uint256 value, bytes memory data) internal virtual {
function _sendETH(address to, uint256 value, bytes memory data) internal {
(bool success,) = to.call{value: value}(data);
require(success, FailedClaim());
}
Expand Down
Loading
Loading