Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ Two independent Solidity contracts are deployed per chain:

### `TricksforBoosterNFT`

ERC-721 NFT collection (OpenZeppelin). OpenSea-compatible: supports `tokenURI`, `contractURI`,
and ERC-2981 royalties. Minting is role-gated (`MINTER_ROLE`), administration uses delayed
ERC-721 NFT collection (OpenZeppelin) with ERC-721 Enumerable support for `totalSupply`,
`tokenByIndex`, and `tokenOfOwnerByIndex` reads. OpenSea-compatible: supports `tokenURI`,
`contractURI`, and ERC-2981 royalties. Minting is role-gated (`MINTER_ROLE`), administration uses delayed
`AccessControlDefaultAdminRules`, and minting can be paused by the default admin. The documented
collection policy is 600 tokens across three game themes (Coin, Dice, Rock
Paper Scissors) — a project specification constraint, not an on-chain enforced supply cap.

Enumeration follows current on-chain custody: while a token is staked it is enumerated under the
staking contract, not the original wallet. Frontends should combine wallet enumeration with
`getWalletStakedTokens(wallet)` when presenting both unstaked and staked Boosters.

### `TricksforBoosterStaking`

Custodial staking contract. Players transfer their Booster NFTs into this contract to stake them.
Expand Down

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@
"linkReferences": {},
"deployedLinkReferences": {},
"immutableReferences": {
"9788": [
"10252": [
{
"length": 32,
"start": 670
Expand Down Expand Up @@ -578,5 +578,5 @@
]
},
"inputSourceName": "contracts/TricksforBoosterStaking.sol",
"buildInfoId": "solc-0_8_26-2940a0bf7433fbb858475891fd4d03553b51db3322b46d171911ad73848093b6"
"buildInfoId": "solc-0_8_26-984e3da0ea7a4cb6316d037d7053b51a1bbe4566d9a30d0f82f96c5a0d9ed7ee"
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"linkReferences": {},
"deployedLinkReferences": {},
"immutableReferences": {
"10388": [
"10852": [
{
"length": 32,
"start": 261
Expand All @@ -125,5 +125,5 @@
]
},
"inputSourceName": "contracts/test/ReentrantERC721.sol",
"buildInfoId": "solc-0_8_26-2940a0bf7433fbb858475891fd4d03553b51db3322b46d171911ad73848093b6"
"buildInfoId": "solc-0_8_26-984e3da0ea7a4cb6316d037d7053b51a1bbe4566d9a30d0f82f96c5a0d9ed7ee"
}
30 changes: 27 additions & 3 deletions contracts/TricksforBoosterNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

/// @title TricksforBoosterNFT
/// @notice ERC-721 NFT collection for Tricksfor Booster items.
/// @notice ERC-721 Enumerable NFT collection for Tricksfor Booster items.
/// OpenSea-compatible: supports tokenURI, contractURI, and ERC-2981 royalties.
/// Minting is role-gated (MINTER_ROLE) and can be paused by the default admin.
/// @dev Does not include staking logic — staking is handled by TricksforBoosterStaking.
contract TricksforBoosterNFT is ERC721, ERC2981, AccessControlDefaultAdminRules, Pausable {
contract TricksforBoosterNFT is
ERC721,
ERC721Enumerable,
ERC2981,
AccessControlDefaultAdminRules,
Pausable
{
// -------------------------------------------------------------------------
// Roles
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -148,6 +155,23 @@ contract TricksforBoosterNFT is ERC721, ERC2981, AccessControlDefaultAdminRules,
return _baseTokenURI;
}

/// @dev Keeps ERC-721 ownership and enumerable indexes synchronized on mint and transfer.
function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}

/// @dev Resolves the balance hook shared by ERC721 and ERC721Enumerable.
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}

// -------------------------------------------------------------------------
// Interface support
// -------------------------------------------------------------------------
Expand All @@ -156,7 +180,7 @@ contract TricksforBoosterNFT is ERC721, ERC2981, AccessControlDefaultAdminRules,
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC2981, AccessControlDefaultAdminRules)
override(ERC721, ERC721Enumerable, ERC2981, AccessControlDefaultAdminRules)
returns (bool)
{
return super.supportsInterface(interfaceId);
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ This index provides a single entry point to all implementation guidance, referen

| Document | Description |
|---|---|
| [OpenSea Readiness Checklist](opensea-readiness-checklist.md) | Deployment-ready checklist covering ERC-721 compliance, metadata output, `tokenURI` / `contractURI` / ERC-2981 royalty configuration, staking UX implications, and pre-launch smoke tests |
| [OpenSea Compatibility Skill](ai/skills/opensea-compatibility-skill.md) | Pre-PR checklist covering ERC-721 compliance, metadata output, `tokenURI` / `contractURI` / ERC-2981 royalty support, and staking UX implications for marketplace listings |
| [OpenSea Readiness Checklist](opensea-readiness-checklist.md) | Deployment-ready checklist covering ERC-721 and Enumerable interfaces, metadata output, `tokenURI` / `contractURI` / ERC-2981 royalty configuration, staking UX implications, and pre-launch smoke tests |
| [OpenSea Compatibility Skill](ai/skills/opensea-compatibility-skill.md) | Pre-PR checklist covering ERC-721 and Enumerable interfaces, metadata output, `tokenURI` / `contractURI` / ERC-2981 royalty support, and staking UX implications for marketplace listings |

---

Expand Down
3 changes: 2 additions & 1 deletion docs/ai/agent-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ Before writing or modifying any contract code, work through the following:
- Do not add reward logic, token payouts, or point accumulation to the contracts.
- Do not remove or rename event parameters without flagging it as a breaking change.
- Do not use `require` with long strings in hot paths — prefer custom errors (`error Unauthorized()`).
- Do not use `ERC721Enumerable` unless on-chain enumeration is explicitly justified.
- Preserve the explicitly approved `ERC721Enumerable` support for frontend wallet and collection
discovery. Document its gas cost and unstable ordering; do not describe it as required by OpenSea.
- Do not add cleverly abstracted code when explicit logic is clearer.
- Do not generate migrations or deployment scripts without confirming they align with the deployment assumptions in `memory.md`.

Expand Down
2 changes: 1 addition & 1 deletion docs/ai/architecture-guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Contract inheritance chains must be kept short and purposeful. Prefer compositio

### Rule 5: Use OpenZeppelin battle-tested components
Always prefer OpenZeppelin contracts over custom implementations for:
- `ERC721` base contract
- `ERC721Enumerable` for the NFT contract's required wallet and global enumeration reads
- `Ownable` / `AccessControl` for admin functions
- `ReentrancyGuard` for external call protection
- `Pausable` for emergency stop
Expand Down
6 changes: 4 additions & 2 deletions docs/ai/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,10 @@ The `TricksforBoosterNFT` contract must maintain the following for marketplace c
- `contractURI()` — returns collection-level metadata URI (OpenSea extension)
- `royaltyInfo(uint256 tokenId, uint256 salePrice)` — returns royalty recipient and amount (ERC-2981)
- `Transfer` events on all ownership changes — required by ERC-721 and indexed by marketplaces
- `supportsInterface` correctly advertises ERC-721, ERC-165, and ERC-2981 support
- `ERC721Enumerable` is **not** used by default (adds gas cost without OpenSea benefit)
- `supportsInterface` correctly advertises ERC-721, ERC-721 Metadata, ERC-721 Enumerable, ERC-165,
and ERC-2981 support
- `ERC721Enumerable` is intentionally enabled for direct frontend wallet and collection discovery.
It is not required by OpenSea, adds mint/transfer gas cost, and does not guarantee stable order.

---

Expand Down
6 changes: 4 additions & 2 deletions docs/ai/repository-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This repository contains the Solidity smart contracts and Nethereum integration

| Contract | Responsibility |
|---|---|
| `BoosterNFT` (ERC-721) | Minting, ownership, metadata, royalties, OpenSea compatibility |
| `BoosterNFT` (ERC-721 Enumerable) | Minting, ownership, enumeration, metadata, royalties, OpenSea compatibility |
| `BoosterStaking` | NFT custody during staking period, stake/unstake event emission |

These contracts are **intentionally separate**. Do not merge them.
Expand Down Expand Up @@ -78,7 +78,9 @@ The `BoosterNFT` contract must:
- Implement `contractURI()` returning collection-level metadata (OpenSea extension)
- Implement ERC-2981 (`royaltyInfo`) for on-chain royalty information
- Emit `Transfer` events on all ownership changes (standard ERC-721 requirement)
- Not use `ERC721Enumerable` by default unless on-chain enumeration is justified — it increases gas costs without benefiting OpenSea compatibility
- Implement ERC-721 Enumerable for direct frontend wallet and collection discovery. This is a
project UX decision, not an OpenSea requirement, and its additional mint/transfer gas cost is
accepted. Enumeration order must never be treated as stable.

---

Expand Down
6 changes: 4 additions & 2 deletions docs/ai/skills/opensea-compatibility-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ The `TricksforBoosterNFT` contract must remain compatible with OpenSea and equiv

### ERC-721 Compatibility
- [ ] The contract still correctly implements the ERC-721 standard interface (`IERC721`)
- [ ] `supportsInterface(bytes4 interfaceId)` correctly returns `true` for ERC-721 (`0x80ac58cd`), ERC-165 (`0x01ffc9a7`), ERC-721 Metadata (`IERC721Metadata`, `0x5b5e139f`), and ERC-2981 (`0x2a55205a`) so marketplace metadata detection continues to work as expected
- [ ] `supportsInterface(bytes4 interfaceId)` correctly returns `true` for ERC-721 (`0x80ac58cd`), ERC-165 (`0x01ffc9a7`), ERC-721 Metadata (`IERC721Metadata`, `0x5b5e139f`), ERC-721 Enumerable (`0x780e9d63`), and ERC-2981 (`0x2a55205a`) so marketplace and frontend interface detection continues to work as expected
- [ ] `Transfer` events are emitted on all ownership changes (mint, transfer, burn) — required by ERC-721 and indexed by marketplaces
- [ ] `Approval` and `ApprovalForAll` events are emitted correctly
- [ ] The staking contract's use of `transferFrom` does not break marketplace approval flows (the NFT contract itself remains standard)
- [ ] `ERC721Enumerable` is not added without explicit justification (it increases gas cost without OpenSea benefit)
- [ ] ERC-721 Enumerable reads are preserved for direct frontend discovery, with tests for mint,
transfer, staking custody, and out-of-range indexes; code and documentation do not describe it
as an OpenSea requirement

### Metadata Output
- [ ] Token metadata follows the OpenSea metadata standard: `name`, `description`, `image`, and `attributes` fields are present and correctly populated
Expand Down
3 changes: 2 additions & 1 deletion docs/ai/task-templates/solidity-task-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ Before implementing, confirm the following:

- [ ] **Indexer impact**: Will the backend indexer still reconstruct staking state correctly after this change?
- [ ] **Nethereum DTO impact**: Does this change require updating typed event DTOs or function message classes?
- [ ] **OpenSea impact**: Does this change affect `tokenURI`, `contractURI`, or ERC-2981 royalty support?
- [ ] **OpenSea / NFT interface impact**: Does this change affect `tokenURI`, `contractURI`,
ERC-721 Enumerable, or ERC-2981 royalty support?
- [ ] **ABI/BIN artifact**: Does this change require regenerating and committing updated ABI/BIN artifacts?
- [ ] **Read method alignment**: Are all `isStaked` / `stakedOwnerOf` / other read methods still consistent with the new event semantics?

Expand Down
14 changes: 14 additions & 0 deletions docs/backend-integration-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The smart contracts are the authoritative source of truth for the following:
| Responsibility | Contract |
|---|---|
| Booster NFT ownership (who holds which token) | `TricksforBoosterNFT` (ERC-721) |
| Current NFT enumeration (`totalSupply`, `tokenByIndex`, `tokenOfOwnerByIndex`) | `TricksforBoosterNFT` (ERC-721 Enumerable) |
| Stake / unstake state transitions | `TricksforBoosterStaking` |
| Event emission on every state change | `TricksforBoosterStaking` |
| Current-state read methods (`isStaked`, `stakedOwnerOf`) | `TricksforBoosterStaking` |
Expand All @@ -38,6 +39,19 @@ Both events are emitted exactly once per state transition. There are no silent s

These read methods always reflect on-chain truth at the queried block. They are guaranteed to be consistent with the event log: if a `TokenStaked` event exists for a token with no subsequent `TokenUnstaked` event, `isStaked(tokenId)` will return `true`.

### NFT enumeration and player portfolios

Use `balanceOf(wallet)` plus `tokenOfOwnerByIndex(wallet, index)` to enumerate NFTs currently held
by a wallet. Use `totalSupply()` plus `tokenByIndex(index)` for collection-wide reconciliation.
Enumeration order is unspecified and may change after transfers, so callers must not use the index
as a persistent token identifier.

Custodial staking changes the ERC-721 owner to the staking contract. Consequently,
`tokenOfOwnerByIndex(player, ...)` contains only the player's unstaked NFTs. To build a complete
player portfolio, combine that result with `getWalletStakedTokens(player)` from the staking
contract (or the confirmed staking event index). Do not attribute every token enumerated under the
staking contract to the staking-contract owner; resolve its original staker through staking state.

---

## Backend Responsibilities
Expand Down
3 changes: 2 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ Do not call the deployment successful until all of these are checked:
- workflow summary contains both non-zero addresses;
- both deployment transaction receipts succeeded;
- deployed runtime bytecode exists at both addresses;
- NFT name, symbol, URIs, interfaces, royalty result, admin, minter, and pause state are correct;
- NFT name, symbol, URIs, ERC-165/ERC-721/ERC-721 Enumerable/ERC-2981 interfaces, royalty result,
admin, minter, and pause state are correct;
- staking owner and NFT linkage are correct;
- verification transcripts show success when verification was enabled;
- `smoke-result.json` contains mint/approve/stake/unstake transaction hashes;
Expand Down
5 changes: 5 additions & 0 deletions docs/indexing-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ The `TricksforBoosterNFT` contract emits standard ERC-721 `Transfer` events on e
- Tracing the full ownership history of a token
- Audit and debugging purposes

The NFT contract also implements ERC-721 Enumerable. `totalSupply`, `tokenByIndex`, and
`tokenOfOwnerByIndex` may be used for bounded current-state reconciliation, but they do not provide
history and their index order is not stable. A staked token enumerates under the staking contract;
use staking state to resolve the original staker.

---

## Event Identity
Expand Down
14 changes: 14 additions & 0 deletions docs/nft-collection-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ optional metadata `Chain` attributes, refer to the chain naming table in
- Each contract contains tokens from **all three game themes**: `coin`, `dice`, and `rps`.
- Contracts are independent. There is no cross-chain bridging or token sharing.
- Token IDs start at `1` and run sequentially to `600` within each contract.
- The contract implements ERC-721 Enumerable (`IERC721Enumerable`, interface ID `0x780e9d63`).

### 2.1 Enumeration behavior

Frontends may discover currently held NFTs with `balanceOf(owner)` and
`tokenOfOwnerByIndex(owner, index)`, and may inspect the minted collection with `totalSupply()` and
`tokenByIndex(index)`. Enumeration reports minted on-chain supply; it does not enforce or replace
the documented 600-token allocation policy.

Enumeration order is not stable and must not be treated as token-ID or mint order. Transfers use
swap-and-pop indexing, so clients must read indexes from `0` through the latest balance or supply
minus one and tolerate the list changing between blocks. While an NFT is staked, its ERC-721 owner
is the staking contract. Use `getWalletStakedTokens(wallet)` to discover tokens beneficially staked
by the original wallet.

---

Expand Down
9 changes: 8 additions & 1 deletion docs/opensea-readiness-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ This checklist must be completed before deploying the `TricksforBoosterNFT` coll

## 1. ERC-721 Compliance

- [ ] `supportsInterface` returns `true` for all required interface IDs:
- [ ] `supportsInterface` returns `true` for all project-advertised interface IDs (Enumerable is a
Tricksfor frontend feature, not an OpenSea requirement):
- ERC-165: `0x01ffc9a7`
- ERC-721: `0x80ac58cd`
- ERC-721 Metadata: `0x5b5e139f`
- ERC-721 Enumerable: `0x780e9d63`
- ERC-2981 (royalties): `0x2a55205a`
- [ ] `Transfer` events are emitted for every ownership change (mint, transfer, burn) as required by ERC-721 and expected by marketplace indexers
- [ ] `Approval` and `ApprovalForAll` events are emitted correctly
- [ ] `ownerOf(tokenId)` returns the correct current owner for every minted token
- [ ] `balanceOf(address)` returns the correct token count per wallet
- [ ] `totalSupply()`, `tokenByIndex(index)`, and `tokenOfOwnerByIndex(owner, index)` track mint and transfer custody correctly
- [ ] Frontend code does not assume enumerable index order is stable
- [ ] `tokenURI(tokenId)` reverts for unminted token IDs (OpenZeppelin default behaviour; behaviour is consistent with contract design)

---
Expand Down Expand Up @@ -132,9 +136,12 @@ Run these checks against the deployed contract on a testnet before deploying to
- [ ] Call `royaltyInfo(1, 10000)` — confirm the result is `(royaltyReceiver, 500)` for a 5% royalty (or the configured value)
- [ ] Call `supportsInterface(0x2a55205a)` — confirm it returns `true` (ERC-2981)
- [ ] Call `supportsInterface(0x80ac58cd)` — confirm it returns `true` (ERC-721)
- [ ] Call `supportsInterface(0x780e9d63)` — confirm it returns `true` (ERC-721 Enumerable)
- [ ] Confirm `totalSupply()` includes token 1 and `tokenOfOwnerByIndex(testWallet, index)` can find it
- [ ] Import the collection on OpenSea testnet and confirm the collection and token metadata render correctly
- [ ] Transfer the test token to a second wallet and confirm `ownerOf(1)` reflects the new owner
- [ ] Stake the test token and confirm `isStaked(1)` returns `true` and the token no longer shows in the original wallet on OpenSea
- [ ] While staked, confirm enumerable custody moves from the test wallet to the staking contract
- [ ] Unstake the test token and confirm `isStaked(1)` returns `false` and the token reappears in the original wallet

---
Expand Down
15 changes: 15 additions & 0 deletions docs/security-review-2026-07-19.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,18 @@ slither . \
--fail-medium \
--json slither-report.json
```

## Post-review change — ERC-721 Enumerable (2026-07-22)

`TricksforBoosterNFT` now inherits OpenZeppelin `ERC721Enumerable` to support direct frontend
wallet and collection discovery. This adds standard enumerable storage writes to mint and transfer
operations, so those operations cost more gas than the reviewed non-enumerable version. The
contract still uses explicit single-token minting; OpenZeppelin's unsupported batch-mint path is
not exposed.

Tests cover interface detection, global and per-owner enumeration, transfer updates, out-of-range
indexes, and Nethereum enumeration across stake/unstake custody. Enumeration order is explicitly
unspecified. The updated run passed 102 Hardhat tests with 95.83% NFT line coverage and all 76 .NET
tests (31 package unit, 37 deployment, and 8 Nethereum integration). This addendum records the
focused change and does not claim a new full Slither or third-party audit until the reproduction
commands above are rerun on the updated commit.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public async Task<IReadOnlyList<SanityCheckResult>> RunAsync(
{
("ERC-165", "01ffc9a7"),
("ERC-721", "80ac58cd"),
("ERC-721 Enumerable", "780e9d63"),
("ERC-2981", "2a55205a"),
})
{
Expand Down
Loading
Loading