Technical Documentation

Cipolla Layer Zero — smart contract reference. Contract address: 0x33D2827fa39A49DC4De76467c93e58Bf9DE96072 — verified source code available on Basescan: https://basescan.org/address/0x33D2827fa39A49DC4De76467c93e58Bf9DE96072#code

Contract overview

PropertyValue
NameCipolla Layer Zero
SymbolCIPO
StandardERC721Enumerable + ERC2981
NetworkBase L2
CompilerSolidity 0.8.24 / EVM Shanghai / Optimizer 200
LibrariesOpenZeppelin v4.9.6
Max supply250 tokens (fixed, immutable)
Prior artINPI ASV-2026-PROVENANCE-ROYALTIES-001

Economic parameters

ParameterValue
Mint price0.05 ETH (adjustable with 48h on-chain timelock)
Creator royalty10% of msg.value on each distributeRoyalties() call
Holder pool5% of msg.value, split equally among all unique historical owners
Total royalties15% of declared sale price
Anti-wash tradingEach wallet address appears exactly once in any token's ownership history
Dust handlingInteger division remainders accumulate in dustReserve, withdrawable by creator
P2P price floormax(seller's lowestPrice, 80% of getMarketAverage()), or mint price if no sale history exists
Market oraclegetMarketAverage(tokenId, k) — on-chain average of the last k coherent sales, used to prevent wash-trading via artificially low P2P prices

Sale channels

ValueChannelPrice verifiable?
0UnknownNo
1OpenSeaYes — via OpenSea API
2FoundationYes — via Foundation API
3BlurYes — via Blur API
4P2PYes — msg.value IS the price, cryptographically verified in an atomic transaction
5OtherDepends

mint()

function mint() external payable

Mints one token to the caller. The caller is immediately registered in the token's historical ownership record.

ConditionDetails
msg.valueMust equal mintPrice exactly (currently 0.05 ETH)
mintActiveMust be true
pausedMust be false
SupplyMust not exceed MAX_SUPPLY (250)
CallerMust not be contract owner (owner cannot mint public tokens)

distributeRoyalties()

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.

Parameters

ParameterDescription
tokenIdID of the token that was sold (1–250)
salePriceDeclared sale price in wei. For marketplace sales: verifiable via API.
channelSale channel enum (0–5, see Sale channels)
msg.valueMust 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.

Authorization (v1.7)

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.

Distribution

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.

claimRoyalties()

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.

getMarketAverage()

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.

View functions

FunctionReturns
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

Admin functions (owner only)

FunctionDescription
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.

Guide — Marketplace sale

1
Token sold on marketplace
e.g. OpenSea, Foundation. The sale price is publicly visible on the marketplace.
2
Marketplace sends 15% to creator address (ERC-2981)
The marketplace honors the royalty standard and forwards the creator's share automatically. Some marketplaces apply zero royalties, however, which in our view undermines the sustainability of this value-creation system — and this is precisely why atomic buyP2P() was developed as a recommended alternative.
3
Call distributeRoyalties()
In V1, this function is restricted to the contract owner only (onlyOwner) — see Authorization (v1.7) above.
distributeRoyalties(tokenId, salePrice_in_wei, 1) + send 15% of salePrice as msg.value.
4
Contract distributes automatically
10% → creatorWallet. 5% → recorded as claimable by all historical owners. For a P2P sale, distribution is atomic with the sale itself (buyP2P()). For a marketplace sale, this depends on the marketplace's royalty policy AND on action by the contract owner — a V1 limitation, which is why P2P is the recommended channel.

Guide — P2P sale

buyP2P() handles a peer-to-peer sale atomically, in a single transaction. The steps differ for the seller and the buyer.

Seller

1
Call approve(contractAddress, tokenId)
Authorizes the contract to transfer the token on the seller's behalf.
2
Call listForSale(tokenId, intendedBuyer, lowestPrice)
intendedBuyer can be 0x0000000000000000000000000000000000000000 for a sale open to anyone. lowestPrice is the optional minimum price set by the seller.
3
Call withdrawSeller()
After the buyer has completed buyP2P(), your proceeds are credited to your address. Call withdrawSeller() to collect your funds. This pull payment pattern protects against gas griefing attacks via malicious receive() functions.

Buyer

1
Call buyP2P(tokenId, sellerAddress)
Send the purchase price in ETH (msg.value). The contract automatically checks: the listing is active, the buyer matches intendedBuyer if one was specified, and the price respects both the lowestPrice AND the oracle floor — the price paid must be at least 80% of getMarketAverage(tokenId, 5) (i.e. it cannot fall more than 20% below the recent average of coherent sales), or the mint price if no sale history exists yet.

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.

Guide — Claiming royalties

Royalties accumulate on-chain in your personal balance. There is no deadline — claim whenever you wish.

Option A — via cipollaprotocol.io

Go to the Dashboard → connect your wallet → click Claim royalties. Gas cost on Base: ~$0.01.

Option B — directly on Basescan

Go to the contract on Basescan → Write Contract → Connect wallet → call claimRoyalties().

Check your balance first

// Read-only, no gas needed
claimableRoyalties("0xYourAddress") // → returns wei

SaleRecord struct

Each call to distributeRoyalties() records a SaleRecord on-chain, permanently visible on Basescan and via getSaleHistory().

FieldDescription
timestampBlock timestamp of the distribution
callerAddress that called distributeRoyalties()
salePriceDeclaredDeclared sale price in wei
royaltiesSentFor P2P sales: salePrice × 15%. For marketplace declarations: msg.value
holderPool5% allocated to historical owners
creatorShare10% sent to creatorWallet
holdersCountNumber of historical owners at time of distribution
royaltiesCoherenttrue if royalties are coherent with the declared sale price
channelSale channel enum (0–5)
priceVerifiabletrue if marketplace sale (price publicly verifiable)
priceDiffDifference vs previous declared price
priceIncreasedtrue if price increased vs previous sale

Wei reference

ETHWei
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 ETH1,000,000,000,000,000,000
48h in seconds172,800

Known limitations

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.

FAQ

Is this an investment?
No. This is an economic experimentation. Any potential value depends entirely on the secondary market. No returns are guaranteed.
Can royalties be blocked or stolen?
No. The contract maintains a totalPendingClaims counter that tracks all ETH owed to holders. The owner's withdraw() function is mathematically blocked from touching this amount. It is impossible by contract design — not just by policy.
What happens if I buy and resell the same token multiple times?
Each wallet address appears exactly once in the ownership history of a token, regardless of how many times it buys and resells. You receive one equal share of the holder pool on every future distribution — no more, no less. This prevents wash trading.
What is the reveal?
During the mint, all tokens display a placeholder image. 30 days after the mint opens, the creator calls setBaseURI() to point all tokens to their final images. At the same time, the generation seed is publicly revealed, allowing anyone to verify the intruder positions were determined before any mint occurred.
How do I verify the intruder positions were not manipulated?
Before the mint, we published the SHA-256 hash of our seed phrase: c0c2663a44d7998cef3140261b53744468d714c975809f7e11161a11bd8e1747. At reveal, we publish the seed itself. Anyone can compute SHA-256(seed) and verify it matches the published hash, then run our open-source placement algorithm to confirm the intruder positions in the collection.
What if there is a critical bug after deployment?
Cipolla Protocol has been seriously audited by an independent security researcher, with a public mitigation report and verified source code available on Basescan.

Our position reflects a core conviction: no centralization, no dependency on external governance — just code. This means there is no admin function to pause claims, reverse transactions, or freeze funds in the contract once deployed and active.

This is a deliberate trade-off. Even the largest, most heavily audited protocols in DeFi have suffered exploits. For this reason, V1 is intentionally limited — a small, experimental collection (250 tokens at 0.05 ETH) designed to test the technical and economic viability of the Provenance Royalties model before any larger deployment.

We recommend all holders monitor their claimable royalties and withdraw them regularly, rather than letting them accumulate over long periods.
Is there a deadline to claim royalties?
No. Your royalty balance accumulates on-chain indefinitely. Claim whenever you wish — in one transaction or let it accumulate over time.