Bridge-and-Call Guide
Bridge-and-Call Guide
Complete guide to atomic asset transfer with contract execution using AggSandbox.
Bridge-and-call is an atomic operation that combines asset bridging with contract execution in a single transaction. It transfers tokens to a contract on the destination network and immediately executes a function call with those tokens, enabling complex cross-chain interactions like DeFi operations, NFT minting, or automated workflows.
Overview
Bridge-and-call in Agglayer follows a dual-bridge model:
- Source Network: Creates both asset and message bridges atomically
- Exit Root Update:
- If L1 source: Direct Global Exit Root update on L1
- If L2 source: Local Exit Root update, then AggKit updates GER on L1
- Bridge Service: AggKit syncs GER to destination network
- Destination Network: Asset bridge transfers tokens, message bridge executes function
- Claim Process: User claims both bridges independently (asset first, then message)
Bridge-and-Call Process
Bridge-and-call follows the same process for all network combinations. Use these steps for any LY (destination) bridge operation.
Step 1: Bridge-and-Call
Execute atomic bridge-and-call from source network (LY):
# Bridge-and-call from $LX to $LY
aggsandbox bridge bridge-and-call \
--network-id $LX \
--destination-network-id $LY \
--amount $AMOUNT \
--token-address $TOKEN_ADDRESS \
--to-address $CONTRACT_ADDRESS \
--call-data $CALL_DATA \
--private-key $PRIVATE_KEYNetwork ID Reference:
- L1 (Ethereum):
--network-id 0- The settlement layer that coordinates both asset transfers and contract execution in bridge-and-call operations - L2-1 (zkEVM):
--network-id 1- Primary Layer 2 network supporting atomic bridge-and-call operations with full EVM contract execution capabilities - L2-2 (Additional):
--network-id 2- Secondary Layer 2 network available in multi-L2 mode for testing complex atomic operations between different L2 networks
Variable Reference:
$LX= Source network ID (0, 1, or 2)$LY= Destination network ID (0, 1, or 2)
Step 2: Monitor Bridge Transaction
# Check bridge status on source network (creates 2 bridges)
aggsandbox show bridges --network-id $LXYou'll see two bridges with consecutive deposit counts:
- Asset Bridge:
deposit_count: N,leaf_type: 0(asset) - The first bridge operation that transfers tokens to the destination network, creating the foundation for the subsequent contract execution - Message Bridge:
deposit_count: N+1,leaf_type: 1(message) - The second bridge operation that executes the contract function using the transferred tokens, completing the atomic bridge-and-call sequence
Step 3: Prepare Call Data
# Example: Encode function call that JumpPoint will execute on your contract
TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
AMOUNT="1000000000000000000"
MESSAGE="Bridge-and-call from $LX!"
CALL_DATA=$(cast calldata "receiveTokensWithMessage(address,uint256,string)" \
$TOKEN_ADDRESS $AMOUNT "$MESSAGE")
echo "Call data: $CALL_DATA"
# Alternative: Any function on your target contract
# CALL_DATA=$(cast calldata "processPayment(uint256,bytes32)" $AMOUNT 0x1234...)Important: The --call-data is the actual function call that the JumpPoint contract will execute on your target contract. This should be a complete function call with signature and parameters.
Step 4: Check if Claimable
First, manually check the destination network claims API:
# Wait for AggKit sync, then check destination claims
# L1<->L2: ~20-25 seconds
# L2<->L2: ~45-60 seconds
aggsandbox show claims --network-id $LYLook for both bridge transactions in the output:
[
{
"bridge_tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c",
"claim_tx_hash": "",
"origin_network": $LX,
"destination_network": $LY,
"amount": "1000000000000000000",
"status": "pending",
"type": "asset"
},
{
"bridge_tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c",
"claim_tx_hash": "",
"origin_network": $LX,
"destination_network": $LY,
"amount": "0",
"status": "pending",
"type": "message"
},
]What to look for:
- Both bridges share the same
bridge_tx_hash - Asset bridge:
type: "asset", amount > 0 - Message bridge:
type: "message", amount = 0 - Both have status
"pending"(ready to claim)
For automated scripts, use this pattern:
BRIDGE_TX="0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c"
DESTINATION_NETWORK=$LY
echo "Checking if bridge-and-call bridges are claimable on network $DESTINATION_NETWORK..."
# Check for asset bridge
ASSET_CLAIMABLE=$(aggsandbox show claims --network-id $DESTINATION_NETWORK --json | \
jq -r --arg tx "$BRIDGE_TX" '.[] | select(.bridge_tx_hash == $tx and .type == "asset" and .status == "pending") | .bridge_tx_hash')
# Check for message bridge
MESSAGE_CLAIMABLE=$(aggsandbox show claims --network-id $DESTINATION_NETWORK --json | \
jq -r --arg tx "$BRIDGE_TX" '.[] | select(.bridge_tx_hash == $tx and .type == "message" and .status == "pending") | .bridge_tx_hash')
if [ "$ASSET_CLAIMABLE" = "$BRIDGE_TX" ]; then
echo "✅ Asset bridge is claimable"
else
echo "⏳ Asset bridge not yet claimable"
fi
if [ "$MESSAGE_CLAIMABLE" = "$BRIDGE_TX" ]; then
echo "✅ Message bridge is claimable"
else
echo "⏳ Message bridge not yet claimable"
fi
# Wait if needed
if [ "$ASSET_CLAIMABLE" != "$BRIDGE_TX" ] || [ "$MESSAGE_CLAIMABLE" != "$BRIDGE_TX" ]; then
echo "Waiting for bridges to become claimable..."
# Adjust sleep time based on network combination
if [ $DESTINATION_NETWORK -eq 2 ] || [ $SOURCE_NETWORK -eq 2 ]; then
sleep 15 # L2-L2 takes longer
else
sleep 10 # L1-L2 timing
fi
# Check again
aggsandbox show claims --network-id $DESTINATION_NETWORK --json | jq --arg tx "$BRIDGE_TX" '.[] | select(.bridge_tx_hash == $tx)'
fiStep 5: Claim Asset Bridge (First)
# Claim asset bridge first (MUST be done before message bridge)
ASSET_CLAIM_TX=$(aggsandbox bridge claim \
--network-id $LY \
--tx-hash $BRIDGE_TX \
--source-network-id $LX | \
grep "claim transaction submitted:" | cut -d' ' -f4)
echo "Asset claim transaction submitted: $ASSET_CLAIM_TX"Step 6: Verify Asset Claim Completion
First, manually check the destination network claims API:
# Check destination claims to find your asset claim transaction
aggsandbox show claims --network-id $LYLook for your asset claim transaction in the output:
[
{
"bridge_tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c",
"claim_tx_hash": "0xa9fa5418144f7c8c1b78cd0e5560d6550411667ef937b554636a613f933b3d9f",
"origin_network": $LX,
"destination_network": $LY,
"amount": "1000000000000000000",
"status": "completed",
"type": "asset"
}
]What to look for:
- Your asset claim transaction hash appears as
claim_tx_hash - Status changed from
"pending"to"completed" - Type is
"asset"with amount > 0
For automated scripts, use this pattern:
ASSET_CLAIM_TX="0xa9fa5418144f7c8c1b78cd0e5560d6550411667ef937b554636a613f933b3d9f"
DESTINATION_NETWORK=$LY
echo "Monitoring asset claim status until completion..."
while true; do
ASSET_CLAIM_DATA=$(aggsandbox show claims --network-id $DESTINATION_NETWORK --json | \
jq --arg tx "$ASSET_CLAIM_TX" '.[] | select(.claim_tx_hash == $tx)')
if [ "$ASSET_CLAIM_DATA" != "null" ] && [ "$ASSET_CLAIM_DATA" != "" ]; then
ASSET_STATUS=$(echo "$ASSET_CLAIM_DATA" | jq -r '.status')
echo "$(date): Asset claim status: $ASSET_STATUS"
if [ "$ASSET_STATUS" = "completed" ]; then
echo "✅ Asset claim completed! Proceeding to message claim..."
break
elif [ "$ASSET_STATUS" = "pending" ]; then
echo "⏳ Asset claim pending, waiting 5 seconds..."
sleep 5
else
echo "❌ Unknown asset claim status: $ASSET_STATUS"
break
fi
else
echo "❌ Asset claim not found"
break
fi
doneStep 7: Claim Message Bridge (Second)
# Claim message bridge AFTER asset claim is completed
MESSAGE_CLAIM_TX=$(aggsandbox bridge claim \
--network-id $LY \
--tx-hash $BRIDGE_TX \
--source-network-id $LX | \
grep "claim transaction submitted:" | cut -d' ' -f4)
echo "Message claim transaction submitted: $MESSAGE_CLAIM_TX"Step 8: Verify Message Claim Completion
First, manually check the destination network claims API:
# Check destination claims to find your message claim transaction
aggsandbox show claims --network-id $LYLook for your message claim transaction in the output:
[
{
"bridge_tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c",
"claim_tx_hash": "0xb8e6521234f8c7d9e1f45678901234567890abcdef1234567890abcdef123456",
"origin_network": $LX,
"destination_network": $LY,
"amount": "0",
"status": "completed",
"type": "message"
}
]What to look for:
- Your message claim transaction hash appears as
claim_tx_hash - Status changed from
"pending"to"completed" - Type is
"message"with amount = 0
For automated scripts, use this pattern:
MESSAGE_CLAIM_TX="0xb8e6521234f8c7d9e1f45678901234567890abcdef1234567890abcdef123456"
DESTINATION_NETWORK=$LY
echo "Monitoring message claim status until completion..."
while true; do
MESSAGE_CLAIM_DATA=$(aggsandbox show claims --network-id $DESTINATION_NETWORK --json | \
jq --arg tx "$MESSAGE_CLAIM_TX" '.[] | select(.claim_tx_hash == $tx)')
if [ "$MESSAGE_CLAIM_DATA" != "null" ] && [ "$MESSAGE_CLAIM_DATA" != "" ]; then
MESSAGE_STATUS=$(echo "$MESSAGE_CLAIM_DATA" | jq -r '.status')
echo "$(date): Message claim status: $MESSAGE_STATUS"
if [ "$MESSAGE_STATUS" = "completed" ]; then
echo "✅ Message claim completed successfully!"
break
elif [ "$MESSAGE_STATUS" = "pending" ]; then
echo "⏳ Message claim pending, waiting 5 seconds..."
sleep 5
else
echo "❌ Unknown message claim status: $MESSAGE_STATUS"
break
fi
else
echo "❌ Message claim not found"
break
fi
doneStep 9: Verify Contract Results
# Check if contract received tokens and executed function
echo "Verifying bridge-and-call results..."
# Check contract token balance
TOKEN_BALANCE=$(cast call $TOKEN_ADDRESS \
"balanceOf(address)(uint256)" \
$CONTRACT_ADDRESS \
--rpc-url http://localhost:$PORT)
echo "Contract token balance: $TOKEN_BALANCE"
# Check if contract function was called (depends on your contract)
cast call $CONTRACT_ADDRESS \
"lastMessage()(string)" \
--rpc-url http://localhost:$PORT
echo "✅ Bridge-and-call operation completed successfully!"RPC Port Reference:
- L1:
http://localhost:8545- Primary RPC endpoint for L1 bridge-and-call operations, contract deployment, and settlement layer interactions - L2-1:
http://localhost:8546- L2 RPC endpoint for deploying target contracts and executing atomic bridge-and-call operations on the primary Layer 2 network - L2-2:
http://localhost:8547- Additional L2 RPC endpoint for multi-L2 bridge-and-call testing scenarios between different Layer 2 networks
Network Combinations
L1 ↔ L2 Bridging
# L1 → L2-1: LX=0, LY=1
# L2-1 → L1: LX=1, LY=0
# Sync time: ~20-25 secondsL2 ↔ L2 Bridging
# Start multi-L2 mode first
aggsandbox stop
aggsandbox start --multi-l2 --detach
# L2-1 → L2-2: LX=1, LY=2
# L2-2 → L2-1: LX=2, LY=1
# Sync time: ~45-60 seconds (longer for L2-L2)Bridge-and-Call with ETH
Bridge with ETH Value
# Bridge tokens and send 0.1 ETH to contract function
aggsandbox bridge bridge-and-call \
--network-id 0 \
--destination-network-id 1 \
--amount 1000000000000000000 \
--token-address $TOKEN_ADDRESS \
--to-address $CONTRACT_ADDRESS \
--call-data $CALL_DATA \
--msg-value 100000000000000000 \
--private-key $PRIVATE_KEY
# When claiming the message bridge, include the ETH value
aggsandbox bridge claim \
--network-id 1 \
--tx-hash $BRIDGE_TX \
--source-network-id 0 \
--msg-value 100000000000000000Note: Use wei values for amounts (1 ETH = 1000000000000000000 wei).
Smart Contract Integration
Bridge-and-Call Architecture
Important: Bridge-and-call uses a different architecture than regular message bridging. The BridgeExtension contract handles the onMessageReceived call and automatically deploys a JumpPoint contract to execute your function. Your target contract does NOT need to implement IBridgeMessageReceiver.
How it works:
- Bridge-and-call sends message to
BridgeExtensionon destination network - BridgeExtension receives both asset and message bridges
- BridgeExtension deploys a
JumpPointcontract with your encoded function call - JumpPoint transfers tokens to your contract and executes the function call
Bridge-and-Call Receiver Contract
Here's a complete contract that can receive bridge-and-call operations (no special interface required):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract BridgeAndCallReceiver is ReentrancyGuard {
struct Operation {
address token;
uint256 amount;
string message;
uint256 timestamp;
address sender;
}
Operation[] public operations;
mapping(address => uint256) public operationCounts;
mapping(address => uint256) public tokenBalances;
event TokensReceived(
address indexed token,
uint256 amount,
string message,
address indexed sender
);
// This function is called by the JumpPoint contract deployed by BridgeExtension
function receiveTokensWithMessage(
address token,
uint256 amount,
string calldata message
) external payable nonReentrant {
require(token != address(0), "Invalid token address");
require(amount > 0, "Amount must be positive");
// Record the operation
operations.push(Operation({
token: token,
amount: amount,
message: message,
timestamp: block.timestamp,
sender: msg.sender
}));
operationCounts[msg.sender]++;
tokenBalances[token] += amount;
emit TokensReceived(token, amount, message, msg.sender);
}
function getOperationCount() external view returns (uint256) {
return operations.length;
}
function getLastOperation() external view returns (Operation memory) {
require(operations.length > 0, "No operations");
return operations[operations.length - 1];
}
function getTokenBalance(address token) external view returns (uint256) {
return tokenBalances[token];
}
}Deploy and Test
# 1. Deploy contract on destination network
CONTRACT_ADDRESS=$(forge create src/BridgeAndCallReceiver.sol:BridgeAndCallReceiver \
--rpc-url http://localhost:8546 \
--private-key $PRIVATE_KEY \
--json | jq -r '.deployedTo')
echo "Contract deployed to: $CONTRACT_ADDRESS"
# 2. Prepare call data for the deployed contract
CALL_DATA=$(cast calldata "receiveTokensWithMessage(address,uint256,string)" \
$TOKEN_ADDRESS \
2000000000000000000 \
"Bridge-and-call test!")
# 3. Execute bridge-and-call
aggsandbox bridge bridge-and-call \
--network-id 0 \
--destination-network-id 1 \
--amount 2000000000000000000 \
--token-address $TOKEN_ADDRESS \
--to-address $CONTRACT_ADDRESS \
--call-data "$CALL_DATA" \
--private-key $PRIVATE_KEY
# 4. Wait and claim both bridges (asset first, then message)
sleep 30
aggsandbox bridge claim --network-id 1 --tx-hash $BRIDGE_TX --source-network-id 0
aggsandbox bridge claim --network-id 1 --tx-hash $BRIDGE_TX --source-network-id 0
# 5. Verify results
cast call $CONTRACT_ADDRESS "getOperationCount()(uint256)" --rpc-url http://localhost:8546
cast call $CONTRACT_ADDRESS "getLastOperation()(address,uint256,string,uint256,address)" --rpc-url http://localhost:8546
cast call $CONTRACT_ADDRESS "getTokenBalance(address)(uint256)" $TOKEN_ADDRESS --rpc-url http://localhost:8546Troubleshooting
Common Issues
Bridge Transaction Fails
# Check account balance and token allowance
cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $YOUR_ADDRESS --rpc-url http://localhost:8545
cast call $TOKEN_ADDRESS "allowance(address,address)(uint256)" $YOUR_ADDRESS $BRIDGE_ADDRESS --rpc-url http://localhost:8545
# Check network status
aggsandbox statusClaim Order Error
# Verify you claimed asset bridge first
aggsandbox show claims --network-id $LY --json | jq '.[] | select(.type == "asset")'
# Check if message claim failed due to missing tokens
aggsandbox show claims --network-id $LY --json | jq '.[] | select(.type == "message")'Contract Function Not Executed
# Check if contract received tokens
cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $CONTRACT_ADDRESS --rpc-url http://localhost:8546
# Test function call locally
cast call $CONTRACT_ADDRESS \
--data "$CALL_DATA" \
--rpc-url http://localhost:8546
# Check transaction receipt
cast receipt $MESSAGE_CLAIM_TX --rpc-url http://localhost:8546Last updated on