# Zoe Overview

# What is Zoe?

Zoe is Agoric's smart contract framework. Use Zoe to:

  • Run your code on-chain
  • Mint new digital assets
  • Credibly trade assets

# Why Use Zoe?

# For Users

Zoe is safer. Traditionally, putting digital assets in a smart contract has carried the risk of losing them. But Zoe guarantees you get either what you wanted or a full refund of the assets you put in. You will never leave a smart contract empty-handed, even if it is buggy or malicious.

# For Developers

Zoe is easier. Traditionally, writing a smart contract meant learning a new, untried language. And don't make any mistakes - if you do, your users might lose millions.

However, you write Zoe contracts in a secure subset of JavaScript. Moreover, Zoe automatically escrows all user digital assets and handles their subsequent payout. Even a buggy contract can't cause users to lose their assets.

# Contracts on Zoe

Agoric has written a number of example contracts that you can use, including:

# Using an Example Zoe Smart Contract

You must have a Zoe invitation to a specific contract instance to join and participate in it. Let's imagine your friend Alice has sent an invitation for a contract instance to your wallet.

Compare this to a smart contract on Ethereum. On Ethereum, the smart contract developer must guard against malicious calls and store an internal access control list to check whether the message sender is allowed to send such a message. Zoe, built on Agoric's object capability security model, is just easier.

This particular invitation is for an Atomic Swap contract. In an Atomic Swap, one party puts up digital assets they want to exchange and sends an invitation to a second party for them to possibly complete the exchange. In this example, Alice has already escrowed the assets she wants to swap and is asking you to pay a specified price to receive her digital assets.

# Inspecting an Invitation

So you have an invitation, but how do you use it? First, you use Zoe to inspect and validate the invitation.

const invitationDetails = await E(zoe).getInvitationDetails(invitation);
const { installation, customDetails } = invitationDetails;
assert(typeof customDetails === 'object');
const { asset, price } = customDetails;


E() is part of the Agoric platform and is used to call methods on remote objects and receive a promise for the result. Code on the Agoric platform is put in separate environments, called vats, for security. Zoe is a remote object in its own vat, so we must use E().

Invitations include information about their contract's installation. Essentially, this is the contract's source code as installed on Zoe. From this overall contract installation, people use Zoe to create and run specific instances of the contract. For example, if a real estate company has a contract for selling a house, they would create an instance of the contract for each individual house they have up for sale.

You use object identity comparison to quickly check that you recognize this contract installation, without having to compare source code line-by-line. If the installation matches, you're sure the invitation is for participating in an instance of the expected contract rather than an unknown and possibly malicious one.

  const isCorrectCode = installation === atomicSwapInstallation;

However, if you don't recognize the installation, you can inspect its code directly by calling:

  const bundledCode = await E(installation).getBundle();

In most cases, the bundle contains a base64-encoded zip file that you can extract for review:

echo "$endoZipBase64" | base64 -d > bundle.zip
unzip bundle.zip

Contracts can add their own specific information to invitations. In this case, the Atomic Swap contract adds information about what is being traded: the asset amount Alice has escrowed, and the price amount that you must pay to get it. Note that both are descriptions of digital assets with no intrinsic value of their own.

# Making an Offer

You've successfully checked out the invitation, so now you can make an offer.

An offer has three required parts:

  • a Zoe invitation
  • a proposal
  • a payment containing the digital assets you're offering to swap

The proposal states what you want from the offer, and what you will give in return. Zoe uses the proposal as an invariant to ensure you don't lose your assets in the trade. This invariant is known as offer safety.

You use the invitation's asset and price amounts to make your proposal. Let's say asset is an amount of 3 Moola, and price is an amount of 7 Simoleans (Moola and Simoleans are made-up currencies for this example).

