Skip to content
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

Add LibValueTicket and ETHBridge for native token bridging #52

Open
wants to merge 21 commits into
base: state-syncer
Choose a base branch
from
Open
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
83 changes: 83 additions & 0 deletions src/libs/LibValueTicket.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibSignal} from "../libs/LibSignal.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// @dev Library to create and verify value tickets per chain (source and destination), nonce, sender, receiver.
///
/// Tickets can be used to bridge ETH or standard tokens (e.g. ERC20, ERC271, ERC1155) with the only condition of having
/// the same contract deployed on both chains and a trusted source of a state root.
library LibValueTicket {
using SafeCast for uint256;
using StorageSlot for bytes32;
using LibSignal for *;
using SlotDerivation for *;

struct ValueTicket {
uint64 chainId;
uint64 nonce;
address from;
address to;
uint256 value;
}

/// @dev The ticket is not valid (i.e. couldn't be verified)
error InvalidTicket();

/// @dev Unique ticket identifier.
function id(ValueTicket memory ticket) internal pure returns (bytes32 _id) {
return keccak256(abi.encode(ticket));
}

/// @dev Verifies that a ticket created with `nonce` by the receiver (`from`) is valid for the receiver (`to`)
/// to claim `value` on this chain. It does so by performing an storage proof of `address(this)` on the source chain
/// using `accountProof` and validating it against the network state `root` using `proof`.
/// The `root` MUST be trusted.
function verifyTicket(ValueTicket memory ticket, bytes32 root, bytes[] memory accountProof, bytes[] memory proof)
internal
view
returns (bool verified, bytes32 _id)
{
_id = id(ticket);
(verified,) = address(this).verifySignal(root, ticket.chainId, _id, accountProof, proof);
return (verified, _id);
}

/// @dev Creates a ticket with `msg.value` ETH for the receiver (`to`) to claim on the `destinationChainId`.
function createTicket(uint64 destinationChainId, address from, address to, uint256 value)
internal
returns (ValueTicket memory ticket)
{
ticket = ValueTicket(destinationChainId, _useNonce(from).toUint64(), from, to, value);
bytes32 _id = id(ticket);
_id.signal();
return ticket;
}

/// @dev Reverts if a ticket was not created by `from` with `nonce` on `chainId`. See `verifyTicket`.
function checkTicket(ValueTicket memory ticket, bytes32 root, bytes[] memory accountProof, bytes[] memory proof)
internal
view
returns (bytes32)
{
(bool valid, bytes32 _id) = verifyTicket(ticket, root, accountProof, proof);
require(valid, InvalidTicket());
return _id;
}

/// @dev Consumes a nonce and returns the current value and increments nonce.
function _useNonce(address account) private returns (uint256) {
// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be
// decremented or reset. This guarantees that the nonce never overflows.

unchecked {
// It is important to do x++ and not ++x here.
// slot: keccak256(abi.encode(uint256(keccak256("LibValueTicket.nonces")) - 1)) & ~bytes32(uint256(0xff))
return 0x23c95d7a21dec6ba744555d361d2572ad62017f33fd3da51a4ffa8cde254e900.deriveMapping(account)
.getUint256Slot().value++;
}
}
}
23 changes: 12 additions & 11 deletions src/protocol/CommitmentSyncer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ abstract contract CommitmentSyncer is ICommitmentSyncer {
}

