Skip to content

About

ERC-1155 crafting case study on Ethereum Sepolia: the Forge contract holds the recipes and cooldowns and is the only minter of FToken; composites burn their inputs and mint in one transaction. Next.js, TypeScript, Solidity, Foundry, wagmi.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

76 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Live Demo CI-Foundry Foundry

Forge — A Web3 Token-Composition Case Study

Forge is an Ethereum Sepolia DApp built around ERC-1155 crafting: mint three base materials (token IDs 0–2) under a per-address cooldown, forge composites (3–6) with a single-transaction burn-then-mint, optionally trade any held id for a basic, or burn a forged id through Forge.burn. Rules live in Forge.sol; balances live in FToken.sol, which only the deployed Forge can mint or burn.

A small learning project from November 2025, on testnet. Its gaps are listed under Known limitations.


Why

ERC-1155 is a practical standard when one contract needs to represent many fungible and semi-fungible ids. A natural follow-up question is how to keep composition rules and supply mutations honest when recipes get non-trivial: who is allowed to mint, and how do you keep users from minting or burning outside the recipes?

Forge is an experiment in that direction. It keeps the ledger (FToken) supply-focused and the rules (Forge) explicit: every supply change routes through Forge, which encodes cooldowns, recipes, trade constraints, and burn permissions. The UI is a single-page app so contracts and UI stay easy to read, reason about, and test together.


Try the Live Demo

  1. Open the live demo.
  2. Connect a wallet on Ethereum Sepolia.
  3. Mint basics (IDs 0–2, 15-second cooldown per address) to assemble inputs.
  4. Forge composites (IDs 3–6) by spending the right recipe, trade any token for exactly one basic (0, 1, or 2), or burn a forged token (3–6 only).

Need testnet ETH? Sepolia Faucet


Features in Action

Minting basic tokens (0–2)

Cooldown-gated mint of the base layer. Each call mints exactly one token.

Minting Tokens

Trading any token for a basic

Burn one token to receive exactly one of token 0, 1, or 2 (cannot trade into the same id).

Trading Tokens

Forging composites (3–6) via atomic burn-mint

burnBatch consumes the recipe and mint issues the composite, both in one transaction.

Forging Tokens

Burning forged tokens

Forge.burn accepts forged IDs (3–6) only. Basic IDs can still be burned through trade, which burns one token of any ID.

Burning Tokens

Mobile experience

The same flows on narrow and mobile viewports.

Mobile Mint


How It Works

Two contracts, one ledger: Forge encodes behavior; FToken stores ERC-1155 balances. Forge deploys FToken in its own constructor and becomes its immutable owner, so only Forge can call mint, burn or burnBatch on the token contract, and that ownership cannot be misconfigured or transferred. FToken has no public mint or burn, so users cannot mint or burn outside the recipes.

Component Role
Forge Mint basics (0–2), forge composites (3–6), burn forged only, trade (any id → one basic), owner-only cooldown tuning
FToken ERC-1155 ledger; supply mutations restricted to the owning Forge

Design choices

Topic What the code does
Forging in one transaction For IDs 3–6, Forge.mint calls I_TOKEN.burnBatch then I_TOKEN.mint in one transaction.
Cooldown (basics) mapping(address => uint256) userCoolDownTimer gates mints for ids 0–2. Default delay is 15 seconds in be/script/Forge_Constants.sol. The timer is written before the mint, so re-entering through onERC1155Received cannot skip it. It is per address, so it slows minting but a new wallet gets around it.
Access control FToken guards mint / burn / burnBatch with a custom check against an immutable I_OWNER (not OpenZeppelin Ownable, and no ownership transfer); the owner is Forge. In Forge, the only admin function, setCoolDownDelay, is limited to the deployer's address.
Trade and burn rules trade requires _tokenIdToBurn != _tokenIdToMint and _tokenIdToMint ∈ {0,1,2}; it does not bound-check the ID it burns, and it allows basic-to-basic swaps with no cooldown. burn rejects basic ids, so 0–2 are not destroyed via that path.
Tests Foundry tests under be/test/ run the real deploy script in setUp and cover mint, forge, burn, trade, cooldowns, admin paths and revert cases, asserting exact error selectors and events.

