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.
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.
- Open the live demo.
- Connect a wallet on Ethereum Sepolia.
- Mint basics (IDs 0–2, 15-second cooldown per address) to assemble inputs.
- 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
Cooldown-gated mint of the base layer. Each call mints exactly one token.
Burn one token to receive exactly one of token 0, 1, or 2 (cannot trade into the same id).
burnBatch consumes the recipe and mint issues the composite, both in one transaction.
Forge.burn accepts forged IDs (3–6) only. Basic IDs can still be burned through trade, which burns one token of any ID.
The same flows on narrow and mobile viewports.
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 |
| 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. |
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.
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. |
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]
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 coverageView 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) |
| 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 |
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 |
git clone git@github.com:SiegfriedBz/Forge-DApp.gitDeployment 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 \
--verifyCreate 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- The admin can stop basic mints.
setCoolDownDelayhas 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_IDis 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.
tradedoes 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.
- L2 deployment comparison — Deploy the same
Forge/FTokenpattern to an L2 testnet and compare gas onburnBatch+mintpaths vs Sepolia. - Bounded admin — Cap
setCoolDownDelayand derive recipes fromI_MAX_TOKEN_ID(or fix it at 6). - Per-token or progressive cooldowns — Replace the single
coolDownDelaywith per-id tuning if you want different issuance curves for each basic. - Secondary marketplace — Integrate standard ERC-1155 marketplace flows alongside the on-chain
tradehelper (which only mints basics).
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).




