Skip to content

Elysium testnet. Tokens have no value.

DanakeDanake
Build

Integrations

Projects can open Fares from their own scripts, and launchpads can make their tokens eligible by emitting one event shape or by recording cost basis on-chain.

For projects: open a Fare from a script#

The steps are always the same: build the Roll at a past block, approve the budget, call openFare with the root. This script was run against the local development chain and opened a Fare whose Roll then passed pnpm roll verify. It lives in the repository at scripts/examples/open-fare.ts.

import { createPublicClient, createWalletClient, http, parseEther, parseEventLogs, type Address, type PublicClient } from "viem";
import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts";
import { fareMarketAbi, getDeployment, getNetwork, liveTokenAbi } from "@danake/config";
import { buildRoll, formatRollURI } from "@danake/roll";

const token = process.argv[2] as Address;
const network = getNetwork(process.argv.includes("local") ? "local" : "testnet");
const deployment = getDeployment(network.chain.id);
if (!deployment) throw new Error("Contracts are not deployed on this network");
const { FareMarket, LiveToken } = deployment.contracts;

const account =
  network.key === "local"
    ? mnemonicToAccount("test test test test test test test test test test test junk", { addressIndex: 7 })
    : privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const transport = http(network.rpcUrl);
const client = createPublicClient({ chain: network.chain, transport }) as PublicClient;
const wallet = createWalletClient({ chain: network.chain, transport, account });

// 1. Build the Roll at a block in the past. Anyone can rebuild it from public logs.
const snapshot = await client.getBlock({ blockNumber: (await client.getBlockNumber()) - 1n });
const roll = await buildRoll({ client, deployment, network }, { token, snapshotBlock: snapshot.number });
if (!roll.root) throw new Error("No wallet has a proven loss at this snapshot");
console.log(`Roll: ${roll.entries.length} wallets, total loss ${roll.totalLoss} wei, root ${roll.root}`);

// 2. Get a budget. Here: $LIVE, the testnet demo token (1,000,000 per wallet per day).
const budget = parseEther("100000");
const nextMint = await client.readContract({ address: LiveToken, abi: liveTokenAbi, functionName: "nextMintAt", args: [account.address] });
if (nextMint === 0n) {
  await client.waitForTransactionReceipt({
    hash: await wallet.writeContract({ address: LiveToken, abi: liveTokenAbi, functionName: "mint" }),
  });
}
await client.waitForTransactionReceipt({
  hash: await wallet.writeContract({ address: LiveToken, abi: liveTokenAbi, functionName: "approve", args: [FareMarket, budget] }),
});

// 3. Open the Fare. The payer chooses the rules; the market takes 3% at opening.
const hash = await wallet.writeContract({
  address: FareMarket,
  abi: fareMarketAbi,
  functionName: "openFare",
  args: [
    {
      target: token,
      payoutToken: LiveToken,
      budget,
      snapshotBlock: snapshot.number,
      snapshotTime: snapshot.timestamp, // must equal the block's timestamp, or the Roll badge shows a mismatch
      maxRate: parseEther("50000"), // at most 50,000 $LIVE per HYPE lost
      maxLossPerWallet: parseEther("1"), // count at most 1 HYPE of loss per wallet
      window: 3600n, // 1 hour of crossing after the dispute period
      vesting: 86_400n, // streamed over 1 day after settlement
      stay: false,
      rollURI: formatRollURI({ minAge: 0n }),
    },
    roll.root,
  ],
});
const receipt = await client.waitForTransactionReceipt({ hash });
const [opened] = parseEventLogs({ abi: fareMarketAbi, eventName: "FareOpened", logs: receipt.logs });
console.log(`Opened Fare No. ${opened!.args.fareId} in tx ${hash}`);

Run it from the repository root:

PRIVATE_KEY=0x... pnpm exec tsx scripts/examples/open-fare.ts <deadToken> --network testnet

Notes:

  • snapshotTime must be the timestamp of snapshotBlock and be in the past. Take both from the same getBlock call.
  • Paying in HYPE. For a WHYPE Fare, skip the approval, set payoutToken to the WHYPE address and pass value: budget to writeContract.
  • Errors are custom errors. viem decodes them with the ABI from @danake/config; for example TargetNotDead means the oracle does not consider the token dead yet.
  • Pending. The Fare can be cancelled during its dispute period with cancel(fareId). The whole budget returns, fee included.
  • Settle is public. After the window, call settle(fareId) from any wallet, then withdrawSurplus(fareId) from the payer.

For launchpads: make your tokens readable by the Roll#

The Roll needs, for every purchase, who bought, how many tokens and how much HYPE was paid, plus the curve's reserves at the snapshot to value a bag. There are two ways to provide it.

Option 1: emit the Trade event shape#

Emit this event from the curve (or pool) on every buy and sell, in the same transaction as the token transfer:

event Trade(
    address indexed trader,
    bool isBuy,
    uint256 hypeAmount,
    uint256 tokenAmount,
    uint256 hypeReserveAfter,
    uint256 tokenReserveAfter,
    uint256 timestamp
);
  • trader is the wallet that receives the tokens on a buy and sends them on a sell.
  • tokenAmount must equal the Transfer value for that trade, so the Roll can match them.
  • The reserves after the trade let anyone rebuild the price at any block, and the value of any bag, from logs alone.
  • If anything else changes the reserves (liquidity removed, migration), emit an event with the reserves after it, as the demo curve does with Rug.

Also emit one creation event with the initial reserves, like CurveCreated on the demo factory. With these, the Roll needs no indexer and no archive node: address-filtered eth_getLogs is enough.

Option 2: write cost basis on-chain#

A launchpad can keep each wallet's cost basis itself, with the same average-cost rules as the Roll:

// on buy:    cost[w] += hypeIn;  (balance tracked by the token)
// on outflow of x from w with balance b: cost[w] -= cost[w] * x / b;
function costOf(address wallet) external view returns (uint256);
function quoteSell(uint256 amount) external view returns (uint256);

Then a loss is costOf(wallet) − quoteSell(balanceOf(wallet)) at any block, readable with a call at that block. This costs some gas on every transfer, and is what would let a later version of the market compute losses on-chain instead of trusting a root.

Dead-token oracle#

To be targeted by Fares, a launchpad's tokens must be dead according to an IDeathOracle:

interface IDeathOracle {
    function isDead(address token) external view returns (bool);
}

The testnet oracle reads lastTradeAt, athPrice and belowSince from the demo curve. A launchpad that exposes the same three values can be supported by the same logic. See Dead-token rules.

Reading the protocol#

  • ABIs and addresses: @danake/config exports the ABIs, the chain definition and getDeployment(chainId).
  • Rolls and proofs: @danake/roll exports buildRoll, proofFor, verifyFareRoll and the pure computeRoll.
  • HTTP: the site serves /api/roll/<token>/<block>, /api/proof/<fareId>/<wallet> and /api/verify/<fareId>. Results for past blocks are cached forever, because they cannot change.
  • Logs: always filter by address. The public RPC caps eth_getLogs at 2,000 blocks without an address filter and at 10,000 logs per query. getLogsPaged in @danake/roll pages adaptively.