Skip to main content

Overview

eStoryToken (ESTORY) is the native utility token of the iStory platform. It implements ERC20 with additional features:
  • Supply Cap: 100M tokens max (100,000,000 * 10^18 wei)
  • Burnable: Users can burn their tokens
  • Pausable: Admin can pause transfers in emergencies
  • Permit: Gasless approvals via EIP-2612
  • Access Control: Role-based minting and pausing

Contract Address

const STORY_TOKEN_ADDRESS = "0xf9eDD76B55F58Bf4E8Ae2A90a1D6d8d44dfA74BC";

Token Details

name
string
eStoryToken
symbol
string
ESTORY
decimals
uint8
18 (standard ERC20 decimals)
MAX_SUPPLY
uint256
100,000,000 * 10^18 (100 million tokens)
initialSupply
uint256
1,000,000 * 10^18 (1 million tokens minted to admin on deployment)

Contract Code

contracts/iStoryToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract EStoryToken is ERC20, ERC20Burnable, ERC20Permit, AccessControl, Pausable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    uint256 public constant MAX_SUPPLY = 100_000_000 * 10 ** 18; // 100M tokens

    constructor(address initialAdmin)
        ERC20("eStoryToken", "ESTORY")
        ERC20Permit("eStoryToken")
    {
        _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin);
        _grantRole(MINTER_ROLE, initialAdmin);
        _grantRole(PAUSER_ROLE, initialAdmin);

        // Initial supply to admin for liquidity/rewards pool
        _mint(initialAdmin, 1_000_000 * 10 ** decimals());
    }

    // Called by Backend to reward users for off-chain activity (Likes, Streaks)
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) whenNotPaused {
        require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(to, amount);
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    // Hook required by OZ 5.0 to enforce pause state
    function _update(address from, address to, uint256 value) internal override(ERC20) {
        require(!paused(), "Token transfer while paused");
        super._update(from, to, value);
    }
}

Functions

Standard ERC20

Returns the token balance of owner.
function balanceOf(address owner) view returns (uint256)
owner
address
required
Address to check balance for
balance
uint256
Token balance in wei (18 decimals)
Transfers amount tokens to to.
function transfer(address to, uint256 amount) returns (bool)
to
address
required
Recipient address
amount
uint256
required
Amount in wei (e.g., 10 tokens = 10 * 10^18 wei)
Reverts if paused or if sender has insufficient balance.
Allows spender to transfer up to amount tokens from caller’s account.
function approve(address spender, uint256 amount) returns (bool)
spender
address
required
Address authorized to spend tokens (e.g., StoryProtocol contract)
amount
uint256
required
Maximum amount spender can transfer
Users must approve StoryProtocol before tipping or paying paywalls:
await approve(STORY_PROTOCOL_ADDRESS, parseEther("100"));
Returns remaining allowance for spender from owner.
function allowance(address owner, address spender) view returns (uint256)

Minting (Admin Only)

Mints new tokens to to. Only callable by addresses with MINTER_ROLE.
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) whenNotPaused
to
address
required
Recipient of newly minted tokens
amount
uint256
required
Amount to mint in wei
Supply Cap: Reverts if totalSupply() + amount > MAX_SUPPLY.Use Case: Backend calls this to reward users for off-chain activity (likes, streaks, weekly challenges).

Burning

Burns amount tokens from caller’s balance.
function burn(uint256 amount) external
amount
uint256
required
Amount to burn in wei
Permanently reduces total supply. Irreversible.

Pausing (Admin Only)

Pauses all token transfers. Only callable by PAUSER_ROLE.
function pause() external onlyRole(PAUSER_ROLE)
Emergency use only. All transfers will revert until unpause() is called.
Resumes token transfers after a pause.
function unpause() external onlyRole(PAUSER_ROLE)

Events

event Transfer(address indexed from, address indexed to, uint256 value);
Emitted when tokens are transferred (including mints and burns).
from
address
Sender address (address(0) for mints)
to
address
Recipient address (address(0) for burns)
value
uint256
Amount transferred in wei

Roles

MINTER_ROLE
bytes32
keccak256("MINTER_ROLE") - Can mint new tokens up to MAX_SUPPLY
PAUSER_ROLE
bytes32
keccak256("PAUSER_ROLE") - Can pause/unpause transfers
DEFAULT_ADMIN_ROLE
bytes32
0x00...00 - Can grant/revoke all roles

Usage Examples

Check Balance

import { useEStoryToken } from '@/app/hooks/useEStoryToken';
import { formatEther } from 'viem';

function TokenBalance() {
  const { balance, isLoading } = useEStoryToken();
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      Balance: {formatEther(balance)} ESTORY
    </div>
  );
}

Approve StoryProtocol

Users must approve StoryProtocol before tipping or paying paywalls:
app/hooks/useStoryProtocol.ts
import { useEStoryToken } from '@/app/hooks/useEStoryToken';
import { STORY_PROTOCOL_ADDRESS } from '@/lib/contracts';
import { parseEther } from 'viem';

async function tipCreator() {
  const { approve } = useEStoryToken();
  
  // 1. Approve StoryProtocol to spend tokens
  await approve(STORY_PROTOCOL_ADDRESS, parseEther("10"));
  
  // 2. Call tipCreator on StoryProtocol
  // (handled by StoryProtocol contract)
}

Transfer Tokens

Transfer to Another User
import { useWriteContract } from 'wagmi';
import { STORY_TOKEN_ABI, STORY_TOKEN_ADDRESS } from '@/lib/contracts';
import { parseEther } from 'viem';

const { writeContract } = useWriteContract();

await writeContract({
  address: STORY_TOKEN_ADDRESS,
  abi: STORY_TOKEN_ABI,
  functionName: 'transfer',
  args: [recipientAddress, parseEther("5")] // Send 5 ESTORY
});

Mint Tokens (Backend Only)

Backend API Route
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';
import { STORY_TOKEN_ABI, STORY_TOKEN_ADDRESS } from '@/lib/contracts';

const account = privateKeyToAccount(process.env.MINTER_PRIVATE_KEY);
const walletClient = createWalletClient({
  account,
  chain: baseSepolia,
  transport: http()
});

// Mint 10 ESTORY to user for completing a streak
await walletClient.writeContract({
  address: STORY_TOKEN_ADDRESS,
  abi: STORY_TOKEN_ABI,
  functionName: 'mint',
  args: [userAddress, parseEther("10")]
});

Security Considerations

Approval Best Practices:
  • Never approve type(uint256).max (infinite approval) unless you fully trust the spender contract
  • Revoke approvals after use: approve(spender, 0)
  • Check allowances before approving: allowance(owner, spender)
Pausable Design:The _update() hook prevents all transfers when paused, including:
  • Direct transfer() calls
  • transferFrom() via approved spenders
  • Minting (checked separately in mint())
Burning is NOT blocked when paused (OpenZeppelin design choice).

ABI Reference

lib/contracts.ts
export const STORY_TOKEN_ABI = [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function transfer(address to, uint256 amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "event Transfer(address indexed from, address indexed to, uint256 value)"
] as const;

Next Steps

StoryProtocol

Use ESTORY tokens for tipping and paywalls

StoryNFT

Pay mint fees in ETH to create NFT collections

Build docs developers (and LLMs) love