æternity Blockchain
Sophia & FATE

Sophia & FATE

Sophia is æternity's smart-contract language: functional, strongly and statically typed, with no mutable global state. FATE (the Fast æternity Transaction Engine) is the virtual machine that executes it. Every Superhero contract is written in Sophia and runs on FATE.

If you are arriving from Ethereum, most of your instincts transfer. The table covers the ones that do not.

Coming from Solidity

SoliditySophiaNote
contract C with a body in bracescontract C = with an indented bodyLayout is significant, like Python or Haskell
public / external functionentrypointOnly entrypoints are callable from outside
internal / private functionfunctionA local function, invisible off-chain
view / purea plain entrypointNon-stateful is the default
state-mutating functionstateful entrypointYou must opt in; see below
payablepayable entrypointContracts are non-payable by default
msg.senderCall.callerThe calling entity, possibly another contract
msg.valueCall.valueCoins transferred in this call
block.numberChain.block_heightHeight of the block including this call
block.timestampChain.timestampUnix time in milliseconds, not seconds
emit Transfer(...)Chain.event(Transfer(...))Events are a datatype, declared once as event
mappingmap(k, v)A first-class value in Sophia, not a storage-only construct
structrecord
enumdatatypeSophia datatypes carry payloads, so they cover tagged unions too
uint256intArbitrary precision on FATE, and signed
addressaddressSame idea, ak_/ct_ encoded off-chain
scattered storage variablesone state record, updated with putThere is no ambient mutable storage
require(cond, "msg")require(cond, "msg")Also abort("msg")
ABIACISee below

Three differences do real work:

No mutable global state. A Sophia contract has exactly one state value. A stateful function reads it and calls put with a new one; there is no assignment to a storage slot anywhere. This is what reduces the surface for reentrancy and state-corruption bugs.

No floating point. Sophia omits it. Every price, fee and curve value in Superhero is an integer in aettos or token-decimals. See AE & Gas.

Types are checked, then checked again. The compiler is strict, and FATE re-checks at runtime: each FATE operation is typed, and a type mismatch raises an exception that reverts the call. A malformed call does not corrupt state; it fails.

Entrypoint annotations

Three orthogonal properties, all opt-in, all visible in the ACI:

AnnotationMeaningOmit it and…
(none)Read-only. Can be dry-run without a transaction.This is the default
statefulMay modify contract state, emit events, or call another contract with a non-zero valueThe compiler rejects any put, any Chain.event, any value-carrying call
payableMay receive coins in the callSpending to the contract fails outright

payable also applies to the contract itself: a contract is non-payable unless declared otherwise, so a plain AE transfer to a ct_ address fails by default.

These map straight onto how you call a contract. A non-stateful entrypoint can be answered by a dry-run with no transaction, no gas and no wallet prompt. That is how Superhero prices a trade before you confirm it. A stateful entrypoint needs a signed contract-call transaction. A payable one also takes an amount option:

// Calling a payable, stateful entrypoint
await factory.create_community(
  collectionId,
  tokenName,
  BigInt(initialBuyTokenDecimals),
  false,                              // is_private
  metaInfoMap,
  { amount: initialBuyPriceAetto },   // the coins this call carries, in aettos
);

ACI: the ABI equivalent

ACI (æternity Contract Interface) is the JSON description of a contract's interface: its name, its type definitions, its event datatype, and every function with argument names, argument types, return type, and the stateful and payable flags. It is what a client needs to encode a call and decode a result, the role Ethereum's ABI plays.

The ACI carries named, typed arguments and the annotations, so a client can tell a read from a write without being told. From the Tipping v3 ACI:

{
  "arguments": [
    { "name": "title",  "type": "string" },
    { "name": "media",  "type": { "list": ["string"] } },
    { "name": "author", "type": "address" },
    { "name": "sig",    "type": "signature" }
  ],
  "name": "post_without_tip_sig",
  "payable": false,
  "returns": "int",
  "stateful": true
}

You can read off that this is a write (stateful: true), that it takes no coins (payable: false), and that sig is a first-class signature type. That is how delegated posting passes an off-chain signature into the contract for Crypto.verify_sig to check.

Generate an ACI

ACI is generated from Sophia source by the compiler:

aesophia_cli --create_json_aci Tipping.aes -o Tipping.aci.json

The same generation is available over HTTP from a compiler service at its /aci endpoint; Superhero's web app uses the hosted compiler at https://v7.compiler.aepps.com for both networks. Calling an already-deployed contract needs only its ACI, not a compiler. The agent skill vendors the JSON files and reads them from disk:

const ACI = JSON.parse(fs.readFileSync('./contracts/FungibleTokenFull.aci.json', 'utf8'));
await Contract.initialize({ aci: ACI, address: tokenAddress, onAccount: aeSdk, onNode: aeSdk.api });

Keep a vendored ACI in step with the deployed contract. A stale ACI does not fail loudly; it encodes arguments against the wrong signature.

The @compiler >= N pragma at the top of a .aes file is a minimum compiler version, not the version the contract was built with.

FATE, briefly

FATE runs directly on æternity's state tree rather than on a general byte-code machine bolted on top of one.

  • Typed instructions. Type errors raise an exception and revert, rather than reinterpreting bytes.
  • A restricted memory model and high-level instructions. Contracts are smaller and use less gas than equivalent EVM contracts.
  • Native access to chain features. AENS names and oracles are reachable from contract code as protocol primitives, not through a precompile.

Related