Skip to main content

Cardano for Ethereum Developers

Coming from Ethereum, Cardano will look and feel different: different account model, different smart contract paradigm, different tooling. This page maps the key differences, translates the Solidity habits that carry over (and the ones that don't), and ends with concrete next steps for building.

What Makes Cardano Different from Ethereum?​

Account Model​

When developing on Cardano, the most significant difference you will encounter is the account model design. Understanding why Cardano's model was designed differently helps make sense of everything else.

Unlike Ethereum, Cardano is designed around the Extended UTxO (eUTxO) model rather than an account-based model. On Ethereum, each address maintains a balance stored in global state. Transactions update these balances directly, and smart contracts hold and modify their own storage.

eUTxO vs Account Model

On Cardano, there is no global mutable contract storage like on Ethereum. Instead, both value and application state are carried in discrete Unspent Transaction Outputs (UTxOs). While the ledger itself maintains global state (such as the UTxO set, protocol parameters, and staking state), smart contracts do not read from or write to shared storage. Everything a validator is allowed to reason about must be provided explicitly by the transaction through its inputs and outputs.

A helpful mental model is to think of UTxOs like physical bills rather than a single bank balance. Your wallet doesn’t hold “100 ADA” as one number; it holds individual UTxOs whose values sum to your balance. When you spend, you consume entire UTxOs and create new ones as outputs. For example, if you have a single 100 ADA UTxO and want to send 10 ADA, the transaction consumes the 100 ADA UTxO and creates two new ones: one with 10 ADA for the recipient and one with 90 ADA as change.

Smart contracts on Cardano follow the same model. Validators (Cardano's term for smart contracts) have no internal, mutable storage. There is no state living inside a contract. Instead, application state lives in datums, which are arbitrary data attached to UTxOs, similar to a struct stored alongside the UTxO's value. A validator's job is not to update state, but to check that a transaction correctly transforms one set of UTxOs (and their datums) into the next.

Counter Example​

To make the contrast concrete, here is a counter implemented in Solidity on Ethereum, followed by the same counter implemented in Aiken on Cardano.

Ethereum Counter Smart Contract​

contract Counter {
uint256 private count;

function increment() public {
count += 1;
}

function decrement() public {
count -= 1;
}

function getCount() public view returns (uint256) {
return count;
}
}

In Solidity, count is stored directly in the contract's storage and modified in place. The contract itself owns this state. When increment() or decrement() is called, the contract executes code that mutates its internal storage as part of transaction execution.

Cardano Counter in Aiken​

On Cardano, the mental model is fundamentally different. There is no contract-owned storage that gets updated by executing code. Instead, state (the counter value) lives in a datum attached to a UTxO. The validator does not perform the state change; it only validates that a proposed state transition encoded in the transaction is correct.

In other words:

  • Off-chain code constructs a transaction describing the desired state change
  • On-chain code (the validator) verifies that the transaction follows the rules

First, we define the shape of the state we want to track. This is the datum attached to the UTxO holding the counter value:

pub type CounterDatum {
count: Int,
}

We define the set of actions (similar to endpoints of the spending smart contract we will write later). This intent is provided to the validator via a redeemer, which is data supplied by whoever builds the transaction, telling the validator which action to perform:

pub type CounterAction {
Increment
Decrement
}

Building a counter on Cardano means designing valid state transitions. You construct a transaction with the correct inputs (the UTxO holding current state) and outputs (a new UTxO with updated state) that satisfy the validator's rules. When someone wants to increment the counter, they build a transaction that consumes the current state and produces the new state:

The transaction:

  1. Supplies a redeemer specifying the desired action (Increment or Decrement)
  2. Spends the UTxO containing the current counter value in its datum (input)
  3. Creates a new UTxO at the same address with the updated datum (output)

Here's the validator:

use aiken/collection/list
use cardano/transaction.{InlineDatum, OutputReference, Transaction, find_input}

validator counter_validator {
spend(
datum_opt: Option<CounterDatum>,
redeemer: CounterAction,
own_ref: OutputReference,
tx: Transaction,
) {
// --- Input state (current counter) ---
// The state lives in the datum of the UTxO being spent.
expect Some(input_datum) = datum_opt

// --- Output state (next counter) ---
// Exactly one output must go back to this script's address, so the state is
// neither duplicated nor lost. That output carries the next state in its datum.
expect Some(own_input) = find_input(tx.inputs, own_ref)
expect [output] = list.filter(
tx.outputs,
fn(o) { o.address == own_input.output.address },
)
expect InlineDatum(data) = output.datum
expect output_datum: CounterDatum = data

// --- Transition rule ---
// The redeemer tells the validator which transition to check.
when redeemer is {
Increment -> output_datum.count == input_datum.count + 1
Decrement -> output_datum.count == input_datum.count - 1
}
}

else(_) {
// Non-spending purpose (e.g., minting) is not supported by this validator.
fail
}
}

To explore more real-world smart contracts written in Aiken, see the Contract library.

How Do Transactions Work on Cardano?​

A Cardano transaction transforms UTxOs: it spends existing ones and creates new ones.

UTxO Transaction Flow

The key components:

  • Inputs: UTxOs being spent
  • Outputs: New UTxOs being created, each with an address, value, and optionally a datum attached to it
  • Signatures: Authorize spending by verifying that the transaction is signed by the required keys to spend the input UTxOs
  • Redeemers: Data passed to validators when spending UTxOs locked at validator script addresses
  • Validity interval: A time window when the transaction is valid
  • Mint/burn: Token operations, if any

The validity interval deserves special attention. On Ethereum, smart contracts can read block.timestamp during execution. Cardano validators do not have access to a notion of “current time.” Instead, time-based constraints are enforced using the transaction’s declared validity interval.

Every transaction specifies a lower and/or upper bound on when it may be included. The ledger rejects transactions outside this window before any scripts run. Validators can then rely on the declared bounds themselves.

To enforce "action X only after date Y," store Y in the datum and check that the transaction's lower bound ≥ Y.

Another significant difference from Ethereum is composition. On Ethereum, a transaction has a single entry point and composition is typically expressed through internal contract calls (routers, aggregators, multicalls). On Cardano, a single transaction can spend from multiple script addresses and mint tokens under multiple policies. Each validator runs independently; all must pass, and the entire transaction succeeds or fails atomically. No router contracts are required.

How often do validators run?

Validators run per script purpose, not once per transaction:

  • Spending validators run once for each script-locked input being spent
  • Minting policies run once per policy ID (not per asset or per input)
  • Staking and governance scripts run once per withdrawal, certificate, voter, or proposal that requires a script

A single transaction can trigger multiple executions of the same spending validator (with different datums/redeemers), plus minting policy executions, plus staking scripts. This is a key difference from Ethereum, where one contract call means one execution.

In the UTxO model, the transaction itself is the local state. Every input, every output, every signature, every piece of data the validator needs is contained within the transaction. There's no querying external state and as a result there are no surprises from concurrent modifications. When a validator runs, it receives this complete local state as context. For example, the transaction's extra_signatories field lists the key hashes the transaction declares as required signers, and the ledger checks their signatures before any validator runs. The validator can inspect all inputs being spent, all outputs being created, and all metadata attached. Given the same transaction inputs and outputs, a validator will always produce the same result because everything it needs is self-contained.

How Do Fees Work on Cardano?​

Cardano uses a deterministic fee model. Fees are calculated using a fixed formula based on transaction size (a × tx_size + b), the size of any reference scripts in the UTxOs it spends or references, and, for smart contract transactions, known execution budgets (CPU and memory units). Fees are fully calculable before submission, with no gas price auctions or bidding.

One important detail: every UTxO must contain a minimum amount of ADA (called minUTxO). This prevents dust attacks and ensures UTxOs are economically meaningful. The minimum depends on the UTxO's size, so more data in the datum means more ADA required. Client SDKs handle this automatically when building transactions.

Common Patterns​

For a deeper dive into how smart contracts work on Cardano, see the Smart Contracts overview. Below are common patterns you'll encounter when building validators.

Adding Authorization: The onlyOwner Pattern​

Let's extend our counter example to require owner authorization. On Ethereum, you'd use a modifier:

address public owner;

modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}

function increment() public onlyOwner {
count += 1;
}

On Cardano, we would check if the defined owner signed the transaction. The standard library's list.has checks whether a specific key hash appears in tx.extra_signatories, the transaction's required signers (the off-chain code must list the owner there):

use aiken/collection/list
use aiken/crypto.{VerificationKeyHash}
use cardano/transaction.{InlineDatum, OutputReference, Transaction, find_input}

validator counter_with_owner(owner: VerificationKeyHash) {
spend(
datum_opt: Option<CounterDatum>,
redeemer: CounterAction,
own_ref: OutputReference,
tx: Transaction,
) {
// onlyOwner: the owner must be one of the transaction's required signers
expect list.has(tx.extra_signatories, owner)

expect Some(input_datum) = datum_opt
expect Some(own_input) = find_input(tx.inputs, own_ref)
expect [output] = list.filter(
tx.outputs,
fn(o) { o.address == own_input.output.address },
)
expect InlineDatum(data) = output.datum
expect output_datum: CounterDatum = data

when redeemer is {
Increment -> output_datum.count == input_datum.count + 1
Decrement -> output_datum.count == input_datum.count - 1
}
}

else(_) {
fail
}
}

The owner parameter is Cardano's equivalent of Solidity's constructor arguments, but with an important difference: it is applied to the compiled script off-chain and becomes part of the script bytes. Even if two contracts have identical logic, applying different owner verification keys produces different bytecode:

Since the script address is derived by hashing this bytecode, each owner gets their own unique address. This means each owner's counter state lives in UTxOs at a completely separate address, providing isolation between different instances of the same contract logic.

Adding Time Locks​

What if the owner may only act before a certain deadline? On Ethereum, you'd check block.timestamp. On Cardano, we use validity intervals:

use aiken/collection/list
use aiken/crypto.{VerificationKeyHash}
use aiken/interval
use cardano/transaction.{OutputReference, Transaction}

pub type LockDatum {
owner: VerificationKeyHash,
// POSIX timestamp in milliseconds
deadline: Int,
}

validator before_deadline {
spend(
datum_opt: Option<LockDatum>,
_redeemer: Data,
_own_ref: OutputReference,
tx: Transaction,
) {
expect Some(datum) = datum_opt

let is_owner_signed = list.has(tx.extra_signatories, datum.owner)
let is_not_expired = interval.is_entirely_before(
tx.validity_range,
datum.deadline,
)

is_owner_signed && is_not_expired
}

else(_) {
fail
}
}

interval.is_entirely_before checks that the transaction's validity interval ends before the deadline. A transaction with no upper bound fails the check, so the builder must set one.

Native Tokens vs ERC-20/721​

On Ethereum, tokens are smart contracts. Creating an ERC-20 means deploying a contract, and every transfer is a contract call that costs gas. The approve + transferFrom pattern is necessary for dApps to spend your tokens.

On Cardano, native tokens are built into the ledger itself. Remember that UTxOs are the fundamental unit that all network logic applies to. Each UTxO can carry multiple values: ADA plus any number of native tokens. When you spend a UTxO, you're moving all the value it contains in one atomic operation. This means transferring tokens is no different from transferring ADA. There's no approve pattern, no separate contract calls, no extra gas. A single transaction can move ADA and dozens of different tokens across multiple UTxOs, all with the same predictable fee.

Native Scripts​

For simple minting rules, you don't need to write any smart contract code. Cardano has native scripts, a minimal scripting language built into the ledger with six simple constructors: sig (require signature), all (all conditions), any (any condition), atLeast (n-of-m), before (time lock), and after (time lock).

For example, to create a token that requires your signature and can only be minted before a deadline:

const nativeScript: NativeScript = {
type: "all",
scripts: [
{ type: "before", slot: lockSlot.toString() }, // a future slot: minting closes after it
{ type: "sig", keyHash: yourPubKeyHash },
],
};

The ledger validates these rules directly. This covers most basic token use cases: single-owner minting, multisig minting, time-locked minting.

Validators​

When you need complex business logic like conditional minting based on other UTxOs, oracle data, or custom validation, you write a minting policy smart contract:

use aiken/collection/list
use aiken/crypto.{VerificationKeyHash}
use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction}

