Skip to main content

Synchronous Cross-Chain Composability

Pylon appchains already support synchronous reads from their settlement layer: your contracts can call settlement layer view functions and use the results in the same transaction. The full concept and roadmap live in Synchronous Composability—this section focuses on how to use it in practice. Today:
  • ✅ Read settlement layer state atomically (no async polling or bridges)
  • ✅ Use forwarding proxies or the SettlementPort directly for developer ergonomics
Coming soon:
  • 🔄 Synchronous writes and broader multi-chain coordination (see the roadmap section)
To integrate reads in your appchain:
  • Follow the Quickstart for a step-by-step proxy deployment example
  • Use forwarding proxies or call SettlementPort directly (see examples below)
  • Lean on pre-deployed utility contracts for deterministic addresses and simplified setup
For architectural details, see SettlementPort, forwarding proxies, and the coordinator overview in the Architecture page.

Standard Ethereum RPC Compatibility

Pylon appchains expose full Ethereum JSON-RPC 2.0 compatible endpoints. The RPC endpoint received during chain setup functions identically to any other Ethereum-compatible chain. What this means:
  • Standard RPC methods (eth_sendRawTransaction, eth_getTransactionReceipt, eth_call, etc.) work as expected
  • All Ethereum development tools are compatible (Foundry, Hardhat, ethers.js, web3.js, etc.)
  • Wallets connect using standard network configuration
  • No special integrations or custom SDKs required
The RPC endpoint uses the same protocol and response formats as Ethereum mainnet, Base, Arbitrum, and other EVM chains.

Deploying Contracts

Deploy contracts using forge create. Configure your RPC endpoint in foundry.toml:
[rpc_endpoints]
pylon = "https://pylon.{settlement-layer}.spire.dev/v1/chain/{chain-id}/rpc"
Then deploy:
forge create MyContract --rpc-url pylon --private-key $PRIVATE_KEY --broadcast
For complex deployments with constructor arguments or multi-step workflows, use forge script.

Sending Transactions

Send transactions using standard methods and libraries. Examples below show the same operation using different tools: Foundry’s cast send, ethers.js, and web3.js.
cast send {CONTRACT_ADDRESS} \
    "functionName(uint256)" \
    123 \
    --rpc-url pylon \
    --private-key $PRIVATE_KEY \
    --broadcast

Interacting with Contracts

Call contract functions using standard RPC methods.

View Calls

Read contract state with cast call:
cast call {CONTRACT_ADDRESS} \
    "functionName()(uint256)" \
    --rpc-url pylon

State-Changing Calls

Call functions that modify state using cast send:
cast send {CONTRACT_ADDRESS} \
    "setValue(uint256)" \
    456 \
    --rpc-url pylon \
    --private-key $PRIVATE_KEY \
    --broadcast

Testing Your Contracts

The simplest-pylon-demo repository provides working examples of contracts and unit tests that demonstrate cross-chain calling patterns on Pylon.

Testing Patterns

Unit tests for contracts that interact with the settlement layer should mock the cross-chain infrastructure. Two mocking approaches are available:
  • Mock the SettlementForwardingProxy - Implement the same interface your application uses (e.g., IERC20) with setters to configure mock responses. This is the standard contract mocking pattern.
  • Mock the SettlementPort - Mock the SettlementPort contract to test the full proxy code path. The repository demonstrates this approach.
Follow standard Solidity mocking best practices—implement the interface with setters to configure mock behavior. Run tests using forge test:
forge test -vvv

Debugging Transactions

When transactions fail or the RPC rejects them during simulation, use these debugging techniques.

Simulate Calls to Inspect Errors

The RPC often rejects transactions that would fail during simulation. Use cast call to see the error that would occur:
cast call {CONTRACT_ADDRESS} \
    "functionName(params)(returnType)" \
    {ARGUMENTS} \
    --rpc-url pylon
This executes the call as a view function and returns either the result or the error message. This helps identify issues before submitting transactions. For cross-chain reads, test the settlement port directly (using cast calldata to encode function calls):
cast call 0x0000000000000000000000000000000000000042 \
    "readSettlement(address,bytes)(string)" \
    {TARGET_CONTRACT} \
    "$(cast calldata 'functionName()')" \
    --rpc-url pylon
If this returns an error, the issue is with the cross-chain call itself rather than contract logic.

Debug Failed Transactions with Cast Run

When a transaction fails after submission, use cast run to trace execution and identify the failure point:
cast run {TX_HASH} \
    --rpc-url pylon \
    -vvv
The verbose flags (-vvv) show detailed execution traces, including:
  • Opcode-level execution
  • Storage changes
  • Revert reasons
This is especially useful for debugging cross-chain calls that revert without clear error messages.

Verify Settlement Layer Connectivity

If cross-chain calls consistently fail, verify the coordinator can reach the settlement layer:
cast call 0x0000000000000000000000000000000000000042 \
    "readSettlement(address,bytes)" \
    {KNOWN_CONTRACT} \
    "$(cast calldata 'name()')" \
    --rpc-url pylon
Use a known contract address from the settlement layer to test connectivity. If this fails, the coordinator may be down or the settlement layer unreachable.

Inspect Transaction Receipts

For successfully submitted transactions, inspect the receipt for execution details using cast receipt:
cast receipt {TX_HASH} --rpc-url pylon
Check logs for cross-chain call execution events and verify the transaction actually executed as expected.