const proposal = {
  want: { Asset: asset }, // asset: 3 Moola
  give: { Price: price }, // price: 7 Simoleans

Proposals must use Keywords, which are identifier (opens new window) properties that start with an upper case letter and contain no non-ASCII characters. Here, the specific keywords, Asset and Price, are determined by the contract code.

You said you would give 7 Simoleans, so you must send 7 Simoleans as a payment. You happen to have some Simoleans lying around in a Simolean purse (used to hold digital assets of a specific type). You withdraw a payment of 7 Simoleans from the purse for your offer, and construct an object using the same Keyword as your proposal.give:

const simoleanPayment = await E(purse).withdraw(price);
const payments = { Price: simoleanPayment };

Now you need to harden (opens new window) your just created proposal and payments objects. Hardening is transitively freezing an object. For security reasons, we must harden any objects that will be passed to a remote object like Zoe.


You've put the required pieces together, so now you can make an offer:

  const userSeat = await E(zoe).offer(invitation, proposal, payments);

At this point, Zoe confirms your invitation's validity and burns it. Zoe also escrows your payments, representing their value as amounts in your Allocation in the contract.

Troubleshooting missing brands in offers

If you see...

Error#1: key Object [Alleged: IST brand] {} not found in collection brandToIssuerRecord

then it may be that your offer uses brands that are not known to the contract. Use E(zoe).getTerms() to find out what issuers are known to the contract.

If you're writing or instantiating the contract, you can tell the contract about issuers when you are creating an instance or by using zcf.saveIssuer().

# Using Your UserSeat

Making an offer as a user returns a UserSeat representing your position in the ongoing contract instance (your "seat at the table"). You can use this seat to:

  1. Exit the contract.
  2. Get information about your position such as your current allocation.
  3. Get your payouts from Zoe.

Check that your offer was successful:

  const offerResult = await E(userSeat).getOfferResult();

In response to your offer, the atomicSwap contract returns the message: "The offer has been accepted. Once the contract has been completed, please check your payout." Other contracts and offers may return something different. The offer's result is entirely up to the contract.

# Getting Payouts

The atomicSwap contract of this example is over once the second party escrows the correct assets. You can get your payout of Moola with the Keyword you used ('Asset'):

  const moolaPayment = await E(userSeat).getPayout('Asset');

Alice also receives her payouts:

  const aliceSimoleanPayment = await E(aliceSeat).getPayout('Price');

# Writing and Installing a Contract

Now that you've seen how to participate in a contract instance, let's look at how you'd create a contract and its instances.

Let's pretend Alice wrote that contract from scratch, even though atomicSwap is one of Agoric's example contracts (see Atomic Swap). Note: All Zoe contracts must have this format:

Show contract format
// @ts-check
// Checks the types as defined in JSDoc comments

// Add imports here

// Optional: you may wish to use the Zoe helpers in
// @agoric/zoe/src/contractSupport/index.js
import { swap as _ } from '@agoric/zoe/src/contractSupport/index.js';

// Import the Zoe types
import '@agoric/zoe/exported.js';

 * [Contract Description Here]
 * @type {ContractStartFn}
const start = (zcf, _privateArgs) => {
  // ZCF: the Zoe Contract Facet

  // privateArgs: any arguments to be made available to the contract
  // code by the contract owner that should not be in the public
  // terms.

  // Add contract logic here, including the
  // handling of offers and the making of invitations.

  // Example: This is an example of an offerHandler
  // which just gives a refund payout automatically.
  const myOfferHandler = zcfSeat => {
    const offerResult = 'success';
    return offerResult;

  // Example: This is an invitation that, if used to make
  // an offer will trigger `myOfferHandler`, giving a
  // refund automatically.
  const invitation = zcf.makeInvitation(myOfferHandler, 'myInvitation');

  // Optional: Methods added to this object are available
  // to the creator of the instance.
  const creatorFacet = {};

  // Optional: Methods added to this object are available
  // to anyone who knows about the contract instance.
  // Price queries and other information requests can go here.
  const publicFacet = {};

  return harden({
    creatorInvitation: invitation, // optional
    creatorFacet, // optional
    publicFacet, // optional

export { start };

Alice fills in this code template with atomicSwap's particulars. To install this particular code, Alice first must bundle it off-chain, meaning the code and its imports are flattened together:

import bundleSource from '@endo/bundle-source';
const atomicSwapUrl = await importMetaResolve(
const atomicSwapPath = url.fileURLToPath(atomicSwapUrl);
const atomicSwapBundle = await bundleSource(atomicSwapPath);

Then Alice must install it on Zoe:

  const atomicSwapInstallation = await E(zoe).install(atomicSwapBundle);

The return value is an installation, which we saw earlier. It is an object identifying a particular piece of code installed on Zoe. It can be compared to other installations, and you can call E(atomicSwapInstallation).getBundle() to see the code itself.

# Creating an Instance

Now Alice uses the installation to create a new instance. She must also tell Zoe about the ERTP issuers she wants to use, by specifying their role with Keywords. Alice was escrowing Moola, so she uses the keyword Asset to label the moolaIssuer. She wanted Simoleans, so she uses the keyword Price to label the simoleanIssuer.

const issuerKeywordRecord = harden({
  Asset: moolaKit.issuer,
  Price: simoleanKit.issuer,
const { creatorInvitation } = await E(zoe).startInstance(

Even the creator of a contract instance needs an invitation to participate in it. Alice uses the returned creatorInvitation to make an offer, from which she gets an invitation that can be sent to the counter-party.

const aliceSeat = await E(zoe).offer(
const invitation = await E(aliceSeat).getOfferResult();

# Zoe's Two Sides: Zoe Service and Zoe Contract Facet (ZCF)

You may have noticed the contract code's start method has a zcf parameter. This is the Zoe Contract Facet. Zoe has two sides: the Zoe Service, which you've seen users interact with, and the Zoe Contract Facet (ZCF), which is accessible to the contract code. Note that users have access to the Zoe Service, but do not have access to ZCF. Contract code has access to ZCF and can get access to the Zoe Service.

To learn more about the Zoe Service, Zoe Contract Facet, and Zoe Helper APIs, see our Zoe API documentation.