validator my_token(owner: VerificationKeyHash) {
mint(_redeemer: Data, _policy_id: PolicyId, tx: Transaction) {
list.has(tx.extra_signatories, owner)
}

else(_) {
fail
}
}

This gives you full programmability by allowing you to check transaction inputs/outputs, reference other UTxOs, and enforce arbitrary conditions. Native tokens work just like ADA in transactions. The only difference is that minting and burning require a policy.

Accepting Payment: payable and msg.value​

On Ethereum, a sale is a payable function that checks msg.value:

function buyTicket() external payable {
require(msg.value >= price, "Insufficient payment");
payable(treasury).transfer(msg.value);
_mint(msg.sender, 1);
}

Cardano has no msg.value. The payment is an output of the same transaction, so the minting policy checks that one pays the treasury before a ticket is minted:

use aiken/collection/dict
use aiken/collection/list
use cardano/address.{Address}
use cardano/assets.{PolicyId}
use cardano/transaction.{InlineDatum, Transaction}

validator ticket(treasury: Address, price: Int) {
mint(_redeemer: Data, policy_id: PolicyId, tx: Transaction) {
// Exactly one ticket is minted under this policy
expect [Pair(_name, 1)] = dict.to_pairs(assets.tokens(tx.mint, policy_id))

// require(msg.value >= price): an output pays the treasury, tagged with
// this policy ID so the same payment cannot also cover another sale
list.any(
tx.outputs,
fn(o) {
and {
o.address == treasury,
assets.lovelace_of(o.value) >= price,
o.datum == InlineDatum(policy_id),
}
},
)
}

else(_) {
fail
}
}

