Contracts: programs on Dznye Chain
Write, deploy and call contracts on Dznye Chain: a JSON language with gas, all-or-nothing calls, tokens, events and contract-to-contract calls, with full examples.
A contract is a small program that lives on the chain. It has its own address, can hold coins and tokens, and keeps its own storage. Nobody holds a key for it: only its code can move what it holds, and its code cannot be changed. That makes contracts good for escrow, crowdfunding, shops, games and markets.
How contracts behave
- Same result everywhere. Contracts use whole numbers only, with no randomness and no outside data, so every computer gets exactly the same answer.
- Metered. Every step costs gas. A call states the most gas it may use, so a program that would run for ever simply stops.
- All or nothing. A call works on a draft. If it fails, nothing it did happens, including coins sent with it.
- No call-backs. A contract that is running cannot be called again until it finishes. This closes the "re-entrancy" hole behind the largest thefts on other chains.
The language
A contract is JSON. It has functions, and each function's body is an expression. An expression is either a value or a list whose first item names an operation, like ["+", 1, 2].
{
"init": ["set", "owner", ["caller"]], // runs once, when deployed (optional)
"functions": {
"lock": { // a function that changes things
"args": ["until"],
"body": ["seq",
["require", [">", ["paid", "DZY"], 0], "send the DZY to lock with this call"],
["set", "owner", ["caller"]],
["set", "until", ["arg", "until"]]
]
},
"unlocks": { "view": true, "body": ["get", "until"] } // a view: free to ask, cannot change anything
}
}
Values
Whole numbers, text, true, false and null. Fractions do not exist: use the smallest unit (1 DZY is 1,000,000). Times are in milliseconds.
Operations
| Group | Operations | Notes |
|---|---|---|
| Maths | + - * / % min max | Whole numbers. / rounds toward zero. Dividing by zero fails. |
| Compare | = != < <= > >= | Give true or false. |
| Logic | and or not | Only true counts as true. |
| Text | str (join), len | ["str","p:",["caller"]] joins into one text. |
| Flow | if seq for | ["for","i",1,10,body] runs up to 1,000 rounds. |
| Names | arg let var | ["let","x",5] then ["var","x"]. |
| Stop | require fail | ["require", test, "message"] stops the call, undoing everything, if the test is not true. |
| The call | caller self now height paid | ["paid","DZY"] is how much of that coin came with this call. now is the block's time. |
| Checks | isaddress balance | ["balance","DZY"] is what the contract holds. Add an address for someone else's. |
| Storage | get has set del | ["get","key",default]. Keys up to 96 characters; texts up to 256. |
| Coins | transfer | ["transfer","DZY",to,amount] sends from the contract. |
| Other contracts | call | ["call",address,"fn",[args],"DZY",amount]: the last two are optional. |
| Tokens | newtoken mint | ["newtoken","SYM","Name",decimals,cap] makes a token the contract issues (it needs 1 DZY of its own). ["mint",asset,to,amount] issues it. |
| Events | emit | ["emit", value] announces something. The newest 50 are kept per contract. |
Gas and fees
| An ordinary step | 1 gas |
| Reading storage | 20 gas |
| Writing storage | 100 gas, plus 1 per character |
| Sending coins | 200 gas |
| Calling another contract | 500 gas |
| Emitting an event | 50 gas, plus 1 per character |
| Making a token | 2,000 gas |
A call asks for a gas limit (10,000 if you say nothing, at most 200,000). It pays the chain's fee once, plus once more for every 1,000 gas of the limit, and unused gas is paid back. If a call fails while running, it still pays for the gas it used, and the failure is written in the contract's events. A call that is badly formed (a missing function, wrong number of arguments) is refused and costs nothing.
Limits
- A program is at most 12,000 characters, with at most 64 functions of at most 16 arguments.
- Expressions nest at most 64 deep. Loops run at most 1,000 rounds.
- Contracts call contracts at most 4 deep, sharing one gas limit and one draft.
- At most 10,000 stored values per contract.
Put a contract on the chain
Putting a contract up costs 1 DZY, paid to the treasury, plus the gas fee. Its address comes from the transaction, and nobody can change its code afterwards.
From the command line
node chain/cli.mjs contract deploy --key me.json --file chain/contracts/vault.json
# Deployed. The contract's address is 9f3c…
From code
import { connect, newKey } from 'https://developers.dznye.com/js/chain/sdk.js';
const dz = connect('https://api.dznye.com');
const vault = await dz.deploy(me, programJson);
Use it
# call a function, sending 5 DZY with it
node chain/cli.mjs contract call --key me.json --contract <address> --fn lock --args '[1900000000000]' --pay 5
# ask a view function (free, nothing changes)
node chain/cli.mjs contract view --contract <address> --fn unlocks
# look at its code, storage, balances and events
node chain/cli.mjs contract show --contract <address>
The command line and the developer kit try a call first and refuse to send one that would fail, so you do not pay gas for a mistake. Wallets and apps can do the same with POST /simulate: see the developer kit.
Ready-made examples
The chain/contracts/ folder has four working contracts you can copy:
- vault.json: coins locked until a time. Not even the owner can take them out early.
- escrow.json: a buyer pays in; the buyer or an arbiter releases to the seller, or the seller or arbiter refunds.
- crowdfund.json: pledges by a deadline. If the goal is met the creator collects; if not, everyone takes their pledge back.
- shop.json: makes its own item token and sells it for DZY until the stock runs out.
A walk-through: a counter
{
"functions": {
"bump": { "body": ["set", "n", ["+", ["get", "n", 0], 1]] },
"count": { "view": true, "body": ["get", "n", 0] }
}
}
Deploy it, call bump three times, then ask count and you get 3. Each bump reads the stored number (default 0), adds one and writes it back.
A walk-through: a shop that makes its own tickets
- Deploy
shop.json. Whoever deploys it is the owner. - Send the shop 1 DZY, which it needs to make its token.
- Call
openwith["TICKET","Concert ticket",100,250000]: symbol, name, stock of 100, and a price of 0.25 DZY. - Anyone calls
buy, sending at least 0.25 DZY with the call, and receives one ticket. When 100 are sold, further buys fail. - The owner calls
collectto take the DZY.
Because the shop made the token itself, nobody else, not even the owner, can issue more tickets than the limit. The chain enforces it.
Good habits
- Always
requirewho may do something, with["=",["caller"],["get","owner"]]. - Update storage before you send coins out.
- Test with simulate first. A contract cannot be edited once deployed, so check it before you deploy.
- Use
emitto tell apps what happened, so they do not have to guess from storage.
Built to be permanent
A contract cannot be deleted or changed after it is deployed. That is what lets people trust it: the rules they read are the rules that run. To release a new version, deploy a new contract and point people to it.