Skip to main content

Multi-Rollup Applications

Overview

Applications embracing the "fat app" thesis leverage rollups as servers for their operations. These apps maintain contract instances on multiple chains, requiring seamless inter-contract communication for complex workflows.

State Sync is a compelling example built by our community that allows applications to synchronize key-value pairs across multiple chains in seconds. Learn more at Polymer State Sync.

🌐 Multi-Chain

Deploy contracts across multiple rollups simultaneously

⚡ Real-Time Sync

Synchronize state changes across chains in seconds

💰 Cost Efficient

No predefined pairs - broadcast to any chain

Architecture Overview

The multi-rollup pattern follows a simple yet powerful flow:

  1. Event Emission: Application emits events on any supported chain
  2. Proof Generation: Relayer requests cryptographic proof of the event
  3. Cross-Chain Execution: Proof validates and executes on destination chains

Key Benefits

tip

No Predefined Pairs: Unlike traditional messaging, events can be proven and executed on any supported chain without prior configuration.

Benefits: Flexibility (set state on one chain, sync to any other), Cost Efficiency (single transaction broadcasts to multiple destinations), Speed (proof generation typically completes in 3-4 seconds), Security (cryptographic proofs ensure data integrity)

Implementation Guide

Step 1: Origin Contract - Event Emission

The source contract emits structured events that can be validated across chains:

State Sync Contract - setValue Function
function setValue(string calldata key, bytes calldata value) external {
bytes32 hashedKey = keccak256(abi.encodePacked(msg.sender, key));

// Authorization check - only original setter can update
if (keyOwners[hashedKey] != address(0)) {
require(
keyOwners[hashedKey] == msg.sender,
"Not authorized to update this key"
);
} else {
keyOwners[hashedKey] = msg.sender;
}

// Update state
store[hashedKey] = value;
uint256 currentNonce = nonces[msg.sender]++;
uint256 newVersion = keyVersions[hashedKey] + 1;
keyVersions[hashedKey] = newVersion;

// Emit structured event for cross-chain synchronization
emit ValueSet(
msg.sender, // indexed - sender address
key, // not indexed - the key string
value, // not indexed - the value bytes
currentNonce, // not indexed - replay protection
hashedKey, // indexed - for efficient filtering
newVersion // not indexed - version control
);
}

Step 2: Proof Generation

Request proofs from the Polymer API:

Polymer API Proof Request
import axios from 'axios';

class ProofGenerator {
constructor(apiUrl, apiKey) {
this.apiUrl = apiUrl;
this.apiKey = apiKey;
}

async requestProof(eventParams) {
try {
// Request proof generation from Polymer API
const proofRequest = await axios.post(
this.apiUrl,
{
jsonrpc: "2.0",
id: 1,
method: "polymer_requestProof",
params: [{
srcChainId: eventParams.srcChainId,
srcBlockNumber: eventParams.srcBlockNumber,
globalLogIndex: eventParams.globalLogIndex
}]
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
);

const jobId = proofRequest.data.result;
console.log(`Proof job created: ${jobId}`);

return jobId;

} catch (error) {
console.error("Proof request failed:", error.message);
throw error;
}
}
}

// Usage example
const proofGenerator = new ProofGenerator(
"https://proof.testnet.polymer.zone",
process.env.POLYMER_API_KEY
);

const jobId = await proofGenerator.requestProof({
srcChainId: 11155420, // Optimism Sepolia
srcBlockNumber: 26421705, // Block containing the event
globalLogIndex: 15 // Position of event in block
});

Step 3: Cross-Chain Execution

Once the proof is generated, execute on destination chains:

Destination Contract - setValueFromSource Function
function setValueFromSource(bytes calldata proof) external {
// Step 1: Validate and decode the proof using Polymer's prover
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);

// Step 2: Security validations
require(trustedChains[sourceChainId], "Untrusted source chain");
require(trustedContracts[sourceChainId][sourceContract], "Untrusted contract");

// Step 3: Parse concatenated topics into individual values
bytes32[] memory topicsArray = new bytes32[](3);
require(topics.length >= 96, "Invalid topics length");

assembly {
let topicsPtr := add(topics, 32)
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
mstore(
add(add(topicsArray, 32), mul(i, 32)),
mload(add(topicsPtr, mul(i, 32)))
)
}
}

// Step 4: Verify event signature for security
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");

// Step 5: Extract indexed parameters
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];

// Step 6: Decode non-indexed data
(
, // skip key (using hashedKey from topics)
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(unindexedData, (string, bytes, uint256, uint256));

// Step 7: Replay protection
bytes32 proofHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedProofHashes[proofHash], "Proof already used");
usedProofHashes[proofHash] = true;

// Step 8: Version control
require(version > keyVersions[hashedKey], "Version must be newer");
keyVersions[hashedKey] = version;

// Step 9: Update state
store[hashedKey] = value;
if (keyOwners[hashedKey] == address(0)) {
keyOwners[hashedKey] = sender;
}

emit ValueUpdated(hashedKey, value, version);
}

Live Demo

Demo Flow:

  1. Transaction Sent - User sets a value on the origin chain
  2. Event Detection - Relayer detects the ValueSet event
  3. Parallel Proof Requests - Relayer requests proofs for multiple destination chains simultaneously
  4. Fast Execution - After ~10 seconds, value is synchronized across all chains
info

Polling Configuration: The demo uses 10-second intervals with 5-second retries, but production applications can poll every 500ms for faster synchronization.

Advanced Use Cases

Broadcast Architecture

The Polymer approach enables a powerful broadcast model:

📡 Traditional Messaging

Limitations: Predefined source-destination pairs, separate transactions for each destination, configuration overhead



Cost: N transactions for N destinations

🌟 Polymer Broadcast

Benefits: One event proves everywhere, no predefined pairs, post-hoc destination selection



Cost: 1 transaction + proof generation

Real-World Applications

Cross-Chain Lending

Sync lending markets or yield rates across multiple chains with synchronized:

  • Interest rates updated from rate oracles
  • Risk parameters adjusted by governance
  • Liquidation events propagated instantly
DeFi Rate Synchronization Example
event RateUpdate(
address indexed market,
uint256 borrowRate,
uint256 supplyRate,
uint256 timestamp,
bytes32 indexed rateHash
);

function updateRatesFromSource(bytes calldata proof) external {
// Validate proof and extract rate data
(uint32 sourceChainId, address sourceContract, bytes memory topics, bytes memory data) =
polymerProver.validateEvent(proof);

// Decode and update local rates
(address market, uint256 borrowRate, uint256 supplyRate, uint256 timestamp) =
abi.decode(data, (address, uint256, uint256, uint256));

markets[market].borrowRate = borrowRate;
markets[market].supplyRate = supplyRate;
markets[market].lastUpdate = timestamp;
}

Performance & Economics

Speed Comparison

MethodProof GenerationCross-Chain LatencyTotal Time
Polymer Proving3-4 seconds~4 seconds~8 seconds
Traditional MessagingN/A1-2 minutes1-2 minutes
Native BridgesN/A1-7 days1-7 days

Cost Analysis (Example)

📨 Traditional Messaging

5-Chain Deployment Cost:
• Source transactions: 5 × $2 = $10
• Destination fees: 5 × $15 = $75



Total: $85 per update
❌ Expensive & Complex

🚀 Polymer Broadcast

5-Chain Deployment Cost:
• Source transaction: 1 × $2 = $2
• Destination executions: 5 × $1 = $5



Total: $7 per update
✅ 91% savings!