The datum tag matters. Without it, one payment could satisfy two ticket policies that share a treasury (two events from the same organizer) in a single transaction, the double satisfaction problem.

Developer Environment​

Ready to start building? Here's the tooling landscape.

Programming Languages​

Ethereum developers primarily use Solidity. On Cardano, currently the most popular language is Aiken: a purpose-built language with Rust-like syntax, strong static typing, and its own toolchain (compiler, test framework, formatter, LSP). Aiken compiles directly to UPLC (Untyped Plutus Core), Cardano's native bytecode.

Other languages target the same bytecode, with Python, Scala, and TypeScript-flavoured options among them. See the Smart Contracts overview for the comparison and Builder Tools for the current set.

Tools​

EthereumCardano
HardhatAiken CLI (aiken build, aiken check)
RemixAiken Playground
Web3.js, ethers.jsClient SDKs
Ganache, FoundryLocal devnets
Infura, AlchemyQuery APIs, listed in Builder Tools
EtherscanExplorers
MetaMaskWallets

Client SDKs​

Client SDKs handle transaction building, wallet integration, UTxO selection, and fee calculation. They're equivalent to ethers.js or web3.js but for Cardano. See the full Client SDKs documentation for detailed guides.

LanguageSDK
TypeScriptTypescript SDKs
PythonPython SDKs
RustRust SDKs
GoGo SDKs
C#C# SDKs