Event-driven UI

The app uses watchContractEvent (wagmi core) over an Alchemy WebSocket transport (fe/app/_config/wagmi.ts) to subscribe to all four Forge events and invalidate per-token TanStack Query keys without polling. The watchers do not filter by user and read only the first log of each batch, so another user's action can refresh your balances, and a second event in the same batch is missed until the next refresh:

  • Forge__MintToken — invalidates the minted token's balance and retriggers forgeability.
  • Forge__ForgeToken — invalidates the forged token's balance and all burnt input balances, then retriggers forgeability.
  • Forge__BurnToken — invalidates the burnt token's balance.
  • Forge__Trade — invalidates both the burnt and minted token balances, then retriggers forgeability.

See fe/app/_hooks/_events/use-mint-events.ts and the sibling hooks in the same folder.

Token catalog and recipes

Seven token IDs (0–6). Base layer 0–2 mint with cooldown; composite 3–6 require the exact burn recipe below (names match the UI copy in fe/app/_data/tokens.ts).

Token IDs Name (UI) Rule
0, 1, 2 Iron Ore, Elemental Essence, Crystal Shards Mint via Forge.mint (15s cooldown per address, one token per call).
3 Steel Ingot Burn 1×0 + 1×1.
4 Enchanted Crystal Burn 1×1 + 1×2.
5 Reinforced Crystal Burn 1×0 + 1×2.
6 Legendary Core Burn 1×0 + 1×1 + 1×2.

Forging flow

graph TD
    ironOre[IronOre_id0] --> forge3[Forge_mint_id3]
    elementalEssence[ElementalEssence_id1] --> forge3
    forge3 -->|"burnBatch_then_mint"| steelIngot[SteelIngot_id3]

    elementalEssence --> forge4[Forge_mint_id4]
    crystalShards[CrystalShards_id2] --> forge4
    forge4 -->|"burnBatch_then_mint"| enchantedCrystal[EnchantedCrystal_id4]

    ironOre --> forge5[Forge_mint_id5]
    crystalShards --> forge5
    forge5 -->|"burnBatch_then_mint"| reinforcedCrystal[ReinforcedCrystal_id5]

    ironOre --> forge6[Forge_mint_id6]
    elementalEssence --> forge6
    crystalShards --> forge6
    forge6 -->|"burnBatch_then_mint"| legendaryCore[LegendaryCore_id6]
Loading

Testing

Smart-contract tests live under be/test/ (ForgeTest.t.sol, FTokenTest.t.sol) and run with Foundry. The suite is 38 tests covering deployment, basic mint + cooldown, all forge recipes, insufficient-balance reverts, burn/trade rules, getForgeData, and owner-only setCoolDownDelay.

The tests need no environment variables or secrets: setUp derives a throwaway deployer key with makeAddrAndKey and hands it to the real deploy script through PRIVATE_KEY, so CI runs without GitHub secrets.

Coverage (local run): forge coverage reported 100% of lines, statements, branches and functions on the two contracts and the two deploy scripts (table below). That is 38 unit tests, with no fuzz or invariant tests, and trade has a single happy-path test. CI runs forge test -vvv on every push and PR to main / develop; it does not run forge coverage (.github/workflows/foundry-tests.yml).

cd be && forge install
cd be && forge test --gas-report
cd be && forge coverage
View the local coverage report
File Lines Statements Branches Funcs
script/FTokenScript.s.sol 100% (6/6) 100% (5/5) N/A (0/0) 100% (1/1)
script/ForgeScript.s.sol 100% (6/6) 100% (5/5) N/A (0/0) 100% (1/1)
src/FToken.sol 100% (16/16) 100% (11/11) 100% (3/3) 100% (7/7)
src/Forge.sol 100% (67/67) 100% (78/78) 100% (18/18) 100% (8/8)
Total 100% (95/95) 100% (99/99) 100% (21/21) 100% (17/17)

Tech Stack

