XChain Platform Action - DEPLOY
This action deploys a smart contract to the XChain VM in one of five versioned formats covering inline, stakeable, chunked, and chunk-carrier deployments.
PARAMS
| Name | Type | Description |
|---|---|---|
VERSION |
String | Format Version (0 = standard, 1 = stakeable, 2 = chunked, 3 = chunked + stakeable, 4 = chunk carrier) |
CODE_ENCODING |
String | v0/v1 only: UTF-8 contract source, base64-encoded at/after DEPLOY_BASE64_CODE activation, hex-encoded before it |
CODE_HASH |
String | v2/v3/v4: sha256 hex of the assembled UTF-8 source; both the chunk-group id and integrity check |
GAS_LIMIT |
Integer | v0–v3: maximum gas units allowed for deployment (not used by v4) |
CONSTRUCTOR_PARAMS |
String | v0–v3: optional constructor parameters (pipe-delimited in v0/v2; single field in v1/v3) |
COOLDOWN_BLOCKS |
Integer | v1/v3 only: unstaking cooldown for STAKE v3 against this contract (1…100000) |
SLASH_DESTINATION |
String | v1/v3 only: address that receives slashed stake, or BURN for the chain’s burn address |
CHUNK_INDEX |
Integer | v4 only: 0-based position of this slice within the group |
TOTAL_CHUNKS |
Integer | v4 only: declared number of slices in the group (1..MAX_DEPLOY_CHUNKS) |
CODE_PART |
String | v4 only: one base64 slice of base64(code); plain concatenation in CHUNK_INDEX order restores base64(code) exactly |
Formats
Version 0 - Standard (non-stakeable)
VERSION|CODE_ENCODING|GAS_LIMIT|...CONSTRUCTOR_PARAMS
Version 1 - Stakeable contract
VERSION|CODE_ENCODING|GAS_LIMIT|CONSTRUCTOR_PARAMS|COOLDOWN_BLOCKS|SLASH_DESTINATION
Version 2 - Chunked (non-stakeable)
VERSION|CODE_HASH|GAS_LIMIT|...CONSTRUCTOR_PARAMS
Version 3 - Chunked + stakeable
VERSION|CODE_HASH|GAS_LIMIT|CONSTRUCTOR_PARAMS|COOLDOWN_BLOCKS|SLASH_DESTINATION
Version 4 - Chunk carrier
VERSION|CODE_HASH|CHUNK_INDEX|TOTAL_CHUNKS|CODE_PART
Examples
DEPLOY|0|<base64_code>|200000|arg1|arg2
Deploy a non-stakeable contract with constructor arguments
DEPLOY|0|<base64_code>|100000|
Deploy a non-stakeable contract with no constructor parameters
DEPLOY|1|<base64_code>|200000||1000|BURN
Deploy a stakeable contract: 1000-block cooldown on STAKE v3 unstakes, slashed tokens go to the chain's burn address
DEPLOY|1|<base64_code>|200000||100|bc1q...recipient
Deploy a stakeable contract: 100-block cooldown, slashed tokens routed to a specific recipient address (not BURN)
DEPLOY|4|4651d57c...b6021765|0|3|<base64_slice_0>
First of three v4 carrier slices for the contract whose assembled source hashes to 4651d5...
DEPLOY|4|4651d57c...b6021765|2|3|<base64_slice_2>
Final slice of the same group; a later DEPLOY|2 (or DEPLOY|3) then assembles by CODE_HASH
Rules
- Available on all chains
CODE_ENCODINGmust be valid UTF-8 JavaScript source code (base64-encoded at/after theDEPLOY_BASE64_CODEactivation, hex-encoded before it; see Encoding activation below) and must not exceed 64KB (65536 bytes decoded)GAS_LIMITmust be a positive integer- The VM validates syntax before charging gas:
- V8 compilation check, rejects JavaScript syntax errors
- Acorn metering pass, rejects syntax beyond ES2020 (the supported syntax set)
- Reserved identifier check, rejects code containing
__gas(reserved for gas metering), the allocator metering helpers (__concat,__setconcat,__setconcatL,__tmpl,__tmpltag,__tmpltagm,__arrspread,__objspread,__objspreadmeter), or the call-depth metering helpers (__depth_enter,__depth_exit); all are harness-injected and a contract may not define or reference them - Banned
Math.*check, rejectsMath.sqrt/Math.pow/Math.log/Math.log2/Math.log10(widening underVM_LINT_HARDENINGto the complement of the deterministic SafeMath whitelist, plus the**/**=exponentiation operator) - Banned literal check, rejects
BigIntandRegExpliterals - Banned async check (consensus-gated), rejects
async/await/Promisereferences after theVM_BANNED_ASYNCflag-day - Banned generator check (consensus-gated), rejects
function*, generator methods, andyield; live from genesis on testnet/regtest - Banned WebAssembly check (consensus-gated), rejects any reference to the global
WebAssembly; live from genesis on testnet/regtest
- If syntax validation fails, the deployment is rejected with
invalid: CODE_ENCODING (<reason>)and no gas is charged
flowchart TD
Start["DEPLOY submitted"] --> V8{"1. V8 compilation check"}
V8 -->|"syntax error"| Reject["invalid: CODE_ENCODING (reason), no gas charged"]
V8 -->|"pass"| Acorn{"2. Acorn metering pass, ES2020 syntax set"}
Acorn -->|"fail"| Reject
Acorn -->|"pass"| Reserved{"3. Reserved identifier check, __gas and metering helpers"}
Reserved -->|"fail"| Reject
Reserved -->|"pass"| MathCheck{"4. Banned Math.* check, sqrt/pow/log/log2/log10, widened under VM_LINT_HARDENING"}
MathCheck -->|"fail"| Reject
MathCheck -->|"pass"| Literal{"5. Banned literal check, BigInt and RegExp"}
Literal -->|"fail"| Reject
Literal -->|"pass"| Async{"6. Banned async check, async/await/Promise, consensus-gated"}
Async -->|"fail"| Reject
Async -->|"pass"| Generator{"7. Banned generator check, function*, generator methods, yield"}
Generator -->|"fail"| Reject
Generator -->|"pass"| Wasm{"8. Banned WebAssembly check, global WebAssembly reference"}
Wasm -->|"fail"| Reject
Wasm -->|"pass"| Charge["Gas charged, deployment proceeds"]
- A non-blocking float usage warning is generated if decimal number literals are detected (visible in the execution record)
- A gas fee is charged at deployment:
VM_DEPLOY_BASE + (code_bytes * VM_DEPLOY_PER_BYTE) SOURCEaddress must hold sufficient XCHAIN tokens to cover the gas feeCONSTRUCTOR_PARAMSis a rest-field in v0/v2 (the indexer joins all pipe-segments from position 3 onward with|, so a multi-argument constructor passes each argument as its own pipe-delimited segment). In v1/v3 it is a single fixed field (position 3 only) becauseCOOLDOWN_BLOCKSandSLASH_DESTINATIONfollow; a v1/v3 constructor that needs multiple arguments must sub-delimit them within that one field.- If
CONSTRUCTOR_PARAMSis provided, the VM executes the contract’sinitializemethod immediately after deployment:- Constructor gas is added to the deployment gas:
total_gas = deploy_gas + constructor_gas - If the constructor fails (reverts, out of gas, etc.), the entire deployment is rolled back; the contract is not stored
- The caller pays the combined gas even on constructor failure
- Constructor gas is added to the deployment gas:
- A derived address is created for the contract in the format
C:<CHAIN>:<ACTION_INDEX>(e.g.,C:BTC:500). This address participates in the standard balance system for token custody via DEPOSIT/WITHDRAW.
Staking fields (v1/v3)
- Both staking fields are optional in the wire format. A v1 DEPLOY with empty
COOLDOWN_BLOCKSis treated the same as a v0 deploy (the contract is not stakeable).SLASH_DESTINATIONwithoutCOOLDOWN_BLOCKSis rejected asinvalid: SLASH_DESTINATION (requires COOLDOWN_BLOCKS). COOLDOWN_BLOCKSmust be an integer in[1, 100000]. Sets the unstaking cooldown for STAKE v3 actions against this contract (overrides the globalSTAKING.COOLDOWN_BLOCKSfor v3 unstakes on this contract).SLASH_DESTINATIONaccepts either an address (must be valid on the deploying chain) or the literal sentinelBURN. The sentinel resolves to the chain’s configured burn address.- If
COOLDOWN_BLOCKSis set butSLASH_DESTINATIONis empty, the indexer defaultsSLASH_DESTINATIONto the chain’s burn address. - A contract deployed with both staking fields can receive STAKE v3 actions targeting it; without them, STAKE v3 rejects with
invalid: TARGET_CONTRACT_INDEX (contract is not stakeable). - Stakeable-contract metadata is immutable after deployment: there is no mechanism to update
COOLDOWN_BLOCKSorSLASH_DESTINATIONlater.
Permissions manifest (optional)
- A contract may export a permissions manifest alongside its methods to declare its own bound; the indexer reads it deterministically at deploy time (by instantiating the module top-level, no method runs, so it works even without a constructor) and persists it to the
contract_permissionstable. Both fields are optional:permissions: an array of action-type strings (e.g.['SEND','ISSUE']). The contract may emit only these action types, on every path (constructor,EXECUTE, or controllerguard); any other emission is rejected fail-closed. Absent means unrestricted (the default);[]means the contract may emit nothing.maxTakeBps: an integer in[0, 10000]that tightens this contract’s controller royalty cap tomin(CONTROLLER_MAX_TAKE_BPS, maxTakeBps). Absent means the global cap applies. See Controller-Bound Tokens.
- A malformed manifest (
permissionsnot an array of strings, ormaxTakeBpsnot an integer in range) rejects the deployment withinvalid: CONTRACT_MANIFEST (<reason>). The manifest is immutable after deployment (the code is immutable).
ABI (optional)
- A contract may also export a static
abiobject describing its methods (names, typed params, one-line summaries, read-only flags) for wallets and explorers. Unlike the permissions manifest, theabiis never read or validated at deploy time: it is advisory display metadata parsed off-chain from the source, participates in no consensus rule, and a malformedabineither rejects nor affects the deployment. See Contract ABI.
Chunk carrier rules (v4)
- Available on all chains
CODE_HASHmust be a 64-char lowercase sha256 hex stringCHUNK_INDEXandTOTAL_CHUNKSmust be non-negative integers withCHUNK_INDEX < TOTAL_CHUNKS, andTOTAL_CHUNKSin[1, MAX_DEPLOY_CHUNKS]CODE_PARTmust be a non-empty base64-alphabet string (A-Za-z0-9+/=) no larger thanMAX_DEPLOYCHUNK_PART_BYTES. It is a slice ofbase64(code)and is not individually decoded; the assembling DEPLOY concatenates every slice then decodes and sha256-verifies the whole, so a corrupt or misordered slice surfaces asinvalid: CODE_HASH (assembly mismatch)on the assembling DEPLOY, not on the carrier.- Gas: a valid v4 carrier is charged
len(CODE_PART) * VM_DEPLOY_PER_BYTE(valued atGAS_PRICE), payable in XCHAIN or (when aFEE_DESTINATIONoutput is present) the native coin, exactly like a deploy. An invalid carrier is recorded with its rejection status and charged nothing. - Every carrier (valid or invalid) is recorded so the explorer can surface its status; a chunked DEPLOY assembles only the valid carriers, and if a deployer broadcasts the same
(source, CODE_HASH, CHUNK_INDEX)more than once the lowest action index deterministically wins.
Chunked assembly (v2/v3)
- A chunked DEPLOY assembles its code from the deploying address’s prior v4 carrier actions that share the same
CODE_HASHand were recorded at a lower action index than the DEPLOY. Carriers are matched to their submitter (source_id), so a third party cannot hijack another deployer’s chunk group. - The indexer concatenates the carriers’
CODE_PARTfields inCHUNK_INDEXorder, base64-decodes the result, and rejects unlesssha256(code) === CODE_HASH. A missing position, a non-contiguous set, a short group, a bad chunk count, or a digest mismatch each rejects the DEPLOY (invalid: CODE_HASH (...)); the assembled code then flows through the exact same size/syntax/manifest/constructor path as an inline deploy. - Gas: each v4 carrier pays the per-byte component (
VM_DEPLOY_PER_BYTE) for the bytes it puts on-chain, so the assembling DEPLOY v2/v3 chargesVM_DEPLOY_BASEplus constructor gas only; the net cost approximates a single-shot inline deploy of the same source. - Reorg/recovery: because a DEPLOY only ever consumes carriers at a lower action index, any reorg that removes a carrier also removes the dependent DEPLOY (and its contract) via the standard action-index rollback. No bespoke logic. The code is fully on-chain in the v4 carrier actions, so a from-scratch chain re-parse reconstructs the contract with no ANCHOR change.
- Submit the carriers before the assembling DEPLOY: a DEPLOY v2/v3 only consumes carriers recorded at a lower action index than itself.
- The SDK (
sdk.deployContract) auto-selects: it deploys inline (v0/v1) whenbase64(code)fits one action, else uploads the slices as v4 carriers (awaiting indexer confirmation of each) and assembles via v2/v3.
flowchart TD
Carriers[("v4 carrier actions, same CODE_HASH, lower action index, matched by source_id")] --> Concat["Concatenate CODE_PART fields in CHUNK_INDEX order"]
Concat --> Decode["Base64-decode the concatenated result"]
Decode --> Verify{"sha256(code) === CODE_HASH?"}
Verify -->|"missing position, non-contiguous, short group, bad chunk count, or digest mismatch"| Reject["invalid: CODE_HASH (...), DEPLOY rejected"]
Verify -->|"match"| Inline["Assembled code flows through the same size/syntax/manifest/constructor path as an inline deploy"]
Encoding activation
The inline CODE_ENCODING field (v0/v1) was originally hex-encoded and later changed to base64 (1.33x the source vs hex’s 2x, lifting the single-action contract-size ceiling). To keep the change consensus-safe, the format is gated behind the DEPLOY_BASE64_CODE protocol activation rather than flipped unconditionally:
- Before the activation, the indexer decodes
CODE_ENCODINGas hex (Buffer.from(field, 'hex')). - At/after the activation, it decodes as base64 (
Buffer.from(field, 'base64'), round-tripped to reject non-canonical input).
This makes every historical inline DEPLOY decode identically across node versions and on a from-genesis re-parse, so its code_hash and therefore the per-block contract hash and the federation checkpoint preimage are stable.
The activation is keyed on block time (a single coordinated flag-day), not block height, because DEPLOY runs on BTC, LTC and DOGE, whose heights diverge by millions of blocks; one timestamp names the same cutover on all three chains. Testnet/regtest activate at genesis (base64-native). The mainnet flag-day must be aligned with the SDK’s base64 rollout: the SDK emits the matching encoding for the target block so an inline DEPLOY is always decoded on the side of the gate it was encoded for. v4 carrier slices (assembled by chunked v2/v3) are base64 from genesis and are unaffected.
Notes
- Use
^(caret) as prefix when passing anADDRESS_IDforSLASH_DESTINATION(^57 =ADDRESS_ID57);SLASH_DESTINATIONmay instead be theBURNsentinel, which is never compacted. See Index ID References - The deployed contract is assigned an action index derived from the transaction that contains this action
CODE_ENCODING(v0/v1) is base64-encoded UTF-8 at/after theDEPLOY_BASE64_CODEactivation (hex before it); decode the active format withBuffer.from(field, 'base64'|'hex').toString('utf8')- The
contractstable stores the decoded plain-text JavaScript, not the base64 encoding - The
contractstable’sapi_versionfield (currently frozen at 1) records which gateway API version the contract targets; it is assigned by the indexer at deploy time, not a field a deployer sets in the DEPLOY action wire format - Use
EXECUTEto call methods on a deployed contract - Use
DEPOSITandWITHDRAWto transfer token balances into and out of the contract’s derived address - Deployed contracts are immutable: there is no mechanism to update code after deployment
VM_DEPLOY_BASEandVM_DEPLOY_PER_BYTEconstants are defined in the gas schedule configuration
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.