Development Workflow​

  1. Write validators in Aiken (.ak files)
  2. Build with aiken build, which generates plutus.json (the "blueprint")
  3. Test with aiken check (built-in test framework)
  4. Import compiled scripts into your off-chain app
  5. Deploy by sending UTxOs to the script address

Unlike Ethereum, scripts don't require deployment to exist. The script hash determines the address, and the same script always produces the same address. However, you can publish scripts on-chain as reference scripts (CIP-33) so transactions can reference them instead of including the full script code each time, reducing fees.

What's Different with Smart Contract Development?​

The examples showed how the models differ. Here's why those differences matter.

The eUTxO model offers strong guarantees, but those guarantees come with tradeoffs. Understanding these early helps avoid frustration when building more complex applications.

Parallelization​

Because application state is carried in UTxOs rather than shared contract storage, transactions that operate on different UTxOs can often be processed in parallel. There is no single mutable variable that all users must contend over. For example, if 100 users each control their own counter UTxO, all 100 can update their counters simultaneously without blocking one another.

Deterministic Validation and No Reentrancy​

Validators only inspect the transaction they validate: its inputs, outputs, datums, redeemers, signers, and validity range. They do not depend on execution order or shared mutable state, and always evaluate to true or false.

This deterministic model eliminates reentrancy-style vulnerabilities common in account-based systems. The classic reentrancy pattern, where a contract is re-entered mid-execution while mutating shared storage, does not apply. Each UTxO is consumed atomically, and validators cannot be re-invoked during execution. Because validator behavior is predictable from the transaction itself, contracts are easier to test and formally verify.

