A composable, ERC20-backed, cross-chain ERC721 collection for the Frax ecosystem intended to commemorate the 5th anniversary of Frax Finance.
Fraxiversary is an ERC721-based collection with four core behaviors:
-
BASE minting
- Users mint NFTs by paying with an allowlisted Frax ERC20.
- The contract records an internal balance for the new tokenId.
-
GIFT minting
- Users mint NFTs for a recipient by paying a fixed WFRAX price.
- GIFT tokens have their own supply range.
-
SOULBOUND minting
- Owner-only mints for curated distribution.
- Marked non-transferable via
IERC6454semantics. - Not treated as ERC20-backed positions.
-
FUSION
- Owners can fuse four distinct BASE tokens into one premium FUSED token.
- The four BASE tokens are escrowed by the contract.
- Unfusing burns the FUSED token and returns the originals.
Additionally, the collection supports cross-chain transfers using LayerZero’s ONFT721 pattern. Soulbound rules are enforced consistently across local transfers and bridging flows, with a bridge-aware bypass that prevents normal transfers while still allowing ONFT debit/credit operations.
Token metadata contains information that allows the UIs to display tokens properly. It also includes the link to the image representing the token.
- BASE WFRAX token: https://arweave.net/CxQl0ki_oqypKeGVbVHBIAPjY2QT3jaBDX9FSqrfBd4
- BASE sfrxUSD token: https://arweave.net/VAk7sIe36qVGS80TCBqctI1pZJbwDzx6KytRA5l6VhE
- BASE sfrxETH token: https://arweave.net/Rzs5IiJpVPKIPRHgqmqDhDKikt_miLqJKYTPBEkUSzw
- BASE FPI token: https://arweave.net/p9QZj7kpFy_iOCmDxH3gYqqDtvzZsU81QkWYn7DpBUA
- FUSED token: https://arweave.net/-pyCwZBiSn498egF8gfqrCJz4gmdVC0XxrN8wO9DuWQ
- GIFT token: https://arweave.net/gv2ghexT4l3LVsheyz1MCO86yZnTFeFo__G6guMq7OE
flowchart TB
subgraph Users
U1[User / Minter]
U2[Recipient]
O[Owner / Admin]
end
subgraph Fraxiversary_Contract
C["Fraxiversary
ERC721 + Enumerable + Pausable + Burnable
IERC6454 + IERC7590 + ONFT721Core"]
end
subgraph ERC20s
E1[Allowlisted Frax ERC20s]
E2[WFRAX]
end
subgraph LayerZero
LZ[Endpoint + ONFT721 libraries]
D["Destination Chain
Fraxiversary instance"]
end
U1 -->|paidMint| C
U1 -->|giftMint| C
O -->|soulboundMint| C
C -->|transferFrom price+fee| E1
C -->|transferFrom giftPrice+fee| E2
C -->|records internal balances| C
U1 -->|"fuseTokens (4 BASE)"| C
C -->|escrows BASE NFTs| C
U1 -->|unfuseTokens| C
C -->|returns BASE NFTs| U1
C <-->|send/receive| LZ
LZ --> D
The contract allocates distinct ID ranges:
-
BASE tokens:
0 ... mintingLimit - 1 -
GIFT tokens:
mintingLimit ... mintingLimit + giftMintingLimit - 1 -
Premium tokens (FUSED + SOULBOUND):
start at
mintingLimit + giftMintingLimit
and grow upward.
enum TokenType {
NONEXISTENT,
BASE,
FUSED,
SOULBOUND,
GIFT
}- Minted with an allowlisted Frax ERC20.
- Backed by a single recorded underlying ERC20.
- The deposit (excluding fee) is credited to:
erc20Balances[tokenId][erc20]- and the underlying ERC20 address is stored in
underlyingAssets[tokenId][0].
- Minted with WFRAX.
- Separate supply and default URI.
- Same accounting model as BASE but with fixed asset = WFRAX.
- Minted only by owner.
- Marked non-transferable:
isNonTransferrable[tokenId] = truetokenTypes[tokenId] = SOULBOUND
- Treated as non-ERC20-backed from a UI perspective:
getUnderlyingBalancesreturns empty arrays.
- Created by fusing four BASE tokens owned by the caller.
- All four BASE tokens must have distinct underlying ERC20 assets.
- The BASE tokens are transferred into contract custody.
- The new FUSED token stores:
underlyingTokenIds[premiumTokenId][0..3].
stateDiagram-v2
[*] --> NONEXISTENT
NONEXISTENT --> BASE: paidMint
NONEXISTENT --> GIFT: giftMint
NONEXISTENT --> SOULBOUND: soulboundMint
BASE --> FUSED: fuseTokens\n(escrow 4 BASE)
FUSED --> BASE: unfuseTokens\n(return 4 BASE)
BASE --> NONEXISTENT: burn\n(redeem ERC20)
GIFT --> NONEXISTENT: burn\n(redeem WFRAX)
SOULBOUND --> NONEXISTENT: burn\n(no ERC20 redemption)
FUSED --> NONEXISTENT: unfuse then burn\n(BASE must be returned first)
Note: The actual implementation marks types as
NONEXISTENTafter burn. The "FUSED -> BASE" arrow in the diagram describes the conceptual effect of unfusing (the BASE tokens become user-owned again).
Fraxiversary tracks ERC20 deposits using internal accounting:
erc20Balances[tokenId][erc20]underlyingAssets[tokenId][index]numberOfTokenUnderlyingAssets[tokenId]transferOutNonces[tokenId]
- Allowed only during mint flows:
paidMintgiftMint
- The external
IERC7590deposit function intentionally reverts:TokensCanOnlyBeDepositedByNftMint().
- Allowed only via
burn. - The external
IERC7590withdrawal function intentionally reverts:TokensCanOnlyBeRetrievedByNftBurn().
- When minting:
- User pays
amount + fee. amountis credited to the token’s internal balance.feeis accumulated incollectedFees[erc20].
- User pays
flowchart LR
U[User] -->|approve| ERC20[ERC20]
U -->|mint| C[Fraxiversary]
C -->|transferFrom amount + fee| ERC20
C -->|credit amount to tokenId| BAL[erc20Balances]
C -->|record fee| FEES[collectedFees]
O[Owner] -->|retrieveCollectedFees| C
C -->|transfer fee amount| O
Requirements:
block.number <= mintingCutoffBlocknextTokenId < mintingLimitmintPrices[erc20] != 0
Effects:
- Transfers
mintPrice + feefrom minter. - Credits internal ERC20 balance.
- Sets token type to
BASE. - Sets token URI from
baseAssetTokenUris[erc20].
Requirements:
block.number <= mintingCutoffBlocknextGiftTokenId < mintingLimit + giftMintingLimit
Effects:
- Transfers
giftMintingPrice + feein WFRAX from minter. - Mints to
_recipient. - Sets token type to
GIFT. - Uses
giftTokenUri.
Owner-only.
- Mints in the premium range.
- Uses a custom URI provided at mint time.
sequenceDiagram
participant U as User
participant C as Fraxiversary
U->>C: fuseTokens(t1,t2,t3,t4)
C->>C: verify ownership of all 4
C->>C: verify types == BASE
C->>C: verify underlying assets all distinct
C->>C: _update(address(this), t1..t4)
C->>C: mint premiumTokenId
C->>C: store underlyingTokenIds[premiumTokenId][0..3]
C-->>U: emit TokenFused
sequenceDiagram
participant U as User
participant C as Fraxiversary
U->>C: unfuseTokens(premiumTokenId)
C->>C: verify owner
C->>C: verify type == FUSED
C->>C: read underlyingTokenIds[0..3]
C->>C: _burn(premiumTokenId)
C->>C: clear underlyingTokenIds
C->>C: _update(U, t1..t4)
C-->>U: emit TokenUnfused
Transferability is controlled through:
isNonTransferrable[tokenId]_soulboundCheck(tokenId)- overridden
_update.
Rule:
- A soulbound token is not transferable in normal ERC721 flows.
- The check is bypassed temporarily during bridge operations
using
_isBridgeOperation.
flowchart TB
T[Transfer attempt] --> U[_update override]
U --> B{_isBridgeOperation?}
B -->|yes| OK[Skip soulbound check]
B -->|no| SB{"isNonTransferrable[tokenId]?"}
SB -->|yes| R[Revert: CannotTransferSoulboundToken]
SB -->|no| P[Proceed with OZ _update]
OK --> P
Fraxiversary extends ONFT721Core.
_debit(from, tokenId, ...)- validates approval semantics
- calls
_bridgeBurn(owner, tokenId) - which uses
_update(address(0), tokenId, owner)while_isBridgeOperation = true.
_lzReceivedecodes a composed message that contains:tokenUriisSoulbound
- Calls
_credit(to, tokenId, ...) - Restores:
tokenTypes(implicitly via existing storage)isNonTransferrable[tokenId]tokenURI(tokenId).
sequenceDiagram
participant S as Source Fraxiversary
participant LZ as LayerZero Endpoint
participant D as Destination Fraxiversary
Note over S: User initiates send
S->>S: _buildMsgAndOptions\n(encodes tokenUri + soulbound flag)
S->>S: _debit(from, tokenId)
S->>S: _bridgeBurn(owner, tokenId)\n(_update to address(0))
S->>LZ: send(message, options)
LZ->>D: deliver(message)
D->>D: _lzReceive(origin, guid, message)
D->>D: decode(tokenUri, isSoulbound)
D->>D: _credit(to, tokenId)
D->>D: _setTokenURI(tokenId, tokenUri)
D->>D: isNonTransferrable[tokenId]=isSoulbound
D-->>LZ: sendCompose(...)
The contract emits:
MetadataUpdate(tokenId)BatchMetadataUpdate(first, last)
Admin functions allow refreshing URIs for:
- BASE range
- GIFT range
- premium range
This supports indexer-friendly updates without changing token logic.
Owner-controlled functions include:
-
pricing & allowlist
updateBaseAssetMintPriceupdateGiftMintingPriceupdateMintingFeeBasisPointsupdateMintingCutoffBlock
-
URI management
setBaseAssetTokenUrisetGiftTokenUrisetPremiumTokenUrirefreshBaseTokenUrisrefreshGiftTokenUrisrefreshPremiumTokenUrisupdateSpecificTokenUri
-
protocol controls
pauseunpauseretrieveCollectedFees
MintPriceUpdatedGiftMintPriceUpdatedMintingFeeUpdatedMintingCutoffBlockUpdatedFeeCollectedFeesRetrievedGiftMintedNewSoulboundTokenTokenFusedTokenUnfused
The errors are designed to be explicit and cheap:
- minting bounds
- allowlist failures
- soulbound violations
- fusion invariants
- ERC20 accounting constraints
Key intended invariants:
-
No arbitrary ERC20 movement
- Deposits only during mint.
- Withdrawals only during burn.
-
Fee isolation
- Fees are tracked separately from token balances in
collectedFees. - Owner can sweep only the fee bucket, not user-backed balances.
- Fees are tracked separately from token balances in
-
Fusion uniqueness
- Underlying ERC20 assets for the four BASE tokens must be distinct.
-
Bridge-aware soulbound
- Standard transfers respect soulbound rules.
- ONFT debit/credit is allowed to preserve UX and cross-chain supply integrity.
- For BASE/GIFT tokens:
- show ERC20 backing using
getUnderlyingBalances.
- show ERC20 backing using
- For SOULBOUND:
- expect empty backing arrays (by design).
- For FUSED:
- derive backing from underlying BASE composing tokens.
- Watch for:
MetadataUpdateBatchMetadataUpdate- fusion events
- burn events
Recommended checks:
forge buildforge build --sizes --skip test --skip scriptforge testforge fmtforge coverage --report lcov --ir-minimum && genhtml -o report ./lcov.info- static analysis for:
- mint bounds
- fusion/unfusion ordering
- bridge message decoding
- ERC20 fee accounting
This README includes:
- Architecture map (flowchart)
- Token ID range explanation
- Lifecycle state machine
- ERC20 fee and accounting flow
- Soulbound enforcement flow
- Fusion / Unfusion sequence diagrams
- Bridge (ONFT) sequence diagram
MIT.