Cipolla Layer Zero — smart contract reference. Contract address: 0x33D2827fa39A49DC4De76467c93e58Bf9DE96072 — verified source code available on Basescan: https://basescan.org/address/0x33D2827fa39A49DC4De76467c93e58Bf9DE96072#code
| Property | Value |
|---|---|
| Name | Cipolla Layer Zero |
| Symbol | CIPO |
| Standard | ERC721Enumerable + ERC2981 |
| Network | Base L2 |
| Compiler | Solidity 0.8.24 / EVM Shanghai / Optimizer 200 |
| Libraries | OpenZeppelin v4.9.6 |
| Max supply | 250 tokens (fixed, immutable) |
| Prior art | INPI ASV-2026-PROVENANCE-ROYALTIES-001 |
| Parameter | Value |
|---|---|
| Mint price | 0.05 ETH (adjustable with 48h on-chain timelock) |
| Creator royalty | 10% of msg.value on each distributeRoyalties() call |
| Holder pool | 5% of msg.value, split equally among all unique historical owners |
| Total royalties | 15% of declared sale price |
| Anti-wash trading | Each wallet address appears exactly once in any token's ownership history |
| Dust handling | Integer division remainders accumulate in dustReserve, withdrawable by creator |
| P2P price floor | max(seller's lowestPrice, 80% of getMarketAverage()), or mint price if no sale history exists |
| Market oracle | getMarketAverage(tokenId, k) — on-chain average of the last k coherent sales, used to prevent wash-trading via artificially low P2P prices |
| Value | Channel | Price verifiable? |
|---|---|---|
| 0 | Unknown | No |
| 1 | OpenSea | Yes — via OpenSea API |
| 2 | Foundation | Yes — via Foundation API |
| 3 | Blur | Yes — via Blur API |
| 4 | P2P | Yes — msg.value IS the price, cryptographically verified in an atomic transaction |
| 5 | Other | Depends |
function mint() external payable
Mints one token to the caller. The caller is immediately registered in the token's historical ownership record.
| Condition | Details |
|---|---|
| msg.value | Must equal mintPrice exactly (currently 0.05 ETH) |
| mintActive | Must be true |
| paused | Must be false |
| Supply | Must not exceed MAX_SUPPLY (250) |
| Caller | Must not be contract owner (owner cannot mint public tokens) |
function distributeRoyalties( uint256 tokenId, uint256 salePrice, uint8 channel ) external payable
Distributes royalties for a secondary sale. This function is used to distribute royalties to historical holders for sales made via third-party marketplaces.
| Parameter | Description |
|---|---|
| tokenId | ID of the token that was sold (1–250) |
| salePrice | Declared sale price in wei. For marketplace sales: verifiable via API. |
| channel | Sale channel enum (0–5, see Sale channels) |
| msg.value | Must be exactly 15% of salePrice (salePrice × 15 / 100). Coherence is checked and recorded. Note: in V1 this function is restricted to the contract owner (onlyOwner) — for sales where distribution is atomic and trustless, use buyP2P() instead. |
Restricted to the contract owner only (onlyOwner). This changed from v1.6, where any historical holder could call it, after a security audit identified a price-oracle manipulation vector — a holder could call this function repeatedly with an artificially low declared price to manipulate getMarketAverage() and lower the buyP2P() price floor.
The current token holder is always excluded from this distribution — they cannot be paid royalties from a sale they are the recipient of.
// msg.value = 0.15 ETH (15% of 1 ETH sale) creatorShare = msg.value × 10 / 15 // → 0.10 ETH → creatorWallet (immediate) holderPool = msg.value − creatorShare // → 0.05 ETH // eligibleCount excludes the current token holder (M-1 fix): // the current owner cannot be paid from their own incoming sale. eligibleCount = holdersCount − 1 (if current holder has ever owned the token) = holdersCount (otherwise) sharePerHolder = holderPool / eligibleCount // → split equally among eligible holders only dust = holderPool − (sharePerHolder × eligibleCount) // → dustReserve // If eligibleCount == 0 (the current holder is the only historical owner), // the entire holderPool is sent directly to dustReserve, since there is // no other holder eligible to receive it.
P2P sales. buyP2P() handles everything atomically: the token transfer, the payment to the seller, and the royalty distribution all happen in a single transaction at the moment of purchase — no separate call is needed.
function claimRoyalties() external
Transfers all accumulated royalties to the caller's wallet. Uses a pull payment pattern — royalties accumulate on-chain and must be claimed manually. There is no deadline to claim.
Gas cost on Base: ~$0.01.
Available via: cipollaprotocol.io (connect wallet → Claim button) or directly on Basescan.
function getMarketAverage( uint256 tokenId, uint256 k ) external view returns (uint256)
An internal oracle computing the average price of the last 5 sales where royaltiesSent is coherent with the declared sale price. This covers: (1) all buyP2P() transactions, which are atomic and fully verifiable on-chain — royaltiesCoherent is always true by design; (2) marketplace sales declared via distributeRoyalties(), where the owner verifies the exact sale price using public marketplace APIs (OpenSea, Blur, Foundation, etc.) and sends exactly 15% of that price as msg.value — making the declaration verifiable by anyone against the public on-chain transaction data of the marketplace.
This oracle is used as anti-wash-trading protection in buyP2P(), preventing an attacker from entering false holders into the ownership history at a derisory price, and illegitimately capturing the entire holder pool from genuine holders.
| Function | Returns |
|---|---|
| claimableRoyalties(address) | Amount of ETH claimable by a given address (wei) |
| getSaleHistory(tokenId) | Full array of SaleRecord structs for a token |
| getOwnerHistory(tokenId) | Array of all unique historical owner addresses |
| tokenURI(tokenId) | Full metadata URI (baseURI + tokenId) |
| royaltyInfo(tokenId, salePrice) | ERC-2981: (creatorWallet, 15% of salePrice) |
| totalSupply() | Number of tokens minted so far |
| mintPrice() | Current mint price in wei |
| totalPendingClaims() | Total ETH reserved for holder royalties |
| dustReserve() | Total dust accumulated from integer division |
| Function | Description |
|---|---|
| setMintActive(bool) | Enable or disable minting |
| emergencyPause(reason) | Pause mint. Does NOT block transfers, claims, or royalty distribution. emergencyPause() suspends ONLY the mint() function — every other function (claimRoyalties(), buyP2P(), distributeRoyalties(), listForSale(), standard transfers) continues to work normally, even while paused. This is a deliberate guarantee: even if an issue is detected, holders' funds and rights remain accessible at all times and can never be frozen by the owner. |
| emergencyUnpause() | Resume from pause |
| schedulePriceChange(price, delay) | Schedule a mint price change. Minimum delay: 48 hours. Two levels of notification: an on-chain event is emitted immediately (publicly verifiable by anyone following the contract), and an announcement is also made on our official social media channels to inform the community in an accessible way. |
| cancelPriceChange() | Cancel a pending price change |
| setBaseURI(uri) | Update metadata URI. Used at reveal: setBaseURI("ipfs://CID_FINAL/") |
| setCreatorWallet(address) | Update the wallet that receives creator royalties |
| withdraw() | Withdraw mint proceeds. Only withdraws balance − totalPendingClaims − totalPendingWithdrawals − dustReserve. Holder funds and seller funds are always protected. |
| withdrawDust() | Withdraw accumulated dust from integer division remainders |
Note: transferOwnership() and renounceOwnership() are permanently disabled in V1 — ownership is fixed to the deploying address.
distributeRoyalties(tokenId, salePrice_in_wei, 1) + send 15% of salePrice as msg.value.buyP2P() handles a peer-to-peer sale atomically, in a single transaction. The steps differ for the seller and the buyer.
Atomic transaction. Everything happens in a single atomic transaction: token transfer + payment to the seller (85%) + distribution to historical holders (5%) + creator's share (10%), with no separate call required.
cancelListing(tokenId) lets the seller cancel an active listing at any time.
Important. If a token listed on Cipolla is sold via an external marketplace (e.g. OpenSea) before buyP2P() is called, the Cipolla listing automatically becomes stale and invalid (it is automatically invalidated on any transfer of the token) — a new listing must be created if the seller wants to offer the token via buyP2P() again.
Royalties accumulate on-chain in your personal balance. There is no deadline — claim whenever you wish.
Go to the Dashboard → connect your wallet → click Claim royalties. Gas cost on Base: ~$0.01.
Go to the contract on Basescan → Write Contract → Connect wallet → call claimRoyalties().
// Read-only, no gas needed claimableRoyalties("0xYourAddress") // → returns wei
Each call to distributeRoyalties() records a SaleRecord on-chain, permanently visible on Basescan and via getSaleHistory().
| Field | Description |
|---|---|
| timestamp | Block timestamp of the distribution |
| caller | Address that called distributeRoyalties() |
| salePriceDeclared | Declared sale price in wei |
| royaltiesSent | For P2P sales: salePrice × 15%. For marketplace declarations: msg.value |
| holderPool | 5% allocated to historical owners |
| creatorShare | 10% sent to creatorWallet |
| holdersCount | Number of historical owners at time of distribution |
| royaltiesCoherent | true if royalties are coherent with the declared sale price |
| channel | Sale channel enum (0–5) |
| priceVerifiable | true if marketplace sale (price publicly verifiable) |
| priceDiff | Difference vs previous declared price |
| priceIncreased | true if price increased vs previous sale |
| ETH | Wei |
|---|---|
| 0.05 ETH (mint price) | 50,000,000,000,000,000 |
| 0.15 ETH (15% of 1 ETH) | 150,000,000,000,000,000 |
| 0.10 ETH (10% of 1 ETH) | 100,000,000,000,000,000 |
| 0.05 ETH (5% of 1 ETH) | 50,000,000,000,000,000 |
| 1 ETH | 1,000,000,000,000,000,000 |
| 48h in seconds | 172,800 |
1. Marketplace buyers are not added to the ownership history and do not benefit from future royalty distributions. This restriction is linked to wash trading risk. Wash trading directly harms genuine holders: it allows a bad actor to illegitimately capture the entire holder pool of a sale, effectively stealing from everyone else in the ownership history. To enter the ownership history and benefit from future distributions as well as the atomicity of the transaction, we recommend using the buyP2P() function.
2. Marketplace sales are declared manually by the contract owner (a restriction implemented following an external security audit to prevent wash trading). Each declared marketplace sale is verifiable: the dashboard displays the on-chain proof of the marketplace sale price used to feed the oracle's calculation base. Historical holders of a token sold on a marketplace benefit from the distribution via this mechanism.
3. Royalty distribution for marketplace sales requires a manual call to distributeRoyalties() by the contract owner (onlyOwner restriction — see Authorization (v1.7) above).
4. Risk mitigation plan: no protocol can guarantee 100% absence of risk. Should the protocol gain meaningful traction, we plan to design V2 around a burn-and-migrate mechanism: holders voluntarily burn their V1 token to mint an equivalent V2 token, with ownership history reconstructed from V1's public on-chain record. This allows the protocol to evolve toward greater decentralization, including wash-trading prevention mechanisms currently in research, without forcing any holder to migrate, and without losing the provenance already established.