Contract Calls
Cross-chain contract calls enable smart contracts on one blockchain to execute functions on contracts deployed on different blockchains, creating truly interoperable decentralized applications.
Overview
Contract calls extend beyond simple token transfers by allowing arbitrary function execution across chains. This enables complex multi-chain workflows, cross-chain governance, synchronized state updates, and distributed application logic.
Architecture
Implementation Patterns
1. Simple Function Call
Execute a function on another chain without parameters:
contract CrossChainNotifier {
IIXFIGateway public gateway;
function notifyOnChain(string memory targetChain, address targetContract) external {
bytes memory payload = abi.encodeWithSignature("notify()");
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
);
}
}
contract NotificationReceiver is IXFIExecutable {
event NotificationReceived(string sourceChain, address sourceContract);
function execute(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
bytes memory payload
) external override onlyGateway {
// Decode the function selector
bytes4 selector = bytes4(payload);
if (selector == this.notify.selector) {
notify();
emit NotificationReceived(sourceChain, _parseAddress(sourceAddress));
}
}
function notify() public {
// Notification logic here
}
}
2. Parameterized Function Call
Execute functions with complex parameters:
contract CrossChainDataSync {
struct UserData {
uint256 score;
uint256 level;
bool isActive;
}
function syncUserData(
string memory targetChain,
address targetContract,
address user,
UserData memory data
) external {
bytes memory payload = abi.encodeWithSignature(
"updateUserData(address,uint256,uint256,bool)",
user,
data.score,
data.level,
data.isActive
);
gateway.callContract(targetChain, targetContract, payload);
}
}
contract DataReceiver is IXFIExecutable {
mapping(address => UserData) public userData;
function execute(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
bytes memory payload
) external override onlyGateway {
// Execute the function call
(bool success, bytes memory returnData) = address(this).call(payload);
require(success, "Function execution failed");
}
function updateUserData(
address user,
uint256 score,
uint256 level,
bool isActive
) external {
userData[user] = UserData(score, level, isActive);
emit UserDataUpdated(user, score, level, isActive);
}
}
3. Conditional Execution
Execute functions based on on-chain conditions:
contract ConditionalExecutor is IXFIExecutable {
mapping(bytes32 => bool) public conditions;
function setCondition(bytes32 conditionId, bool value) external onlyOwner {
conditions[conditionId] = value;
}
function execute(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
bytes memory payload
) external override onlyGateway {
// Decode condition and actual payload
(bytes32 conditionId, bytes memory actualPayload) = abi.decode(
payload,
(bytes32, bytes)
);
// Check condition
require(conditions[conditionId], "Condition not met");
// Execute conditional function
(bool success,) = address(this).call(actualPayload);
require(success, "Conditional execution failed");
}
function conditionalFunction(uint256 value) external {
// Function logic here
}
}
4. Multi-Step Workflow
Chain multiple function calls across different contracts:
contract WorkflowOrchestrator {
struct WorkflowStep {
string targetChain;
address targetContract;
bytes payload;
uint256 delay;
}
mapping(bytes32 => WorkflowStep[]) public workflows;
mapping(bytes32 => uint256) public currentStep;
function startWorkflow(bytes32 workflowId) external {
require(workflows[workflowId].length > 0, "Workflow not found");
WorkflowStep memory step = workflows[workflowId][0];
currentStep[workflowId] = 0;
gateway.callContract(
step.targetChain,
Strings.toHexString(uint160(step.targetContract), 20),
abi.encode(workflowId, step.payload)
);
}
function continueWorkflow(bytes32 workflowId) external {
uint256 nextStepIndex = currentStep[workflowId] + 1;
require(nextStepIndex < workflows[workflowId].length, "Workflow complete");
WorkflowStep memory step = workflows[workflowId][nextStepIndex];
currentStep[workflowId] = nextStepIndex;
if (step.delay > 0) {
// Schedule delayed execution
_scheduleExecution(workflowId, step, block.timestamp + step.delay);
} else {
gateway.callContract(
step.targetChain,
Strings.toHexString(uint160(step.targetContract), 20),
abi.encode(workflowId, step.payload)
);
}
}
}
Advanced Patterns
Callback Mechanism
Implement callbacks for bidirectional communication:
contract CallbackContract is IXFIExecutable {
struct PendingCallback {
string sourceChain;
address sourceContract;
bytes callbackData;
uint256 timestamp;
}
mapping(bytes32 => PendingCallback) public pendingCallbacks;
function callWithCallback(
string memory targetChain,
address targetContract,
bytes memory payload,
bytes memory callbackData
) external returns (bytes32 callbackId) {
callbackId = keccak256(abi.encode(
msg.sender,
targetContract,
payload,
block.timestamp,
block.number
));
pendingCallbacks[callbackId] = PendingCallback({
sourceChain: "current_chain",
sourceContract: msg.sender,
callbackData: callbackData,
timestamp: block.timestamp
});
bytes memory wrappedPayload = abi.encode(callbackId, payload);
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
wrappedPayload
);
}
function executeCallback(bytes32 callbackId, bytes memory result) external {
PendingCallback memory callback = pendingCallbacks[callbackId];
require(callback.timestamp > 0, "Callback not found");
delete pendingCallbacks[callbackId];
// Execute callback on original contract
bytes memory callbackPayload = abi.encodeWithSignature(
"handleCallback(bytes32,bytes,bytes)",
callbackId,
callback.callbackData,
result
);
gateway.callContract(
callback.sourceChain,
Strings.toHexString(uint160(callback.sourceContract), 20),
callbackPayload
);
}
}
State Synchronization
Keep state synchronized across multiple chains:
contract MultiChainState is IXFIExecutable {
mapping(bytes32 => uint256) public state;
mapping(string => bool) public supportedChains;
event StateUpdated(bytes32 indexed key, uint256 value, string sourceChain);
function updateState(bytes32 key, uint256 value) external {
state[key] = value;
// Broadcast to all supported chains
_broadcastStateUpdate(key, value);
emit StateUpdated(key, value, "local");
}
function _broadcastStateUpdate(bytes32 key, uint256 value) internal {
bytes memory payload = abi.encodeWithSignature(
"syncState(bytes32,uint256)",
key,
value
);
// Broadcast to all supported chains
for (uint256 i = 0; i < chainNames.length; i++) {
if (supportedChains[chainNames[i]]) {
gateway.callContract(
chainNames[i],
Strings.toHexString(uint160(address(this)), 20),
payload
);
}
}
}
function execute(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
bytes memory payload
) external override onlyGateway {
// Only accept calls from self on other chains
require(
_parseAddress(sourceAddress) == address(this),
"Invalid source contract"
);
(bool success,) = address(this).call(payload);
require(success, "State sync failed");
}
function syncState(bytes32 key, uint256 value) external {
state[key] = value;
emit StateUpdated(key, value, "remote");
}
}
Cross-Chain Governance
Implement governance decisions across multiple chains:
contract CrossChainGovernance is IXFIExecutable {
struct Proposal {
string description;
mapping(string => bool) chainApprovals;
mapping(string => uint256) votes;
uint256 requiredChains;
uint256 approvedChains;
bool executed;
uint256 deadline;
}
mapping(uint256 => Proposal) public proposals;
mapping(string => uint256) public chainWeights;
uint256 public nextProposalId;
function createProposal(
string memory description,
string[] memory targetChains,
uint256 votingPeriod
) external returns (uint256 proposalId) {
proposalId = nextProposalId++;
Proposal storage proposal = proposals[proposalId];
proposal.description = description;
proposal.requiredChains = targetChains.length;
proposal.deadline = block.timestamp + votingPeriod;
// Notify all target chains about the proposal
bytes memory payload = abi.encodeWithSignature(
"notifyProposal(uint256,string,uint256)",
proposalId,
description,
proposal.deadline
);
for (uint256 i = 0; i < targetChains.length; i++) {
gateway.callContract(
targetChains[i],
Strings.toHexString(uint160(address(this)), 20),
payload
);
}
}
function voteOnProposal(uint256 proposalId, bool support) external {
// Local voting logic
_processVote(proposalId, "local", support, msg.sender);
// Broadcast vote to other chains
bytes memory payload = abi.encodeWithSignature(
"receiveVote(uint256,bool,address,string)",
proposalId,
support,
msg.sender,
"local"
);
// Broadcast to all chains
_broadcastToAllChains(payload);
}
function executeProposal(uint256 proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(!proposal.executed, "Already executed");
require(proposal.approvedChains >= proposal.requiredChains, "Insufficient approvals");
proposal.executed = true;
// Execute proposal actions across chains
_executeProposalActions(proposalId);
}
}
Error Handling
Robust Error Recovery
contract RobustCaller is IXFIExecutable {
struct FailedCall {
string targetChain;
address targetContract;
bytes payload;
uint256 attempts;
uint256 lastAttempt;
string lastError;
}
mapping(bytes32 => FailedCall) public failedCalls;
uint256 public constant MAX_RETRY_ATTEMPTS = 3;
uint256 public constant RETRY_DELAY = 1 hours;
function makeRobustCall(
string memory targetChain,
address targetContract,
bytes memory payload
) external returns (bytes32 callId) {
callId = keccak256(abi.encode(
msg.sender,
targetChain,
targetContract,
payload,
block.timestamp
));
try gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
) {
emit CallSucceeded(callId, targetChain, targetContract);
} catch Error(string memory reason) {
_handleCallFailure(callId, targetChain, targetContract, payload, reason);
} catch {
_handleCallFailure(callId, targetChain, targetContract, payload, "Unknown error");
}
}
function retryFailedCall(bytes32 callId) external {
FailedCall storage failed = failedCalls[callId];
require(failed.attempts > 0, "Call not found");
require(failed.attempts < MAX_RETRY_ATTEMPTS, "Max attempts reached");
require(block.timestamp >= failed.lastAttempt + RETRY_DELAY, "Too early to retry");
failed.attempts++;
failed.lastAttempt = block.timestamp;
try gateway.callContract(
failed.targetChain,
Strings.toHexString(uint160(failed.targetContract), 20),
failed.payload
) {
delete failedCalls[callId];
emit CallRetrySucceeded(callId);
} catch Error(string memory reason) {
failed.lastError = reason;
emit CallRetryFailed(callId, reason);
}
}
function _handleCallFailure(
bytes32 callId,
string memory targetChain,
address targetContract,
bytes memory payload,
string memory error
) internal {
failedCalls[callId] = FailedCall({
targetChain: targetChain,
targetContract: targetContract,
payload: payload,
attempts: 1,
lastAttempt: block.timestamp,
lastError: error
});
emit CallFailed(callId, targetChain, targetContract, error);
}
}
Circuit Breaker Pattern
contract CircuitBreakerCaller {
enum CircuitState { CLOSED, OPEN, HALF_OPEN }
struct CircuitBreaker {
CircuitState state;
uint256 failureCount;
uint256 lastFailureTime;
uint256 nextAttemptTime;
}
mapping(string => CircuitBreaker) public circuits;
uint256 public constant FAILURE_THRESHOLD = 5;
uint256 public constant TIMEOUT = 5 minutes;
uint256 public constant RETRY_TIMEOUT = 1 hours;
function callWithCircuitBreaker(
string memory targetChain,
address targetContract,
bytes memory payload
) external {
CircuitBreaker storage circuit = circuits[targetChain];
if (circuit.state == CircuitState.OPEN) {
require(block.timestamp >= circuit.nextAttemptTime, "Circuit breaker open");
circuit.state = CircuitState.HALF_OPEN;
}
try gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
) {
_handleSuccess(targetChain);
} catch {
_handleFailure(targetChain);
}
}
function _handleSuccess(string memory targetChain) internal {
CircuitBreaker storage circuit = circuits[targetChain];
circuit.state = CircuitState.CLOSED;
circuit.failureCount = 0;
}
function _handleFailure(string memory targetChain) internal {
CircuitBreaker storage circuit = circuits[targetChain];
circuit.failureCount++;
circuit.lastFailureTime = block.timestamp;
if (circuit.failureCount >= FAILURE_THRESHOLD) {
circuit.state = CircuitState.OPEN;
circuit.nextAttemptTime = block.timestamp + RETRY_TIMEOUT;
}
}
}
Security Considerations
Input Validation
contract SecureContractCaller {
mapping(address => bool) public authorizedCallers;
mapping(string => mapping(address => bool)) public trustedContracts;
modifier onlyAuthorized() {
require(authorizedCallers[msg.sender], "Unauthorized caller");
_;
}
modifier onlyTrustedTarget(string memory chain, address target) {
require(trustedContracts[chain][target], "Untrusted target contract");
_;
}
function secureCall(
string memory targetChain,
address targetContract,
bytes memory payload
) external onlyAuthorized onlyTrustedTarget(targetChain, targetContract) {
// Validate payload
require(payload.length > 0, "Empty payload");
require(payload.length <= 8192, "Payload too large");
// Validate function selector
bytes4 selector = bytes4(payload);
require(_isAllowedSelector(targetContract, selector), "Function not allowed");
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
);
}
function _isAllowedSelector(address target, bytes4 selector) internal view returns (bool) {
// Implement allowlist logic
return true; // Simplified for example
}
}
Reentrancy Protection
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract ReentrancyProtectedCaller is ReentrancyGuard, IXFIExecutable {
function safeCall(
string memory targetChain,
address targetContract,
bytes memory payload
) external nonReentrant {
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
);
}
function execute(
bytes32 commandId,
string memory sourceChain,
string memory sourceAddress,
bytes memory payload
) external override onlyGateway nonReentrant {
(bool success,) = address(this).call(payload);
require(success, "Execution failed");
}
}
Gas Optimization
Batch Contract Calls
contract BatchCaller {
struct Call {
string targetChain;
address targetContract;
bytes payload;
}
function batchCall(Call[] memory calls) external {
require(calls.length <= 10, "Too many calls");
for (uint256 i = 0; i < calls.length; i++) {
gateway.callContract(
calls[i].targetChain,
Strings.toHexString(uint160(calls[i].targetContract), 20),
calls[i].payload
);
}
}
function encodedBatchCall(bytes memory encodedCalls) external {
Call[] memory calls = abi.decode(encodedCalls, (Call[]));
batchCall(calls);
}
}
Optimized Payload Encoding
contract OptimizedCaller {
function compactCall(
string memory targetChain,
address targetContract,
bytes4 selector,
bytes memory data
) external {
bytes memory payload = abi.encodePacked(selector, data);
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
);
}
function multiCall(
string memory targetChain,
address targetContract,
bytes4[] memory selectors,
bytes[] memory dataArray
) external {
require(selectors.length == dataArray.length, "Array length mismatch");
bytes memory payload = abi.encodeWithSignature("multiCall(bytes4[],bytes[])", selectors, dataArray);
gateway.callContract(
targetChain,
Strings.toHexString(uint160(targetContract), 20),
payload
);
}
}
Testing
Mock Contracts for Testing
contract MockGateway is IIXFIGateway {
event MockContractCall(string destinationChain, string contractAddress, bytes payload);
function callContract(
string memory destinationChain,
string memory contractAddress,
bytes memory payload
) external override {
emit MockContractCall(destinationChain, contractAddress, payload);
// Simulate execution on destination
_simulateExecution(contractAddress, payload);
}
function _simulateExecution(string memory contractAddress, bytes memory payload) internal {
// Mock execution logic for testing
address target = _parseAddress(contractAddress);
if (target != address(0)) {
try IXFIExecutable(target).execute(
keccak256("mock"),
"source_chain",
"0x1234567890123456789012345678901234567890",
payload
) {
// Success
} catch {
// Handle failure
}
}
}
}
Integration Tests
describe("Cross-Chain Contract Calls", function() {
let caller, receiver, gateway;
beforeEach(async function() {
// Deploy contracts
const Gateway = await ethers.getContractFactory("MockGateway");
gateway = await Gateway.deploy();
const Caller = await ethers.getContractFactory("CrossChainCaller");
caller = await Caller.deploy(gateway.address);
const Receiver = await ethers.getContractFactory("ContractReceiver");
receiver = await Receiver.deploy(gateway.address);
});
it("should execute cross-chain contract call", async function() {
const payload = receiver.interface.encodeFunctionData("setValue", [42]);
await expect(
caller.callContract("ethereum", receiver.address, payload)
).to.emit(gateway, "MockContractCall");
expect(await receiver.value()).to.equal(42);
});
it("should handle failed contract calls", async function() {
const invalidPayload = "0x12345678"; // Invalid function selector
await expect(
caller.callContract("ethereum", receiver.address, invalidPayload)
).to.be.revertedWith("Function execution failed");
});
});
Best Practices
1. Design for Failure
Always implement proper error handling and recovery mechanisms.
2. Validate Inputs
Thoroughly validate all parameters before making cross-chain calls.
3. Use Access Control
Implement proper access control for sensitive cross-chain operations.
4. Optimize Gas Usage
Minimize payload size and use efficient encoding methods.
5. Test Thoroughly
Test all cross-chain interactions on testnets before mainnet deployment.
6. Monitor Operations
Implement monitoring and alerting for cross-chain contract calls.
Resources
Last updated