# 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:
- an Automated Market Maker (AMM) implementation
- a covered call option contract
- an OTC Desk market maker contract
- contracts for minting fungible and non-fungible tokens
# 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;
Note
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.
harden(proposal);
harden(payments);
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:
- Exit the contract.
- Get information about your position such as your current allocation.
- 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 => {
zcfSeat.exit();
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
});
};
harden(start);
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(
'@agoric/zoe/src/contracts/atomicSwap.js',
import.meta.url,
);
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(
atomicSwapInstallation,
issuerKeywordRecord,
);
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(
creatorInvitation,
aliceProposal,
alicePayments,
);
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.