Layer Technologies
Smart contracts Solidity ^0.8.13, Foundry (build, test, script), OpenZeppelin ERC-1155
Frontend Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4, shadcn/ui (Radix)
Web3 client wagmi v2, viem, RainbowKit, Alchemy HTTP + WebSocket
State and forms TanStack Query v5, React Context (fe/app/_context/tokens-provider.tsx), React Hook Form + Zod
Metadata The contract's uri points to an IPFS base URI set in be/script/Forge_Constants.sol, with metadata files 0–6 (JSON, no extension) under ipfs-data/metadata/. The app itself loads token images from fe/public/tokens/.
Quality Foundry tests, ESLint, GitHub Actions

Setup (deploy and run locally)

Deployed addresses (Sepolia) — matches the live frontend

These are the addresses wired into fe/app/_contracts/forge-contract-config.ts and fe/app/_contracts/ftoken-contract-config.ts. After a new deploy, update those files (and this table) so the hosted app and docs stay aligned.

Contract Address
Forge 0x7d8A16168D337B2241fCbA1cc5bd196479DF1F0C
FToken (ERC-1155) 0xa6D68eDA0993364481C2c5DA8d6cd43e03f592bA

Clone

git clone git@github.com:SiegfriedBz/Forge-DApp.git

Contracts

Deployment constants (TOKEN_URI, MAX_TOKEN_ID, COOL_DOWN_DELAY) live in be/script/Forge_Constants.sol.

Create be/.env:

ALCHEMY_SEPOLIA_RPC_URL=
ETHERSCAN_API_KEY=
PRIVATE_KEY=

Deploy and verify (ForgeScript deploys Forge, which constructs FToken — no separate FTokenScript needed for the main app path):

cd be
forge script script/ForgeScript.s.sol \
  --rpc-url $ALCHEMY_SEPOLIA_RPC_URL \
  --broadcast \
  --verify

Frontend

Create fe/.env (see fe/.env.example):

NEXT_PUBLIC_ETH_SEPOLIA_ALCHEMY_HTTP_URL=
NEXT_PUBLIC_ETH_SEPOLIA_ALCHEMY_WS_URL=
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=

After deploying, sync ABIs and addresses in fe/app/_contracts/ from your broadcast output, then:

cd fe
pnpm install
pnpm dev

Known limitations

  • The admin can stop basic mints. setCoolDownDelay has no bounds and the owner is a single EOA: a huge value overflows the timer so every basic mint reverts; 0 disables the cooldown.
  • Recipes are hard-coded. I_MAX_TOKEN_ID is a constructor parameter, but recipes stop at 6. A deploy with a higher maximum would let ID 7 mint for free (the live deploy uses 6).
  • Trade. trade does not bound-check the ID it burns and allows basic-to-basic swaps with no cooldown.
  • The cooldown is per address, so it is not Sybil-resistant. The UI countdown uses the browser clock and never reads userCoolDownTimer.
  • Event watchers do not filter by user and read only the first log.
  • No fuzz or invariant tests.

Possible extensions

  • L2 deployment comparison — Deploy the same Forge/FToken pattern to an L2 testnet and compare gas on burnBatch + mint paths vs Sepolia.
  • Bounded admin — Cap setCoolDownDelay and derive recipes from I_MAX_TOKEN_ID (or fix it at 6).
  • Per-token or progressive cooldowns — Replace the single coolDownDelay with per-id tuning if you want different issuance curves for each basic.
  • Secondary marketplace — Integrate standard ERC-1155 marketplace flows alongside the on-chain trade helper (which only mints basics).

Author

Siegfried Bozza · M.Sc / M.Eng · Full-stack engineer & Web3 builder.

Forge was built solo in November 2025, alongside a full-time full-stack role (frontend, contracts, tests, deployment).

LinkedIn

About

ERC-1155 crafting case study on Ethereum Sepolia: the Forge contract holds the recipes and cooldowns and is the only minter of FToken; composites burn their inputs and mint in one transaction. Next.js, TypeScript, Solidity, Foundry, wagmi.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages