Security
The tokenization precompile security model from security.go: caller checks, overflow, validation, DoS limits, error sanitization, threat model.
The tokenization precompile's protections live in x/tokenization/precompile/security.go and validation.go. This page states what the code enforces so contract authors know what they still have to check themselves.
// Every transaction method starts here
caller := contract.Caller()
if err := VerifyCaller(caller); err != nil {
return nil, err
}What the Precompile Enforces
Caller Verification
contract.Caller()is the address that made the call. The EVM sets it; a contract cannot forge it.- The caller becomes the
creatoron tokenization messages. A transfer'sfromremains the requested sender and is subject to approvals. Anycreatorin the JSON is overwritten. - A zero-address caller is rejected by
VerifyCallerwith error code 8.
The caller is the immediate caller, not the transaction origin. See Developer Guide.
Reentrancy
- Transaction atomicity rolls back failed state changes; it is not a reentrancy guard.
- Approval criteria and collection invariants can invoke EVM contracts through EVM query challenges. Review these callbacks when reasoning about a transfer's call graph.
- Contracts must protect their own state around external calls, using checks-effects-interactions and a reentrancy guard where needed. The EVM call stack does not provide this protection automatically. See Solidity's reentrancy guidance.
Overflow
- Every
big.Intis validated before conversion tosdkmath.Uint. CheckOverflowrejects nil, negative, and values above2^256-1(MaxUint256), so nothing is silently truncated on the way back to Solidity.- Ranges must satisfy
start <= end; amounts must be greater than zero. - IDs and times are
uint64on the chain. Values above18446744073709551615fail validation. UseFOREVER.
func CheckOverflow(value *big.Int, fieldName string) error {
if value == nil {
return ErrInvalidInput(fmt.Sprintf("%s cannot be nil", fieldName))
}
if value.Sign() < 0 {
return ErrInvalidInput(fmt.Sprintf("%s cannot be negative", fieldName))
}
if value.Cmp(MaxUint256) > 0 {
return ErrInvalidInput(fmt.Sprintf("%s overflow: value exceeds maximum uint256 (2^256-1)", fieldName))
}
return nil
}Input Validation
- Zero addresses are rejected (
ValidateAddress). - Empty arrays are rejected where the field is required (
ValidateArraySize). - Invalid ranges are rejected (
ValidateBigIntRanges). - Collection IDs must be non-zero (
ValidateCollectionId); zero IDs in queries are rejected byvalidateQueryRequest. - Required strings must be non-empty (
ValidateString). - Every message runs its
ValidateBasicbefore the keeper call.
DoS Limits
Array sizes are capped so a single call cannot exhaust the node.
| Field | Maximum |
|---|---|
Recipients per transfer (MaxRecipients) | 100 |
Token ID ranges (MaxTokenIdRanges) | 100 |
Ownership time ranges (MaxOwnershipTimeRanges) | 100 |
Approval ranges (MaxApprovalRanges) | 100 |
Denom units per path (MaxDenomUnits) | 50 |
Merkle challenges per approval (MaxMerkleChallenges) | 20 |
Coin transfers per approval (MaxCoinTransfers) | 50 |
Dynamic store challenges (MaxDynamicStoreChallenges) | 20 |
ETH signature challenges (MaxETHSignatureChallenges) | 20 |
Voting challenges (MaxVotingChallenges) | 20 |
EVM query challenges (MaxEVMQueryChallenges) | 10 |
Must-own-tokens rules (MaxMustOwnTokens) | 50 |
Addresses per address list (MaxAddressListEntries) | 1,000 |
Metadata string length, URI or customData (MaxMetadataLength) | 10,000 characters |
Messages per executeMultiple (MaxMessagesPerBatch) | 50 |
Ranges per query array (MaxQueryArraySize) | 1,000 |
Input size also adds gas (GasPerInputChunk) on executeMultiple, searchInRanges, and getBalanceForIdAndTime, so large JSON cannot be under-priced. See Gas.
Error Handling
- Errors are structured
PrecompileErrorvalues with a code, message, and details. - Details are sanitized: file paths, Go internals, module paths, and IP addresses are redacted; messages longer than 500 characters are truncated.
- Codes let a contract branch without parsing text. See Errors.
State Consistency
- All writes go through the module keeper.
- A transaction either applies every change or none. A failing
executeMultiplemessage reverts the whole batch.
Threat Model
| Threat | Protection |
|---|---|
| Reentrancy | Review EVM query callbacks and protect application state around external calls; atomicity alone is insufficient |
| Integer overflow | CheckOverflow, range validation, sdkmath.Uint arithmetic |
| Invalid input | Validation of every field before the keeper call |
| DoS through large inputs | Array size limits, input-size gas |
| Information leakage | Sanitized, truncated error details |
| State corruption | Atomic transactions, keeper validation |
| Caller spoofing | contract.Caller(), creator overwritten on the Go side |
Known Limitations
- No rate limiting at the precompile level. Add it at the chain or contract level if you need it.
- Gas price manipulation is handled by the EVM module, not the precompile.
- Access control (who may transfer, who may update a collection) is the tokenization module's approval and permission system. The precompile does not add its own authorization layer.
- Full protobuf decoding of query responses is not available in Solidity. See Return values.
What Your Contract Must Still Do
- Validate inputs before building JSON. The precompile rejects bad input, but a revert after JSON construction wastes gas.
- Check return values. Transaction methods return
bool successor an ID. - Wrap calls in
try/catchwhere you want to recover instead of revert. - Use
TokenizationJSONHelpersandTokenizationErrorsso the JSON matches the schema. - Review the collection's approvals and permissions. A transfer from a contract is subject to the same rules as any other sender. See Transferability and Permissions.
- Do authorization in the contract. The precompile sees the contract as the caller, never the user behind it.