Skip to main content

L2 Reads from L1

Not yet available on UniFi Testnet

The Signal Service and the L1↔L2 synchronous composability it enables are not yet deployed to UniFi Testnet. The examples on this page describe the intended developer experience and cannot be run against the testnet today. This page will be updated with deployed contract addresses once the feature ships.

This page provides practical examples of how to implement L1↔L2 synchronous composability using UniFi's Signal Service. These examples demonstrate how L2 contracts can read and consume data from L1 within the same slot.

Note: Example code is illustrative - actual implementation may vary

// L2 Contract reading fresh L1 Chainlink data
contract L2PriceConsumer {
ISignalService public signalService;

function executeTradeWithFreshPrice(int256 ethPrice, uint256 blockNumber) external {
// Compute signal ID from provided price data
bytes32 signalId = keccak256(abi.encode("chainlink.eth.price", ethPrice, blockNumber));

// Verify the signal was received from L1
require(signalService.isSignalStored(signalId), "Price signal not confirmed");

// Execute trade logic with same-slot L1 price data
_executeTrade(ethPrice);
}
}

Same-Slot Deposit and Trade

// L2 DEX contract utilizing same-slot deposits
contract L2DEX {
ISignalService public signalService;

function swapWithDeposit(uint256 depositAmount, address tokenOut) external {
// Verify same-slot deposit signal from L1
bytes32 depositSignal = keccak256(abi.encode(
"deposit",
msg.sender,
depositAmount,
block.number
));

require(signalService.isSignalStored(depositSignal), "Deposit not confirmed");

// Execute swap immediately with deposited funds
_executeSwap(depositAmount, tokenOut, msg.sender);
}
}

Atomic Cross-Layer Operations

// L2 Contract coordinating atomic L1↔L2 operations
contract AtomicBridge {
function atomicDepositAndWithdraw(
uint256 depositAmount,
uint256 withdrawAmount
) external {
// Verify L1 deposit signal
bytes32 depositSignal = keccak256(abi.encode(
"deposit", msg.sender, depositAmount, block.number
));
require(signalService.isSignalStored(depositSignal), "Deposit failed");

// Process L2 operations
_processL2Operations(depositAmount);

// Initiate instant withdrawal back to L1
_initiateInstantWithdrawal(msg.sender, withdrawAmount);
}
}

Advanced Use Cases

Cross-Layer Arbitrage

contract ArbitrageBot {
ISignalService public signalService;

function executeArbitrage(
uint256 l1Price,
uint256 l2Price,
uint256 amount
) external {
// Verify L1 price signal
bytes32 priceSignal = keccak256(abi.encode("price.update", l1Price, block.number));
require(signalService.isSignalStored(priceSignal), "Price not confirmed");

// Execute arbitrage if profitable
if (l1Price > l2Price) {
_buyOnL2SellOnL1(amount, l1Price, l2Price);
} else if (l2Price > l1Price) {
_buyOnL1SellOnL2(amount, l1Price, l2Price);
}
}
}

Dynamic Collateral Management

contract DynamicVault {
ISignalService public signalService;

function adjustCollateralRatio(uint256 l1CollateralValue, uint256 blockNumber) external {
// Compute signal ID from provided collateral data
bytes32 collateralSignal = keccak256(abi.encode("collateral.value", l1CollateralValue, blockNumber));

// Verify the signal was received from L1
require(signalService.isSignalStored(collateralSignal), "Collateral signal not confirmed");

// Adjust L2 lending parameters based on L1 collateral
_updateLendingRatio(l1CollateralValue);
}
}

Best Practices

Signal Verification

Always verify that signals have been properly received before consuming them:

function verifySignal(bytes32 signalId) internal view {
require(signalService.isSignalStored(signalId), "Signal not received");
}

Replay Protection

Implement replay protection to prevent signal reuse:

mapping(bytes32 => bool) private consumedSignals;

function consumeSignalOnce(bytes32 signalId) internal {
require(!consumedSignals[signalId], "Signal already consumed");
consumedSignals[signalId] = true;
}

Fallback Mechanisms

Provide fallback options when signals are not available:

function processDataWithFallback(uint256 data, uint256 blockNumber) external returns (uint256) {
bytes32 signalId = keccak256(abi.encode("data.update", data, blockNumber));

if (signalService.isSignalStored(signalId)) {
// Use the verified signal data
return _processData(data);
} else {
// Fallback to cached or default value
return _getFallbackData();
}
}

Testing

When testing Signal Service integration:

  1. Testnet Environment: Use UniFi's testnet for realistic testing
  2. Mock Signals: Create mock signals for unit testing
  3. Edge Cases: Test signal failures and fallback scenarios
  4. Gas Optimization: Monitor gas usage for signal operations

For more information on Signal Service architecture and implementation details, see the Signal Service documentation.