14. Cross-Contract Calls
Smart contracts rarely operate in isolation. Modern decentralized applications rely on modular architectures, token transfers, vault interactions, and protocol interoperation. The Quorlin Smart Contract Language and the Kortana Virtual Machine (KVM) provide a strongly typed, security-focused system for executing cross-contract calls.
This chapter details how Quorlin constructs cross-contract calls, how interfaces are declared and consumed, how standard standards like ERC-20 are integrated, and how the underlying KVM dispatch engine handles communication between contracts.
14.1 Fundamentals of Contract Interoperability
In Quorlin, calling an external contract feels like invoking a method on an object. However, under the hood, a cross-contract call involves multi-layered data serialization, gas allocation, message dispatch, and execution context switching.
When Contract $A$ calls Contract $B$:
- ABI Encoding: Contract $A$ serializes the function selector (the first 4 bytes of the Keccak-256 hash of the function signature) and its arguments into ABI-compliant byte buffers.
- Context Dispatch: The KVM host environment pauses Contract $A$'s execution and issues a call request through the
ICallDispatcherinterface. - Target Execution: KVM initializes a new execution frame for Contract $B$. Inside Contract $B$, the context variable
callerevaluates to Contract $A$'s address. - Return Decoding: Contract $B$ finishes execution, returning output bytes or reverting. KVM returns control to Contract $A$, which automatically decodes the ABI-encoded return values.
14.2 Interface Declarations in Quorlin
To interact with an external contract, Quorlin requires an interface definition. Interfaces declare the external contact surface of a contract without providing method bodies.
Syntax and Mutability Modifiers
Interface methods must explicitly declare their state mutability using Quorlin's access keywords:
reads: Denotes a view or pure function that reads contract or block state without modifying storage.writes: Denotes a state-changing method that can write to storage, emit events, or transfer tokens.
interface IERC20 { reads number totalSupply(); reads number balanceOf(address owner); reads number allowance(address owner, address spender); writes truth transfer(address recipient, number amount); writes truth approve(address spender, number amount); writes truth transferFrom(address sender, address recipient, number amount); }
Type Mapping: Quorlin Keywords vs. Ethereum ABI Types
While Quorlin presents high-level, human-readable type names, the compiler automatically maps them to canonical Ethereum ABI types when constructing function signatures for selectors.
| Quorlin Type | Internal Keyword | External Canonical ABI Type |
|---|---|---|
number | Type::U256 | uint256 |
truth | Type::Bool | bool |
address | Type::Address | address |
text | Type::Text | string |
nothing | Type::Void | void |
As seen in quorlin/parser.cpp and quorlin/abi.cpp, the diagnostic human name (e.g., number) and the ABI signature name (e.g., uint256) are strictly separated:
std::string_view abi_type_name(Type type) noexcept { switch (type) { case Type::U256: return "uint256"; case Type::Bool: return "bool"; case Type::Address: return "address"; case Type::Text: return "string"; case Type::Void: return "void"; case Type::Invalid: return "<invalid>"; } return "<unknown>"; }
This ensures that function selectors generated by Quorlin (e.g., transfer(address,uint256)) match the standard EVM Keccak-256 selector calculation hash, ensuring seamless cross-chain and cross-language compatibility.
14.3 Built-in Standard Interfaces
Quorlin includes built-in interface declarations for standard protocols defined directly in quorlin/standard.cpp. You do not need to rewrite common definitions like IERC20 in every source file.
The standard toolchain initializes built-in standards using predefined function signatures:
// Constructed once within quorlin/standard.cpp built.push_back(StandardInterface{ "IERC20", { {"totalSupply", "totalSupply()", {}, Type::U256, Mutability::View}, {"balanceOf", "balanceOf(address)", {Type::Address}, Type::U256, Mutability::View}, {"transfer", "transfer(address,uint256)", {Type::Address, Type::U256}, Type::Bool, Mutability::Mut}, {"allowance", "allowance(address,address)", {Type::Address, Type::Address}, Type::U256, Mutability::View}, {"approve", "approve(address,uint256)", {Type::Address, Type::U256}, Type::Bool, Mutability::Mut}, {"transferFrom", "transferFrom(address,address,uint256)", {Type::Address, Type::Address, Type::U256}, Type::Bool, Mutability::Mut}, } });
Because these standard interfaces are embedded directly into the semantic analyzer (sema.cpp), invoking an IERC20 method guarantees complete type checking and signature alignment at compile time.
14.4 Invoking Foreign Contracts
To invoke an external contract, bind an interface to a target address variable, then call its declared methods.
Example: Token Vault Router
The following Quorlin contract demonstrates receiving standard tokens from a caller and executing external transfers using the IERC20 interface:
contract VaultRouter { address tokenAddress; map<address, number> depositedBalances; event Deposit(address indexed user, number amount); event Withdraw(address indexed user, number amount); constructor { tokenAddress = 0x1234567890123456789012345678901234567890; } writes truth depositTokens(number amount) { require amount > 0, "amount must be positive"; // Bind the interface to the remote contract address IERC20 token = IERC20(tokenAddress); // Execute external 'writes' call truth success = token.transferFrom(caller, address(this), amount); require success, "transferFrom failed"; depositedBalances[caller] = depositedBalances[caller] + amount; emit Deposit(caller, amount); return yes; } writes truth withdrawTokens(number amount) { number userBalance = depositedBalances[caller]; require userBalance >= amount, "insufficient balance"; depositedBalances[caller] = userBalance - amount; IERC20 token = IERC20(tokenAddress); // Execute external state-changing transfer call truth success = token.transfer(caller, amount); require success, "token transfer failed"; emit Withdraw(caller, amount); return yes; } reads number checkRemoteBalance(address account) { IERC20 token = IERC20(tokenAddress); // Execute external 'reads' call return token.balanceOf(account); } }
14.5 KVM Architecture: Dispatching Calls Under the Hood
When a Quorlin contract issues a call, the compiler emits bytecode that interacts with the KVM execution engine's host layer.
The Host Interface (StateHost)
In KVM (kvm/state_host.cpp), cross-contract invocations are handed off to an implementation of the ICallDispatcher interface:
CallResult StateHost::call(const CallRequest& request) { if (dispatcher_ == nullptr) return CallResult{}; return dispatcher_->dispatch(request); }
The CallRequest bundle includes:
- Target Address: The 20-byte destination address converted via
address_from_word(). - Call Data: The ABI-encoded byte sequence consisting of the 4-byte selector followed by encoded positional arguments.
- Gas Limit: The compute allocation passed to the child context.
- Value: Any native currency transferred alongside the call.
Execution Processing Flow
+------------------------+ +------------------------+ +------------------------+
| Quorlin Contract A | | KVM StateHost | | Quorlin Contract B |
| (Caller Frame) | | (ICallDispatcher) | | (Callee Frame) |
+-----------+------------+ +-----------+------------+ +-----------+------------+
| | |
| 1. Formulate Call Data | |
| 2. Emit Call Instruction | |
+------------------------------------->| |
| 3. Resolve Target Address |
| 4. Instantiate Child Interpreter |
+------------------------------------->|
| 5. Match Selector
| 6. Execute Method
| 7. Return Result Bytes
|<-------------------------------------+
|
|<-------------------------------------+
| 8. Decode Return Value
| 9. Resume Execution
14.6 Interoperability with Solidity and the EVM
Because Quorlin is fully compliant with the standard Ethereum ABI specification, Quorlin contracts can seamlessly interoperate with contracts written in Solidity or Vyper.
Calling Solidity Contracts from Quorlin
Suppose a Solidity contract is deployed on the Kortana Blockchain with the following interface:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract PriceFeed { function getLatestPrice(string memory symbol) external view returns (uint256); }
A Quorlin contract can define an interface that mirrors this Solidity contract signature:
interface PriceFeed { reads number getLatestPrice(text symbol); } contract OracleConsumer { reads number fetchPrice(address feedAddress, text symbol) { PriceFeed feed = PriceFeed(feedAddress); return feed.getLatestPrice(symbol); } }
When compiled:
getLatestPrice(text)is translated by the ABI layer intogetLatestPrice(string).- The Keccak-256 hash of
getLatestPrice(string)produces the exact 4-byte selector required by the Solidity contract. - Argument encodings (e.g., dynamic offset pointers for strings and 256-bit word paddings) match the standard EVM ABI specification.
Calling Quorlin Contracts from Solidity
Conversely, when a Solidity contract calls a Quorlin contract, abi.cpp ensures that the JSON ABI produced during compilation contains the expected standard type declarations:
[ { "type": "function", "name": "balanceOf", "inputs": [ { "name": "owner", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "number" } ], "stateMutability": "view" } ]
Tools like Ethers.js, Web3.js, or Solidity contracts calling Quorlin targets process the interface without requiring custom adaptors.
14.7 Error Handling and Call Failure
When making cross-contract calls, execution failures in the target contract must be safely caught or propagated to prevent contract state corruption.
Revert Behavior
If an external call target reverts (e.g., triggering a failed require condition or running out of gas):
- The target contract's child state modifications are completely discarded by the
StateHost. - The execution status in
CallResultrecords the failure. - In Quorlin, an unhandled failed external call bubbles up immediately, causing the invoking context to revert as well, preserving atomic transactional integrity across contract boundaries.
Pre-Condition Validation
To avoid cascading failures, developers should enforce preconditions before initiating external state modifications:
writes truth safeTransfer(address token, address to, number amount) { // 1. Local state validation require to != 0x0000000000000000000000000000000000000000, "invalid target"; require amount > 0, "zero amount"; // 2. Perform external call IERC20 targetToken = IERC20(token); truth success = targetToken.transfer(to, amount); // 3. Post-call assertion require success, "token transfer rejected"; return yes; }