Hey everyone ![]()
We’re the team at Montaq Labs. We’ve been building a decentralized application on Polkadot Asset Hub using Solidity compiled through resolc (the Revive compiler targeting PolkaVM). We wanted to share our experience, both the challenges we hit and the creative approaches we explored to work through them , in the hope that it helps other builders and contributes to the conversation around developer tooling on Polkadot.
First, a genuine acknowledgment: the work Parity and the Revive team have done to bring Solidity compatibility to PolkaVM is impressive. The fact that we can write Solidity and deploy to Polkadot’s native VM at all is a significant engineering achievement. What we’re sharing here isn’t criticism , it’s field notes from pushing the tooling to its current boundaries.
What We Were Building
Without going into the specifics of our product, we were developing a trustless escrow contract with moderately complex logic, multiple user roles, several distinct phases of interaction, proportional fund distribution, and anti-fraud safeguards. Think of it as a contract with roughly:
-
~8 core functions (create, enroll, bet, vote, cancel, finalize, claim, refund)
-
~11 storage mappings (including nested mappings up to 3 levels deep)
-
Structs, dynamic arrays, event logging
-
A few loops and conditional payout calculations
By Ethereum/EVM standards, this is a medium-complexity contract, nothing exotic. On solc targeting the EVM, the compiled bytecode comes in around 6-8 KB, well within the 24 KB deployed code limit and 49 KB initcode limit.
On resolc targeting PolkaVM, our journey looked very different.
The Problem: Initcode Size Limit
Polkadot Asset Hub enforces a 49,152 byte (48 KB) initcode size limit — the same as EIP-3860 on Ethereum. The challenge is that resolc produces significantly larger bytecode than solc for the same Solidity source.
Here’s what we observed through our iterative attempts:
Step-by-Step: What We Tried
Attempt 1 — Original Contract (Full Features, Clean Code)
Standard Solidity patterns: named errors, public mappings, structs, constants, multiple view functions.
Result: 120,294 bytes — 2.4x over the limit
We assumed this was an optimization settings issue, so we tried enabling the compiler optimizer.
Attempt 2 — Compiler Optimization (runs = 200, then runs = 1)
We enabled the optimizer in Remix and set runs to 1 for maximum deployment size reduction.
Result: 120,294 bytes — identical
The output didn’t change by a single byte. The optimizer settings appeared to have no effect on the resolc output, or weren’t being picked up. This was our first indication that the issue wasn’t about Solidity-level optimization.
Attempt 3 — Aggressive Solidity-Level Minification
We stripped the contract significantly:
-
Replaced all named custom errors with a single error E(uint8) using numeric codes
-
Made all mappings internal to eliminate auto-generated getter bytecode
-
Removed structs where possible and flattened storage
-
Reduced events from 7 to fewer, more compact ones
-
Inlined constants as literals
-
Removed all view functions except two essential ones
Result: 99,829 bytes — still 2x over
A ~20% reduction for substantial readability and developer experience sacrifices.
Attempt 4 — Radical Storage Redesign (3 Generic Mappings)
Inspired by the EVM diamond storage pattern, we replaced all 11 typed mappings with 3 generic ones:
mapping(bytes32 => uint256) internal U;
mapping(bytes32 => address) internal A;
mapping(bytes32 => bytes32) internal B;
All data accessed through computed keccak256 keys with tag prefixes. The contract logic remained identical.
Result: 144,596 bytes — actually WORSE
This was a critical learning moment. Each keccak256(abi.encodePacked(…)) call generates massive PolkaVM bytecode. The hashing helper functions that were supposed to save space by reducing mapping count actually increased the output significantly. The resolc compiler appears to expand hash computations into very verbose instruction sequences.
Attempt 5 — XOR Keys Instead of Hashing
We replaced keccak256 composite keys with simple XOR operations:
function _ak(bytes32 id, bytes32 aid) internal pure returns (bytes32) {
return id ^ aid;
}
Back to individual mappings but with XOR-based key derivation, bit-packed structs (timestamps, counters, and flags in a single uint256), and boolean flags packed as individual bits.
Result: 86,869 bytes — progress, but still 1.7x over
Attempt 6 — Function Merging (8 Functions → 5)
Since the overhead seemed proportional to function count and total storage operations, we merged related functions:
-
create + cancel → manage(action)
-
bet + vote → engage(action)
Result: 83,121 bytes — marginal improvement
This confirmed that the bloat is per-storage-operation, not per-function. Merging function bodies doesn’t help because the same number of SLOAD/SSTORE operations exist.
Attempt 7 — Feature Removal
At this point we estimated resolc generates approximately 1.2-1.4 KB of PolkaVM bytecode per storage operation. With ~60 storage operations across the contract, the minimum possible output was ~72-84 KB before even accounting for ABI dispatch, constructor logic, and event encoding.
We could have gotten under 49 KB by removing per-agent betting, on-chain vote tallying, and the agent registry — but that would have meant shipping a fundamentally different (and less trustless) product. We chose not to go down that path.
Attempt 8 — 2-Contract Split (The Solution That Works)
We split the contract into:
-
EscrowData — Pure storage contract. Single-line setter/getter functions, holds all funds, no business logic. (~28 KB estimated)
-
EscrowLogic — All business logic, zero own storage, reads/writes through external calls to EscrowData. (~42 KB estimated)
This preserves every feature: per-agent betting, on-chain voting with tally loop, proportional payouts, refunds, anti-fraud checks — everything. The tradeoff is deployment complexity (deploy two contracts, wire them together) and slightly higher gas costs from cross-contract calls.
What We Learned
The Bytecode Multiplier Is Real
For the same Solidity source, resolc produces roughly 10-20x more bytecode than solc. This appears to stem from:
-
Storage operations: Each SLOAD/SSTORE equivalent on PolkaVM requires ~50-80 instructions (slot computation, memory serialization, host function calls, error handling) vs. 1-3 bytes on EVM
-
Hash functions: keccak256 calls are particularly expensive in bytecode size on PolkaVM
-
ABI encoding/decoding: Struct returns and multi-parameter functions generate substantial code
-
Public mapping getters: Each auto-generated getter adds ~2-3 KB
Optimization Settings May Not Apply
We observed that resolc’s optimizer (at least through Remix) may not have the same impact as solc’s. The output was identical regardless of optimization level in our testing. This may be a Remix integration issue rather than a compiler issue.
What Helps vs. What Doesn’t
| Approach | Impact |
| Removing public mapping getters | |
| Reducing function count | |
| Using keccak256 for key derivation | |
| XOR-based composite keys | |
| Bit-packing multiple values in uint256 | |
| Splitting into multiple contracts | |
| Generic mappings (diamond-style) |
Suggestions for the Ecosystem
-
. Storage operation optimization in resolc: Since storage access is the dominant cost in bytecode size, any compiler-level optimization of the generated instruction sequences for SLOAD/SSTORE equivalents would have outsized impact.
-
Multi-contract deployment tooling: Since splitting contracts is effectively required for medium-to-complex DApps, first-class tooling for deploying and wiring multi-contract systems on Asset Hub would improve the developer experience significantly.
-
Size profiling tools: A resolc --size-breakdown flag that shows bytecode contribution by function/operation would help developers make informed architectural decisions early.
Final Thoughts
We’re committed to building on Polkadot. The network’s architecture, shared security model, and the direction of Asset Hub as a smart contract platform are compelling. The resolc compiler is early-stage technology doing something genuinely hard, translating EVM-oriented bytecode to a completely different VM architecture, and the team behind it deserves credit for how far it’s come.
That said, developers considering Polkadot Asset Hub for contracts beyond simple token logic should plan for the bytecode size constraint from day one. Architect for multi-contract deployments, keep storage operations minimal per contract, and avoid keccak256 for internal key computation where possible.
We hope sharing our experience saves other teams the same discovery process. Happy to answer questions or compare notes with anyone hitting similar issues.