Solidity to Compact: A Developer's Comparison Guide
A practical side-by-side reference for developers transitioning from Solidity to Midnight's Compact language.
Introduction
If you're a Solidity developer looking to build on Midnight Network, you'll find that Compact shares some familiar concepts but introduces privacy-first design patterns that fundamentally change how you think about smart contracts.
This guide maps 10 common Solidity patterns to their Compact equivalents, highlighting key differences along the way.
1. Contract Structure & Module Definition
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyToken {
// state variables, functions, events
}
Compact
// SPDX-License-Identifier: MIT
pragma language_version >= 0.21.0;
module MyToken {
import CompactStandardLibrary;
// ledger entries, circuits, types
}
Key differences:
- Compact uses
moduleinstead ofcontract -
pragma language_versioninstead ofpragma solidity - No inheritance — use
importwith optionalprefixfor namespacing - Functions are called
circuits (they become ZK circuits)
2. State Variables (Ledger Entries)
Solidity
contract Storage {
uint256 public count;
address public owner;
mapping(address => uint256) public balances;
bool public initialized;
}
Compact
module Storage {
import CompactStandardLibrary;
// Public (unshielded) state — visible on-chain
export ledger count: Uint<128>;
export ledger owner: Either<ZswapCoinPublicKey, ContractAddress>;
export ledger balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;
export sealed ledger initialized: Bool;
}
Key differences:
- State variables are called
ledgerentries -
exportmakes them readable;sealedrestricts writes to the module - No
uint256— max isUint<128>(circuit backend limitation) - Addresses use
Either<ZswapCoinPublicKey, ContractAddress>instead of a simpleaddresstype -
Map<K, V>replacesmapping
3. Constructor & Initialization
Solidity
contract Ownable {
address public owner;
constructor(address _owner) {
owner = _owner;
}
}
Compact
module Ownable {
import CompactStandardLibrary;
export ledger _owner: Either<ZswapCoinPublicKey, ContractAddress>;
// Called during contract deployment
export circuit constructor(owner: Either<ZswapCoinPublicKey, ContractAddress>): [] {
assert(!isKeyOrAddressZero(owner), "Invalid owner");
_owner = owner;
}
}
Key differences:
- Compact uses a
constructorcircuit (special naming convention) - Must return
[](empty tuple) - Use
assert()for validation — norequire()equivalent - No constructor overloading
4. Functions → Circuits
Solidity
function transfer(address to, uint256 amount) public returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
Compact
export circuit transfer(
sender: Either<ZswapCoinPublicKey, ContractAddress>,
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
assert(_balances[sender] >= amount, "Insufficient balance");
_balances[sender] = _balances[sender] - amount;
_balances[recipient] = _balances[recipient] + amount;
}
Key differences:
-
function→circuit - No
msg.sender— caller identity passed as parameter or derived from context - No
public/private/internalvisibility — useexportfor public - All circuits are ZK circuits under the hood
- State mutations use direct assignment (
=)
5. Mappings & Storage
Solidity
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
function getBalance(address account) public view returns (uint256) {
return balances[account];
}
Compact
export ledger balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;
export ledger allowances: Map<Either<ZswapCoinPublicKey, ContractAddress>,
Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>;
export circuit getBalance(
account: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
return balances[account];
}
Key differences:
- Nested maps use nested
Map<K, Map<K, V>>syntax - No
.at()or overflow checks —Uint<128>is inherently bounded - Access with
[]notation (same as Solidity) - No
deletekeyword — assign zero manually
6. Access Control
Solidity
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function setPrice(uint256 _price) public onlyOwner {
price = _price;
}
Compact
// Access control is inline — no modifier syntax
export circuit setPrice(
caller: Either<ZswapCoinPublicKey, ContractAddress>,
newPrice: Uint<128>
): [] {
assert(caller == _owner, "Not the owner");
_price = newPrice;
}
Key differences:
-
No modifiers — use inline
assert()checks - OpenZeppelin provides
OwnableandAccessControlmodules for reuse -
ShieldedAccessControlprovides privacy-preserving role management - No
msg.sender— identity comes from circuit context or parameters
7. Events & Logging
Solidity
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) public {
// ... transfer logic ...
emit Transfer(msg.sender, to, amount);
}
Compact
// ⚠️ Compact does NOT currently support events!
// This is a known limitation.
//
// Workaround: Use ledger entries to store an action log
export ledger _lastAction: Opaque<"string">;
export circuit transfer(/* ... */): [] {
// ... transfer logic ...
_lastAction = "transfer executed";
}
Key differences:
- No event emission — this is a major current limitation
- Workaround: store action logs in ledger entries or use off-chain indexing
- Future versions plan to add event support
8. Error Handling
Solidity
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient funds");
require(amount > 0, "Zero amount");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
Compact
export circuit withdraw(
account: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
assert(balances[account] >= amount, "Insufficient funds");
assert(amount > 0u128, "Zero amount");
balances[account] = balances[account] - amount;
// No payable/transfer — use token operations instead
}
Key differences:
-
require()→assert() - No
revert()or custom error types - Error messages are strings (same concept)
- No
try/catch— circuits either succeed or fail entirely - No
payable— value transfers handled through ledger operations
9. Data Types
Solidity
uint256 public totalSupply;
address public tokenHolder;
bool public active;
string public name;
struct UserInfo {
address wallet;
uint256 balance;
bool registered;
}
Compact
export ledger totalSupply: Uint<128>;
export ledger tokenHolder: Either<ZswapCoinPublicKey, ContractAddress>;
export ledger active: Bool;
export ledger name: Opaque<"string">;
// Structs as record types
type UserInfo = {
wallet: Either<ZswapCoinPublicKey, ContractAddress>,
balance: Uint<128>,
registered: Bool
};
| Solidity | Compact | Notes |
|---|---|---|
uint256 |
Uint<128> |
Max 128 bits in Compact |
uint8 |
Uint<8> |
Arbitrary bit widths supported |
address |
Either<ZswapCoinPublicKey, ContractAddress> |
Two address types |
bool |
Bool |
Capitalized |
string |
Opaque<"string"> |
Wrapped in Opaque |
bytes32 |
Bytes<32> |
Fixed-size byte arrays |
struct |
{ field: Type } |
Record type syntax |
enum |
Not supported | Use constants or integers |
array[] |
Limited support | No dynamic arrays on ledger |
10. Loops & Iteration
Solidity
function batchTransfer(address[] memory recipients, uint256 amount) public {
for (uint i = 0; i < recipients.length; i++) {
balances[msg.sender] -= amount;
balances[recipients[i]] += amount;
}
}
Compact
// ⚠️ Loops are generally avoided in Compact circuits
// Each iteration adds to the ZK circuit size (fixed at compile time)
//
// Preferred pattern: Process a fixed number of items
export circuit batchTransfer2(
sender: Either<ZswapCoinPublicKey, ContractAddress>,
recipient1: Either<ZswapCoinPublicKey, ContractAddress>,
recipient2: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
assert(_balances[sender] >= amount * 2u128, "Insufficient balance");
_balances[sender] = _balances[sender] - amount * 2u128;
_balances[recipient1] = _balances[recipient1] + amount;
_balances[recipient2] = _balances[recipient2] + amount;
}
Key differences:
- Dynamic loops are problematic — each iteration increases circuit size
- Prefer fixed-size operations or unrolled patterns
- No
whileloops in practice - Circuit size must be determinable at compile time
Bonus: Privacy Features (Compact-Specific)
Compact introduces concepts that have no direct Solidity equivalent:
Shielded State
// Data that's private by default
// Only the owner can see the actual value
// Others can verify correctness without seeing the data
// Witness circuits — provide private inputs
export circuit proveOwnership(
token_id: Uint<128>,
witness: OwnershipWitness // Private input, not visible on-chain
): Bool {
// Returns true/false without revealing who owns what
}
Selective Disclosure
// Solidity: Everything is public
uint256 public salary; // Anyone can read
// Compact: Prove properties without revealing values
// e.g., "My salary is > $50,000" without revealing the actual amount
Quick Reference Table
| Concept | Solidity | Compact |
|---|---|---|
| Contract/Module | contract Foo {} |
module Foo {} |
| Function | function foo() public |
export circuit foo(): [] |
| State variable | uint256 public x |
export ledger x: Uint<128> |
| Mapping | mapping(K => V) |
Map<K, V> |
| Address | address |
Either<ZswapCoinPublicKey, ContractAddress> |
| Error check | require(cond, msg) |
assert(cond, msg) |
| Events | event Transfer(...) |
❌ Not supported yet |
| Modifiers | modifier onlyOwner() |
Inline assert()
|
| Loops | for (uint i...) |
Avoid; use fixed-size ops |
| Max integer | uint256 |
Uint<128> |
| Constructor | constructor() |
export circuit constructor() |
| Import | import "./Foo.sol" |
import "./Foo" prefix Foo_ |
Next Steps
- Try it yourself: Pick a simple Solidity contract and try converting it to Compact
- Explore OpenZeppelin Compact Contracts: github.com/OpenZeppelin/compact-contracts
- Read the Midnight docs: docs.midnight.network
- Join the community: Discord | Forum
Written by @zhaog100 for the Midnight Network bounty program.
Top comments (0)