Direct Settlement Layer Reads

You can call the SettlementPort directly using the l2Read function to read from settlement layer contracts. This provides more control over calldata encoding and error handling than forwarding proxies.

Basic Usage

The l2Read function is available through the SettlementPort contract at address 0x0000000000000000000000000000000000000042:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;

interface IL2Reader {
    function l2Read(address target, bytes calldata data) external view returns (bytes memory);
}

contract MyAppContract {
    address public immutable settlementPort = 0x0000000000000000000000000000000000000042;

    function getL2TokenBalance(address tokenContract, address account) external view returns (uint256) {
        // Encode the balanceOf function call
        bytes memory data = abi.encodeWithSignature("balanceOf(address)", account);

        // Perform the L2 read
        bytes memory result = IL2Reader(settlementPort).l2Read(tokenContract, data);

        // Decode the result
        return abi.decode(result, (uint256));
    }
}

Key Points

  • View Function: l2Read is a view function, so it doesn’t modify state
  • Synchronous: The read appears synchronous to your contract, but the data is pre-fetched by the sequencer
  • Error Handling: The function will revert if the L2 call fails—use try/catch for graceful error handling

Error Handling

For graceful error handling when L2 reads might fail:
function processL2Data(address l2Contract) external view returns (bool) {
    try IL2Reader(settlementPort).l2Read(l2Contract, abi.encodeWithSignature("getStatus()")) returns (bytes memory result) {
        bool status = abi.decode(result, (bool));
        return status;
    } catch {
        // Handle L2 read failure
        return false;
    }
}

Pre-Deployed Utility Contracts

Pylon can pre-deploy utility contracts at deterministic addresses on your appchain. These contracts enable common development patterns without requiring deployment. Request these contracts or others during chain setup or through the Spire contact form.

Available Contracts

The following utility contracts can be pre-deployed on request:
ContractDefault AddressPurpose
SettlementForwardingProxyFactory0x0000000000000000000000000000000000000043Deploy proxies for cross-chain reads
CREATE2Factory0x0000000000000000000000000000000000000044Deterministic contract deployments
Multicall0x0000000000000000000000000000000000000045Batch multiple calls in a single transaction
MinimalProxyFactory0x0000000000000000000000000000000000000046Deploy minimal proxies (EIP-1167) for cheap cloning
You can request these contracts to be pre-deployed at the default addresses listed above, or specify custom addresses for your appchain. If you don’t request pre-deployment, you can deploy these contracts yourself at any address.

SettlementForwardingProxyFactory

The SettlementForwardingProxyFactory deploys forwarding proxies that enable clean interface-based access to settlement layer contracts. Each proxy forwards calls to a specified settlement layer contract through the SettlementPort. Interface:
interface ISettlementForwardingProxyFactory {
    function createProxy(address settlementAddress) external returns (address proxy);
    function computeProxyAddress(address settlementAddress, bytes32 salt) external view returns (address);
}
The factory uses CREATE2 internally, enabling deterministic proxy addresses. Use computeProxyAddress to calculate the proxy address before deployment. For complete examples, see the simplest-pylon-demo repository which demonstrates proxy deployment and usage patterns.

CREATE2Factory

Enables deterministic contract deployments using the CREATE2 opcode. Contracts can be deployed to addresses computed in advance based on the deployer address, salt, and bytecode hash. Interface:
interface ICREATE2Factory {
    function deploy(bytes32 salt, bytes memory bytecode) external returns (address);
    function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address);
}
Use computeAddress to calculate the deployment address before calling deploy. The factory uses CREATE2 internally, ensuring that contracts with the same salt and bytecode always deploy to the same address.

Multicall

Batches multiple function calls into a single transaction, reducing gas costs by combining operations. Useful for aggregating multiple cross-chain reads or state queries in a single transaction. Interface:
interface IMulticall {
    struct Call {
        address target;
        bytes callData;
    }
    
    function aggregate(Call[] memory calls) external returns (uint256 blockNumber, bytes[] memory returnData);
    function tryAggregate(bool requireSuccess, Call[] memory calls) external returns (uint256 blockNumber, Result[] memory returnData);
}

struct Result {
    bool success;
    bytes returnData;
}
Usage: Call multiple contract functions in a single transaction. The aggregate function reverts if any call fails, while tryAggregate allows partial failures when requireSuccess is false.
// Example: Batch multiple cross-chain reads
IMulticall.Call[] memory calls = new IMulticall.Call[](2);
calls[0] = IMulticall.Call({
    target: settlementProxy1,
    callData: abi.encodeWithSignature("balanceOf(address)", user)
});
calls[1] = IMulticall.Call({
    target: settlementProxy2,
    callData: abi.encodeWithSignature("totalSupply()")
});

(, bytes[] memory results) = IMulticall(MULTICALL_ADDRESS).aggregate(calls);
uint256 balance = abi.decode(results[0], (uint256));
uint256 supply = abi.decode(results[1], (uint256));

MinimalProxyFactory

Deploys EIP-1167 minimal proxies that forward calls to an implementation contract via delegatecall. Each proxy maintains its own storage while sharing the implementation’s code, enabling cheap deployment of many instances. See EIP-1167 and OpenZeppelin Clones for details.

Next Steps