EVM Query Challenges
EVM Query Challenges allow approvals to be gated by read-only EVM contract queries. The same challenge structure is also used in invariants (invariants.evmQueryChallenges), which run after every transfer and are typically used for supply control, balance caps, or max-holder checks rather than per-transfer approval gating.
Approval criteria:
approvalCriteria.evmQueryChallengesβ checked before a transfer is allowed; placeholders:$initiator,$sender,$recipient,$collectionId.Invariants:
invariants.evmQueryChallengesβ checked after all balance updates; same placeholders plus$recipients(all recipients as concatenated 32-byte hex). See Collection setup β invariants for where invariants are configured.
The challenge executes a staticcall to the specified contract with the given calldata. The result is compared against the expected result using the specified comparison operator. All challenges must pass for the transfer to be approved (or for the invariant to pass).
Structure
Challenge Fields
contractAddress
string
EVM contract address to query (0x format or bb1 format)
calldata
string
ABI-encoded function selector + arguments (hex string without 0x prefix)
expectedResult
string
Expected return value (hex string without 0x prefix). If empty, any non-error result passes.
comparisonOperator
string
How to compare: eq, ne, gt, gte, lt, lte. Default is eq.
gasLimit
string
Gas limit for the query (default 100000, max 500000)
uri
string
Optional metadata URI
customData
string
Optional custom data
Placeholders
The calldata field supports dynamic placeholders that are replaced at runtime. Placeholders differ between approval criteria and invariants because approval checks run per (from, to) pair while invariants run once after all transfers and can see multiple recipients.
Approval criteria (transfer gating)
Used in approvalCriteria.evmQueryChallenges on collection or user approvals. One recipient per check.
$initiator
Address of the transfer initiator
$sender
Address sending the tokens (from)
$recipient
Address receiving the tokens (to)
$collectionId
Collection ID (uint256, 32-byte hex)
Not available in approval context: $recipients (approval runs per single recipient).
Invariants (post-transfer)
Used in invariants.evmQueryChallenges. Checked once after all balance updates; can reference multiple recipients.
$initiator
Address of the transfer initiator
$sender
Address sending the tokens (from)
$recipient
First recipient only (convenience for single-recipient transfers)
$recipients
All recipient addresses concatenated as 32-byte-padded hex (no separator)
$collectionId
Collection ID (uint256, 32-byte hex)
Note: $recipients is each address ABI-padded to 32 bytes then concatenated (e.g. for two recipients, 64 hex chars + 64 hex chars). It is not comma-separated.
Example with placeholder:
This checks that the initiator has at least 1 token balance in the ERC-20 contract.
Comparison Operators
eq
Equals
Exact value matching
ne
Not equals
Exclusion checks
gt
Greater than
Minimum balance/value requirements
gte
Greater than or equal
Minimum threshold checks
lt
Less than
Maximum limit checks
lte
Less than or equal
Maximum threshold checks
Note: Only eq and ne work reliably for non-numeric return types.
Query Execution
Placeholder Replacement: All placeholders in
calldataare replaced with actual addressesStatic Call: Execute
eth_call(staticcall) to the contract with the calldataGas Limit: Query is limited by the specified
gasLimitto prevent DoS attacksResult Comparison: Compare the returned value against
expectedResultusingcomparisonOperatorPass/Fail: If comparison succeeds, challenge passes; otherwise, transfer is rejected
Type Definitions
Use Cases
ERC-20 Balance Check
Require the sender to hold at least 100 tokens of an ERC-20:
The 70a08231 is the function selector for balanceOf(address).
NFT Ownership Verification
Require the initiator to own a specific NFT:
The 6352211e is the function selector for ownerOf(uint256).
Building Calldata
To build the calldata for a function call:
Get Function Selector: First 4 bytes of
keccak256(functionSignature)Encode Parameters: ABI-encode the function parameters
Concatenate: Combine selector + encoded parameters
Add Placeholders: Replace address parameters with placeholders as needed
Example for balanceOf(address):
Function signature:
balanceOf(address)Selector:
keccak256("balanceOf(address)")β70a08231Address parameter: Pad to 32 bytes β
000000000000000000000000<address>With placeholder:
70a08231000000000000000000000000$initiator
Gas Limits
Simple storage
30,000
Balance check
50,000
Complex logic
100,000
Multiple calls
200,000
Maximum allowed
500,000
Error Conditions
The challenge fails if:
Contract address is invalid or doesn't exist
Calldata is malformed or empty
Contract reverts during the call
Query exceeds gas limit
Return value doesn't match expected result
Comparison operator is invalid
Result cannot be compared (e.g., numeric comparison on non-numeric data)
Security Considerations
Read-Only Execution
EVM Query Challenges execute as staticcall, which:
Cannot modify state
Cannot emit events
Cannot create or destroy contracts
Is fully deterministic within a block
Gas Limit Protection
Set appropriate gas limits to prevent:
DoS attacks through expensive queries
Excessive resource consumption
Unpredictable execution costs
Contract Trust
Only query trusted contracts:
Malicious contracts could return misleading data
Ensure the contract's logic is verified
Consider upgrade risks for proxy contracts
Determinism
All queries are deterministic within a block because:
EVM state is consistent within a block
staticcallcannot modify stateResults are reproducible
Placeholder Security
Placeholders are replaced at runtime:
Cannot be manipulated by users
Values come from the transfer context
Prevents injection attacks
Best Practices
Use verified contracts: Only query well-audited contracts
Set appropriate gas limits: Balance between reliability and cost
Test thoroughly: Verify calldata produces expected results
Document requirements: Use
urito explain what the challenge verifiesHandle edge cases: Consider what happens with zero balances, non-existent tokens, etc.
Combine with other challenges: Use alongside merkle challenges, voting, etc. for defense in depth
Last updated