Smart Contract Development in Ink! (Polkadot)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Smart Contract Development in Ink! (Polkadot)
Complex
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Smart Contract Development on Ink! (Polkadot)

Substrate chains are not EVM. Developers coming to Polkadot ecosystem with Solidity experience immediately hit fundamentally different execution model: WebAssembly runtime instead of EVM, Rust instead of Solidity, cargo-contract instead of Hardhat. Ink! is embedded DSL atop Rust, compiling to Wasm and deployed on pallet-contracts. Transferring mental models from EVM directly here is dangerous.

How Ink! Fundamentally Differs from Solidity

First thing that strikes — storage model. In Solidity mapping(address => uint256) is just slot in storage with keccak256 key. In Ink! each #[ink(storage)] field translates to separate Lazy entries in Substrate's Merkle storage tree. This means:

  • No concept of "slot" in EVM sense — no slot packing
  • Access to Mapping<AccountId, Balance> is get from off-chain state, not arithmetic over 32-byte word
  • StorageVec in Ink! 5.x lazy by default: elements loaded only on explicit read

Second fundamental difference — call model. In EVM msg.sender always immediate caller. In Ink! self.env().caller() returns previous caller in chain. Reentrancy in Ink! physically disabled by default via ReentrancyGuard at execution environment level, unless --allow-reentrant-calls flag passed explicitly. But this doesn't mean you can relax — cross-contract calls with CallBuilder still require careful state management.

Third feature — contract lifecycle. Ink! supports #[ink(message, payable)] for native token receipt, #[ink(constructor)] for initialization, and — unique to Polkadot ecosystem — set_code_hash() for updating code without address change. This is analog of UUPS proxy from EVM world, but built into protocol.

Typical Ink! Contract Mistakes

Wrong Mapping vs StorageHashMap Usage

Ink! 4.x had ink::storage::Mapping, which doesn't implement iteration over keys (done intentionally — off-chain indexing via events, not on-chain). Developers used to EnumerableMap from OpenZeppelin start storing keys in Vec<AccountId> next to Mapping, and this breaks on scaling: Vec loaded entirely on each read, making call O(n) by gas weight.

Correct solution — index via ink::env::emit_event! and build off-chain state via Subsquid or SubQuery. Don't try to recreate on-chain iterable structures.

Weight vs Gas: Fundamentally Different Cost Model

EVM counts gas operationally. Substrate counts weight — two-dimensional resource: ref_time (nanoseconds CPU) and proof_size (bytes of light client proof). Deploying via cargo-contract need explicitly specify --gas-limit in weight units, or use dry_run for estimation.

Pattern regularly causing issues: developer does cargo-contract call without prior dry_run, contract fails with OutOfGas, team guesses what's wrong — though sufficient to run:

cargo contract call --dry-run --contract <address> --message transfer --args <args>

Update via set_code_hash

Ink! allows updating contract code via self.env().set_code_hash(&new_code_hash). But new code's storage layout must be compatible with old. Changing field order in #[ink(storage)] — data in storage read incorrectly. In EVM proxy pattern this called storage collision — same problem here, but without bytecode-level verification tools.

Use ink_storage_traits::StorageLayout derive macros and manually check storage layout via cargo-contract info --output-json before each upgrade.

How We Build Ink! Projects

Stack and Tools

Tool Role
cargo-contract 4.x Compilation, deployment, calls
substrate-contracts-node Local dev node
drink! Unit testing without node (mock runtime)
openbrush Standard library (PSP22, PSP34)
Subsquid Event indexing
polkadot.js API Frontend integration

Test on three levels:

  1. Unit tests via #[ink::test] — synchronous, mock environment, fast
  2. Integration tests via drink! — real Substrate runtime without network, can test cross-contract calls
  3. E2E tests on substrate-contracts-node — full stack with real transactions

Token Standards in Polkadot Ecosystem

PSP22 — ERC-20 analog. PSP34 — ERC-721 analog. Both implemented via openbrush, providing trait-based extension system — like Solidity mixins via diamond proxy, but without selector clashing issues because Ink! uses blake2b hashes for message dispatch.

One subtlety: PSP22::transfer takes data: Vec<u8> — hook for receiver (ERC-777-like hook). If receiving contract implements PSP22Receiver, it can react to incoming transfer. If not — call still succeeds. Different from ERC-777 where tokensReceived mandatory for contract addresses.

Process

Analysis. Study target Substrate chain: which pallet-contracts version, any custom chain extensions, native token, need XCM integration for cross-chain calls.

Design. Define storage layout (can't change after deployment without migration), events for indexing, message interface. At this stage establish upgrade capability via set_code_hash — if needed.

Development. Write contract with drink! tests. 90%+ coverage. Test cross-contract interaction separately on substrate-contracts-node.

Audit and deployment. Static analysis via cargo clippy + manual review of critical paths. Deploy on testnet (Rococo Contracts), verify via polkadot.js Apps.

Timeline Guidelines

Simple contract (PSP22 token, 1-2 custom messages): 3-5 days including tests. Medium complexity with cross-contract calls and upgrade: 1-2 weeks. Complex protocol with XCM integration and custom chain extensions: from 1 month.

Specific timeline depends on target chain — contract parachains like Astar or Shiden may have own pallet-contracts config peculiarities.