/// @inheritdoc ICommitmentSyncer
function id(uint64 chainId, uint64 height, bytes32 commitment) public pure virtual returns (bytes32 value) {
function commitmentId(uint64 chainId, uint64 height, bytes32 commitment) public pure virtual returns (bytes32 id) {
return keccak256(abi.encodePacked(chainId, height, commitment));
}

Expand All @@ -48,10 +48,10 @@ abstract contract CommitmentSyncer is ICommitmentSyncer {
public
view
virtual
returns (bool valid, bytes32 value)
returns (bool valid, bytes32 id)
{
value = id(chainId, height, commitment);
return (LibTrieProof.verifyState(value.deriveSlot(), value, root, proof), value);
id = commitmentId(chainId, height, commitment);
return (LibTrieProof.verifyState(id.deriveSlot(), id, root, proof), id);
}

/// @inheritdoc ICommitmentSyncer
Expand All @@ -75,17 +75,18 @@ abstract contract CommitmentSyncer is ICommitmentSyncer {
virtual
onlyCheckpointTracker
{
_checkCommitment(chainId, height, commitment, root, proof);
_syncCommitment(chainId, height, commitment);
_syncCommitment(_checkCommitment(chainId, height, commitment, root, proof), chainId, height, commitment);
}

/// @dev Internal version of `syncCommitment` without access control and without verifying the commitment.
/// @dev Internal version of `syncCommitment` without access control.
/// Emits `CommitmentSynced` if the provided `height` is larger than `latestHeight` for `chainId`.
function _syncCommitment(uint64 chainId, uint64 height, bytes32 commitment) internal virtual {
function _syncCommitment(bytes32 id, uint64 chainId, uint64 height, bytes32 commitment) internal virtual {
if (latestHeight(chainId) < height) {
_latestHeight[chainId] = height;
_commitment[chainId][height] = commitment;
id(chainId, height, commitment).signal();
// Invariant:
// assert(id == commitmentId(chainId, height, commitment));
id.signal();
emit CommitmentSynced(chainId, height, commitment);
}
}
Expand All @@ -94,10 +95,10 @@ abstract contract CommitmentSyncer is ICommitmentSyncer {
function _checkCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
internal
virtual
returns (bytes32 value)
returns (bytes32 id)
{
bool valid;
(valid, value) = verifyCommitment(chainId, height, commitment, root, proof);
(valid, id) = verifyCommitment(chainId, height, commitment, root, proof);
require(valid, InvalidCommitment());
}

Expand Down
64 changes: 64 additions & 0 deletions src/protocol/ETHBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibTrieProof} from "../libs/LibTrieProof.sol";

import {LibValueTicket} from "../libs/LibValueTicket.sol";
import {IETHBridge} from "./IETHBridge.sol";

/// @dev Bridge implementation to send native ETH to other chains using storage proofs.
///
/// IMPORTANT: No recovery mechanism is implemented in case an account creates a ticket that can't be claimed. Consider
/// implementing one on top of this bridge for more specific use cases.
contract ETHBridge is IETHBridge {
using LibValueTicket for LibValueTicket.ValueTicket;

mapping(bytes32 id => bool) _claimed;

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

/// @inheritdoc IETHBridge
function ticketId(LibValueTicket.ValueTicket memory ticket) public view virtual returns (bytes32 id) {
return ticket.id();
}

/// @inheritdoc IETHBridge
function verifyTicket(
LibValueTicket.ValueTicket memory ticket,
bytes32 root,
bytes[] memory accountProof,
bytes[] memory proof
) public view virtual returns (bool verified, bytes32 id) {
return ticket.verifyTicket(root, accountProof, proof);
}

/// @inheritdoc IETHBridge
function createTicket(uint64 chainId, address to) external payable virtual {
emit ETHTicket(LibValueTicket.createTicket(chainId, msg.sender, to, msg.value));
}

/// @inheritdoc IETHBridge
function claimTicket(
LibValueTicket.ValueTicket memory ticket,
bytes32 root,
bytes[] memory accountProof,
bytes[] memory proof
) external virtual {
bytes32 id_ = ticket.checkTicket(root, accountProof, proof);
require(!claimed(id_), AlreadyClaimed());
_claimed[id_] = true;
_sendETH(ticket.to, ticket.value);
emit ETHTicketClaimed(ticket);
}

/// @dev Function to transfer ETH to the receiver but ignoring the returndata.
function _sendETH(address to, uint256 value) private returns (bool success) {
assembly ("memory-safe") {
success := call(gas(), to, value, 0, 0, 0, 0)
}
require(success, FailedClaim());
}
}
4 changes: 2 additions & 2 deletions src/protocol/ICommitmentSyncer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ interface ICommitmentSyncer {
error InvalidCommitment();

/// @dev Commitment identifier.
function id(uint64 chainId, uint64 height, bytes32 commitment) external pure returns (bytes32 value);
function commitmentId(uint64 chainId, uint64 height, bytes32 commitment) external pure returns (bytes32 id);

/// @dev Verifies that a `commitment` is valid for the provided `chainId`, `height` and `root`.
/// The `root` MUST be trusted.
function verifyCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
external
view
returns (bool valid, bytes32 value);
returns (bool valid, bytes32 id);

/// @dev Get commitment at a particular `height` for `chainId`.
function commitmentAt(uint64 chainId, uint64 height) external view returns (bytes32 commitment);
Expand Down
52 changes: 52 additions & 0 deletions src/protocol/IETHBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibValueTicket} from "../libs/LibValueTicket.sol";

/// @dev Bridges native value (i.e. ETH) by creating and verifying tickets using the `LibValueTicket`.
///
/// These can be created by sending value to the `createTicket` function. Later, the receiver can claim the ticket on
/// the destination chain by using a storage proof.
///
/// ETH bridge MUST be deployed at the same address on both chains.
interface IETHBridge {
/// @dev Sender (`from`) sent `value` with `nonce` to the receiver (`to`). Claimable on `chainId`.
event ETHTicket(LibValueTicket.ValueTicket ticket);

/// @dev A ticket was claimed.
event ETHTicketClaimed(LibValueTicket.ValueTicket ticket);

/// @dev Failed to call the receiver with value.
error FailedClaim();

/// @dev Ticket was already claimed.
error AlreadyClaimed();

/// @dev Whether the ticket identified by `id` has been claimed.
function claimed(bytes32 id) external view returns (bool);

/// @dev Ticket identifier.
function ticketId(LibValueTicket.ValueTicket memory ticket) external view returns (bytes32 id);

/// @dev Verifies if a ticket defined created on the `chainId` with `nonce` by the receiver (`from`) is valid
/// for the receiver `to` to claim `value` on this chain by performing an storage proof of this bridge address using
/// `accountProof` and validating it against the network state root using `proof`.
function verifyTicket(
LibValueTicket.ValueTicket memory ticket,
bytes32 root,
bytes[] memory accountProof,
bytes[] memory proof
) external view returns (bool verified, bytes32 id);

/// @dev Creates a ticket with `msg.value` ETH for the receiver (`to`) to claim on the `chainId`.
function createTicket(uint64 chainId, address to) external payable;

/// @dev Claims a ticket created on `chainId` by the sender (`from`) with `nonce`. The `value` ETH claimed is
/// sent to the receiver (`to`) after verifying the proofs. See `verifyTicket`.
function claimTicket(
LibValueTicket.ValueTicket memory ticket,
bytes32 root,
bytes[] memory accountProof,
bytes[] memory proof
) external;
}
26 changes: 26 additions & 0 deletions src/protocol/ISignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title ISignalService
/// @notice Interface for the SignalService contract.
interface ISignalService {
error SignalNotReceived();
error CallerNotAuthorised();

event SignalsReceived(bytes32[] signalSlots);

function sendSignal(bytes32 value) external returns (bytes32);

function receiveSignals(bytes32[] calldata signalSlots) external;

function verifySignal(
address account,
bytes32 root,
uint64 chainId,
bytes32 value,
bytes[] memory accountProof,
bytes[] memory storageProof
) external view;

function isSignalSent(bytes32 signal) external view returns (bool);
}
63 changes: 63 additions & 0 deletions src/protocol/SignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibSignal} from "../libs/LibSignal.sol";
import {ISignalService} from "./ISignalService.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";

/// @title SignalService
/// @notice A minimal implementation of a signal service using LibSignal.
contract SignalService is ISignalService {
using LibSignal for bytes32;
using StorageSlot for bytes32;

address internal _rollup;

mapping(bytes32 signal => bool isReceived) internal _receivedSignals;

constructor(address rollup_) {
_rollup = rollup_;
}

modifier onlyRollup() {
require(msg.sender == _rollup, CallerNotAuthorised());
_;
}

/// @dev Only required to be called on L1
function sendSignal(bytes32 value) external returns (bytes32) {
return value.signal();
}

/// @dev Only required to be called on L2
function receiveSignals(bytes32[] calldata signalSlots) external onlyRollup {
for (uint256 i; i < signalSlots.length; ++i) {
_receivedSignals[signalSlots[i]] = true;
}
emit SignalsReceived(signalSlots);
}

/// @dev Only required to be called on L2
function verifySignal(
address account,
bytes32 root,
uint64 chainId,
bytes32 value,
bytes[] memory accountProof,
bytes[] memory storageProof
) external view {
if (accountProof.length == 0) {
require(_receivedSignals[LibSignal.deriveSlot(chainId, account, value)], SignalNotReceived());
return;
}

(bool valid,) = LibSignal.verifySignal(account, root, chainId, value, accountProof, storageProof);
require(valid, SignalNotReceived());
}

/// @dev Only required to be called on L1
function isSignalSent(bytes32 signal) external view returns (bool) {
// This will return `false` when the signal itself is 0
return signal.getBytes32Slot().value != 0;
}
}
Loading
Loading