L2AnchoringManager
The L2AnchoringManager is the L2-side orchestrator for the L1 anchoring pipeline. It manages a queue of user-submitted attestation roots, receives cross-chain notifications from L1, verifies batch integrity, and mints NFT certificates.
submitForL1Anchoring
Users call this function to request L1 anchoring for an existing EAS attestation:
function submitForL1Anchoring(
bytes32 attestationId
) external payable nonReentrant
Validation Steps
- Duplicate check: The attestation must not already be submitted.
- Fee check:
msg.value >= feeOracle.getFloorFee(). - Attestation validation:
- Schema must be
CONTENT_HASH_SCHEMA. - Expiration must be
0(non-expiring). - Must be non-revocable.
- Schema must be
- Decode root:
abi.decode(attestation.data, (bytes32)).
Storage
struct AnchoringRecord {
bytes32 root; // User's content hash
bytes32 attestationId; // EAS attestation ID
uint256 blockNumber; // L2 block of submission
}
indexToRecords[queueIndex] = record;
attestationIdToIndex[attestationId] = queueIndex;
queueIndex++;
The queueIndex starts at 1 and increments monotonically. Index 0 is reserved as a sentinel for “not found”.
Fee Refund
If the user overpays, excess ETH is refunded to a configurable refund address (defaults to msg.sender).
FeeOracle
The FeeOracle calculates the per-item fee for L1 anchoring based on current gas prices:
$$ \text{fee} = \frac{\text{estimatedCost} \times \text{feeMultiplier}}{\text{expectedBatchSize} \times \text{PRECISION}} $$
Where the estimated batch cost is:
$$ \text{estimatedCost} = \underbrace{l1BaseFee \times l1Gas}{\text{L1 attestation cost}} + \underbrace{crossDomainGasPrice \times crossDomainGas}{\text{cross-chain message cost}} + \underbrace{l2BaseFee \times l2ExecutionGas}_{\text{L2 finalization cost}} $$
And L2 execution gas scales with batch size:
$$ l2ExecutionGas = l2ExecutionScalar \times batchSize + l2ExecutionOverhead $$
Default Parameters
| Parameter | Default | Description |
|---|---|---|
l1GasEstimated | 350,000 | Gas to attest batch on L1 |
crossDomainGasEstimated | 110,000 | Gas for L1→L2 message |
l2ExecutionScalar | 3,500 | Per-item L2 gas |
l2ExecutionOverhead | 35,000 | Base L2 gas |
expectedBatchSize | 256 | Assumed items per batch |
feeMultiplier | 1.5 × 10¹⁸ | Safety margin (1.5×) |
The fee oracle reads l1BaseFee from Scroll’s L1 gas price oracle predeployed at 0x5300000000000000000000000000000000000002.
Queue Index Tracking
The manager maintains two indices:
queueIndex: next available slot for new submissions (monotonically increasing).confirmedIndex: the boundary of confirmed batches. All entries with index <confirmedIndexare confirmed.
┌──────────┬───────────────────┬──────────────┐
│Confirmed │ Pending Batch │ Unprocessed │
│ [1, ci) │ [ci, ci+count) │ [ci+count, qi)│
└──────────┴───────────────────┴──────────────┘
1 ci qi
notifyAnchored
Called by the L2 Scroll Messenger when the L1 gateway successfully timestamps a batch:
function notifyAnchored(
bytes32 claimedRoot,
uint256 startIndex,
uint256 count,
uint256 l1Timestamp,
uint256 l1BlockNumber
) external
Guards:
msg.sendermust be the L2 Scroll Messenger.xDomainMessageSendermust be the L1 Gateway.startIndexmust equalconfirmedIndex(sequential batches only).- No pending batch can exist (prevents overlapping batches).
The function stores a PendingL1Batch for later verification and finalization.
finalizeBatch
Anyone can call this function to complete a pending batch:
function finalizeBatch() external nonReentrant
- Load the pending batch.
- Reconstruct the Merkle tree from stored
AnchoringRecordroots. - Verify:
MerkleTree.computeRoot(leaves) == pendingBatch.claimedRoot. - Update
confirmedIndex = startIndex + count. - Store the finalized
L1Batchrecord. - Clear the pending batch.
The on-chain Merkle verification ensures the relayer cannot claim a fraudulent root. The contract independently reconstructs the tree from its own stored data and compares.