More Off-chain Complexity​

On Ethereum, much of the application logic lives inside the contract itself. On Cardano, the validator only verifies correctness of the state transition. The work of constructing the correct state transition happens off-chain. This is what Client SDKs handle:

But Client SDKs are just one piece of the puzzle. A typical Cardano dApp is not just a smart contract. It combines off-chain components like transaction construction, state management, and indexers with on-chain validators:

Typical Cardano DApp Architecture

Off-chain components handle state tracking, transaction building, UTxO selection, mempool monitoring, and blockchain indexing. On-chain validators are lean and only verify that transactions follow the rules.

In practice, Cardano development intentionally shifts complexity from on-chain to off-chain correctness.

State Management​

There is no implicit “current state” stored in a contract. State must be modeled explicitly using UTxOs and datums. Patterns like counters, registries, or mappings require designing how state is split across UTxOs and how those UTxOs evolve over time. This makes state transitions explicit and auditable, but it can feel verbose compared to mutating a variable in contract storage on Ethereum. A UTxO at a script address also proves nothing on its own, since anyone can create one there with any datum. Protocols mark their genuine state UTxO with a unique token whose minting policy checks the initial state; see Missing UTxO authentication.

Concurrency Requires Design​

The eUTxO model enables parallelism, but requires care to avoid contention. If many users need to update a single piece of state (for example, a single global counter), they will contend for the same UTxO:

Avoiding contention requires architectural patterns such as sharded state, per-user UTxOs, or batching. These are design decisions with their own tradeoffs.

No Contract Calls​

Ethereum contracts can call other contracts. Cardano validators do not call other validators. Instead, you compose multiple validations within a single transaction. All referenced validators run independently and must pass for the transaction to succeed.

No Mapping Type​

Ethereum's mapping(address => uint) has no direct equivalent. Instead, you use the UTxO pattern: create one UTxO per entry, with the datum containing the key and value. To look up an entry, query for UTxOs at the script address with matching key in datum. This is more parallelizable since multiple users can update their entries simultaneously without contention.

Smart Contract Security​

The eUTxO model has its own security considerations that differ from account-based systems. Smart Contract Vulnerabilities serves as a reference for common issues and mitigations.

Example: Double Satisfaction

Since validators run independently for each input and all see the same transaction outputs, a careless validator can be "satisfied" multiple times by the same output:

Both validators ask "is there an output paying 5 ADA to the seller?" Both see the same output and pass. The attacker claims 20 tokens but only pays 5 ADA instead of 10. The fix is tagging outputs uniquely so each validator looks for its specific output.

This is just one of many eUTxO-specific patterns. The documentation covers these vulnerabilities, and you can practice exploiting them in the Cardano CTF.

Quick Reference: Ethereum to Cardano​

A few Solidity habits carry over, but the ones that matter are not one-to-one:

  • There is no contract storage. Solidity keeps mutable state inside the contract; Cardano attaches immutable data (a datum) to a UTxO. You never update a datum in place, you consume the UTxO and create a new one with the new value, so a mapping(addr => uint) becomes one UTxO per entry rather than a single mutable map.
  • There is no msg.sender or msg.value. A validator inspects the transaction itself: check tx.extra_signatories for who signed, and read input and output values explicitly.
  • Logic moves from runtime to build time. A constructor becomes a parameterized script whose parameters are applied before it is used. require(condition) becomes Aiken's expect, and modifier onlyOwner becomes an explicit list.has(tx.extra_signatories, owner) check.
  • The interface is a blueprint, not an ABI. Tools read plutus.json the way they read an ABI. Instead of view functions and events, you query UTxOs directly through a provider and use transaction metadata or an indexer for event-style history.

Next steps​

  • Start Building: Module 2 of the curriculum. Pick your tools, get test ADA, and send your first transaction with the same SDKs used throughout.
  • eUTXO: the model behind every difference on this page, if you jumped straight here.
  • Write a validator: hands-on Aiken when you reach Module 5; aiken-lang.org and the standard library pair well with it.