Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Security Considerations

This chapter covers the security properties and protections built into the UTS protocol across both the smart contract layer and the off-chain infrastructure.

Access Control

Smart Contract Roles

RoleContractPrivilege
DEFAULT_ADMIN_ROLEL1Gateway, L2Manager, FeeOracleConfigure contract parameters, grant/revoke roles
SUBMITTER_ROLEL1AnchoringGatewaySubmit batches to L1
FEE_COLLECTOR_ROLEL2AnchoringManagerWithdraw accumulated fees
UPDATER_ROLEFeeOracleUpdate fee parameters

Admin Transfer Delay

Both L1AnchoringGateway and L2AnchoringManager use OpenZeppelin’s AccessControlDefaultAdminRulesUpgradeable with a 3-day transfer delay for the admin role. This prevents:

  • Instant admin takeover via compromised keys.
  • Flash-loan governance attacks.
  • Accidental admin transfers.

The FeeOracle uses the non-upgradeable variant with the same 3-day delay.

Reentrancy Protection

All state-modifying external functions use ReentrancyGuardTransient:

ContractProtected Functions
L1AnchoringGatewaysubmitBatch()
L2AnchoringManagersubmitForL1Anchoring(), claimNFT(), withdrawFees()

The transient variant (EIP-1153) uses transient storage for the reentrancy flag, saving gas compared to the traditional storage-based guard.

Cross-Chain Message Authentication

The L2AnchoringManager validates cross-chain messages with a two-layer check:

// In notifyAnchored():
require(msg.sender == address(l2Messenger));
require(l2Messenger.xDomainMessageSender() == l1Gateway);
  1. msg.sender must be the L2 Scroll Messenger — prevents direct calls from arbitrary addresses.
  2. xDomainMessageSender must be the L1 Gateway — prevents spoofed messages from other L1 contracts.

Both conditions must hold simultaneously. This ensures that notifyAnchored can only be triggered by a legitimate cross-chain message originating from the authorized L1 gateway contract.

Merkle Proof Verification

The L2AnchoringManager independently verifies batch integrity during finalization:

function finalizeBatch() external {
    // Reconstruct leaves from stored records
    bytes32[] memory leaves = new bytes32[](count);
    for (uint256 i = 0; i < count; i++) {
        leaves[i] = indexToRecords[startIndex + i].root;
    }

    // Verify against claimed root
    require(MerkleTree.verify(leaves, claimedRoot));
}

This prevents a malicious relayer from submitting a fraudulent Merkle root that doesn’t match the actual queued entries. The contract uses its own stored data (not relayer-provided data) to reconstruct the tree.

Sequential Batch Ordering

The L2AnchoringManager enforces strict sequential batch ordering:

require(startIndex == confirmedIndex);
require(pendingBatch.count == 0); // No overlapping batches

This prevents:

  • Gap attacks: skipping queue entries to exclude specific timestamps.
  • Overlap attacks: double-processing entries across multiple batches.
  • Reorder attacks: processing entries out of order.

Input Validation

Batch Size Bounds

uint256 constant MAX_BATCH_SIZE = 512;
require(count >= 1 && count <= MAX_BATCH_SIZE);

Gas Limit Bounds

uint256 constant MIN_GAS_LIMIT = 110_000;
uint256 constant MAX_GAS_LIMIT = 200_000;
require(gasLimit >= MIN_GAS_LIMIT && gasLimit <= MAX_GAS_LIMIT);

Address Zero Checks

All address setters validate against address(0) to prevent accidental misconfiguration.

Attestation Immutability

Submitted attestations are verified to be non-revocable and non-expiring:

require(attestation.expirationTime == 0);
require(!attestation.revocable);

This ensures that once an attestation is used for L1 anchoring, it cannot be invalidated by the attester.

Compare-and-Set Status Transitions

The relayer’s batch state machine uses compare-and-set (CAS) semantics in SQL:

UPDATE l1_batch
SET status = ?new_status, updated_at = ?now
WHERE id = ?id AND status = ?expected_status;

The indexer updates the status once new event arrives. There’s a possibility that the event arrives just before the receipt, hence we use CAS.

Fail-Fast Error Handling

The journal implements a fatal error flag:

#![allow(unused)]
fn main() {
fatal_error: AtomicBool
}

Once set, all journal operations immediately return Error::Fatal. The calendar server initiates graceful shutdown rather than risk silent data corruption. This is preferable to attempting recovery from an unknown state.

EAS Contract Addresses

UTS uses well-known, audited EAS contract addresses per chain:

ChainAddress
Mainnet0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587
Scroll0xC47300428b6AD2c7D03BB76D05A176058b47E6B0
Scroll Sepolia0xaEF4103A04090071165F78D45D83A0C0782c2B2a
Sepolia0xC47300428b6AD2c7D03BB76D05A176058b47E6B0

These are hardcoded via a compile-time perfect hash map (phf_map), preventing runtime misconfiguration.