Skip to content

Latest commit

 

History

History
298 lines (199 loc) · 18.9 KB

File metadata and controls

298 lines (199 loc) · 18.9 KB

FundPunks - Trustless CryptoPunk Fundraising

FundPunks is a trustless fundraising protocol for buying CryptoPunks. Anyone can launch a campaign. Anyone can donate. Once there is enough ETH for a Punk, anyone can execute the buy onchain. The campaign creator gets the Punk. Public goods get any change. If no purchase succeeds, contributors can claim a refund.

The project includes smart contracts, a public web app, deployment tooling, and documentation. It was partially inspired by Buy Me A Punk, but is an original development by nuconomy.eth.

Quick Links

Design Constraints

FundPunks is intentionally narrow:

  • Gift-only contributions: no token, no fractional ownership, no art mint, no points, no governance, and no expectation of return.
  • Public campaign creation: anyone can launch a campaign through the factory without permission from the original project creator.
  • Budget-capped funding: contributions are accepted only up to the campaign budget. If the final contribution sends more than the remaining budget, the campaign records only the remaining amount and refunds the excess in the same transaction.
  • Hardcoded dependencies: campaigns always use the marketplace contracts and Protocol Guild recipient listed in Active Deployments, so campaign creators and modified frontends cannot redirect purchases or surplus funds.
  • Time-bounded campaigns: campaigns must finish within one year of creation. The public frontend defaults to a 30-day funding window plus a 30-day execution window.
  • Permissionless execution: anyone can try to execute a Punk purchase before the execution deadline if the listing price is at or below the campaign budget, at or below the executor's transaction-level max price, and covered by the campaign's tracked contribution balance and current ETH balance.
  • Atomic purchase settlement: a successful purchase pays the exact listing price, receives or settles the Punk, sends the Punk to the campaign creator, and donates any leftover ETH to Protocol Guild. If any required post-purchase step fails, the transaction reverts.
  • Refund path: if no purchase succeeds before the execution deadline, or if the creator cancels before a purchase, contributors can claim refunds for their recorded contributions.

Protocol Guild supports Ethereum protocol contributors. FundPunks sends successful-purchase change there as a small public-goods dividend from each campaign.

Active Deployments

The addresses below are the Ethereum mainnet contracts and recipients currently used by FundPunks.

FundPunks Contracts

Role Address Notes
FundPunks factory 0x95d75D46A32C865CCdfa04490b0A9619bFBA9067 Deploys regular CryptoPunks campaigns.
First FundPunks campaign 0xF6AB3893d5C397d53ef37E18b135d6a2d8b0c1AE First regular CryptoPunks campaign.
FundPunks V1 factory 0x502424cce237e11af4b276805b67e777caa4706f Deploys CryptoPunks V1 campaigns.
First FundPunks V1 campaign 0xfd4fc93dc9a9ef389b83a9690c7a524757295d14 First CryptoPunks V1 campaign.

Third-Party Dependencies

Role Address Notes
CryptoPunks marketplace 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB Original CryptoPunks marketplace used by FundPunkCampaign.
CryptoPunks V1 contract 0x6Ba6f2207e343923BA692e5Cae646Fb0F566DB8D Original broken CryptoPunks V1 contract used for ownership and listings.
PunksMarket directed buyer 0x64e507FEBF26521b73FbdfA533106B2042533218 Adapter that safely settles PunksMarket-compatible V1 listings.
Protocol Guild recipient 0x25941dC771bB64514Fc8abBce970307Fb9d477e9 Hardcoded recipient for successful-purchase leftovers and surplus donations.

Status

FundPunks is deployed and active on Ethereum mainnet. The contracts have been tested locally, reviewed with multiple frontier AI models, and exercised through local mainnet-fork simulations before deployment.

The project has not received an independent human smart-contract audit. AI review and local simulation are useful checks, but they are not substitutes for a professional audit. Review the contracts, deployment addresses, and frontend configuration, and only risk what you can afford to lose.

Supported Campaign Types

FundPunks supports separate campaign contracts for original CryptoPunks and CryptoPunks V1. Both modes use the same contribution, purchase, refund, and surplus-donation model, but settle purchases through different marketplace contracts.

Original CryptoPunks

Original CryptoPunks campaigns use contracts/FundPunkFactory.sol and contracts/FundPunkCampaign.sol.

FundPunkCampaign buys through the original CryptoPunks marketplace listed in Active Deployments. It accepts public listings and private listings directed to the campaign contract, verifies the seller still owns the Punk, buys the Punk into the campaign contract, transfers it to the campaign creator, then donates all leftover ETH to Protocol Guild.

CryptoPunks V1

CryptoPunks V1 campaigns use contracts/FundV1PunkFactory.sol and contracts/FundV1PunkCampaign.sol.

FundV1PunkCampaign buys through the PunksMarket directed buyer against the original V1 CryptoPunks contract listed in Active Deployments. Because the original V1 contract is historically broken, V1 campaigns only settle PunksMarket-compatible listings directed to the PunksMarket adapter. Public V1 listings are intentionally rejected.

Sellers must list through the original V1 contract with offerPunkForSaleToAddress(punkId, price, 0x64e507FEBF26521b73FbdfA533106B2042533218). The campaign verifies the listing, rejects invalid Punk IDs and creator-owned seller listings, buys directly to the campaign creator, then donates all leftover ETH to Protocol Guild.

For more context on why PunksMarket-compatible listings are required and how PunksMarket settles them safely, see punksmarket.app/about.

Web App

The FundPunks web app is a Vite React interface in web/. It lets users create campaigns, contribute ETH, attempt permissionless Punk purchases, and claim refunds for regular CryptoPunks and CryptoPunks V1 campaigns.

The app is currently available at fundpunks.com. A decentralised IPFS version is served over ENS at fundpunks.eth.limo.

Frontend setup, environment variables, local development, build checks, deep links, and optional market Worker deployment are documented in web/README.md.

Launch And Verify A Campaign

You can launch a fundraising campaign from fundpunks.com or the decentralised IPFS/ENS version at fundpunks.eth.limo.

Launch From The Live Site

  1. Choose the campaign mode: original CryptoPunks or CryptoPunks V1.
  2. Enter the purchase budget, funding deadline, and execution deadline.
  3. Submit the factory transaction from your wallet.
  4. Save the campaign address from the site, the transaction receipt, or the factory CampaignCreated event.
  5. Verify the new campaign contract on Etherscan.

Verify From This Repo

Install dependencies first:

npm ci --ignore-scripts

Set the verification environment:

  • MAINNET_RPC_URL: an Ethereum mainnet RPC URL.
  • ETHERSCAN_API_KEY: an Etherscan API key.
  • CAMPAIGN_ADDRESS: the campaign contract address from the launch transaction.

Regular CryptoPunks campaigns can be verified with:

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
CAMPAIGN_ADDRESS=0x... \
npx hardhat run scripts/verify-campaign.cjs --network mainnet

CryptoPunks V1 campaigns can be verified with:

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
CAMPAIGN_ADDRESS=0x... \
npx hardhat run scripts/verify-v1-campaign.cjs --network mainnet

Each campaign is a separate contract deployed by the relevant verified factory. Etherscan verification is an offchain step, so new campaign creators should verify their campaign contract after launch. V1 campaigns can only buy PunksMarket-compatible V1 listings directed to the PunksMarket adapter.

Contracts

Key Factory Functions

Both factory contracts expose the same campaign registry interface.

  • createCampaign(uint256 purchaseBudgetWei, uint64 fundingDeadline, uint64 executionDeadline): deploys a new campaign with msg.sender as the campaign creator, records it in the factory registry, and emits CampaignCreated. The execution deadline must be no more than 365 days from creation.
  • allCampaigns(): returns the full list of campaign addresses created by the factory. This is useful for small offchain reads, but can become expensive as the registry grows.
  • campaignsRange(uint256 start, uint256 count): returns a paginated slice of campaign addresses for frontends and indexers.
  • campaignsCount(): returns the total number of campaigns created by the factory.

Key Campaign Functions

FundPunkCampaign and FundV1PunkCampaign share the same contribution, purchase, refund, and surplus flow, with different marketplace settlement logic.

  • getState(): derives the current campaign state as Funding, BuyingOpen, Bought, or Refunding. A campaign moves out of funding when the budget is reached or the funding deadline passes, becomes refundable after the execution deadline or creator cancellation, and becomes bought after a successful purchase.
  • contribute(): accepts ETH while the campaign is in Funding, records the accepted amount in contributions[msg.sender], increases totalRaised and refundableContributionsWei, and refunds any amount above the remaining campaign budget in the same transaction.
  • receive(): treats plain ETH transfers during funding exactly like contribute(). Plain transfers outside the funding state revert.
  • attemptBuy(uint256 punkId, uint256 maxPriceWei): lets anyone try to execute a listed Punk purchase before the execution deadline. The listing price must be nonzero, at or below maxPriceWei, at or below purchaseBudgetWei, covered by refundableContributionsWei, and covered by the campaign's ETH balance. Failed market checks emit BuyAttempt with success = false instead of consuming campaign funds.
  • cancelAndEnableRefunds(): lets the creator cancel a campaign before a successful purchase and immediately enable contributor refunds.
  • enableRefundsIfExpired(): explicitly marks an expired, unbought campaign as refunding and emits RefundsEnabled. This is optional because the first successful claimRefund() after expiry also enables refunds.
  • claimRefund(): lets contributors withdraw their recorded contribution after creator cancellation or execution expiry. Refunds are pull-based and sent back to the original contributor address.
  • surplusBalanceWei(): returns ETH held by the campaign that is not part of the tracked refundable contribution pool. This mainly covers forced or otherwise unattributed ETH.
  • donateSurplus(): sends any untracked surplus to the hardcoded Protocol Guild address without reducing contributor refunds.
  • donationRecipient(): returns the hardcoded Protocol Guild recipient used for successful-purchase leftovers and surplus donations.

Regular CryptoPunks campaigns have one additional recovery helper:

  • transferBoughtPunkToCreator(): retries the post-purchase transfer to the campaign creator if the regular CryptoPunk was bought and the campaign contract still owns it.

See Supported Campaign Types for the marketplace settlement differences between original CryptoPunks and CryptoPunks V1 campaigns.

Development

Use Node.js 22 LTS or another Hardhat-supported even-numbered Node version.

Install contract dependencies:

npm ci --ignore-scripts

Run contract tests:

npx hardhat test

Run a local mainnet-fork rehearsal against the real CryptoPunks contract:

npx hardhat node --fork "$MAINNET_RPC_URL"

In another terminal, deploy a local factory and create a local fork campaign:

npx hardhat run scripts/deploy.cjs --network localhost

FACTORY_ADDRESS=0x... \
PURCHASE_BUDGET_ETH=30 \
FUNDING_DAYS=1 \
EXECUTION_DAYS=1 \
npx hardhat run scripts/create-campaign.cjs --network localhost

Then run the rehearsal with the campaign address printed by scripts/create-campaign.cjs:

CAMPAIGN_ADDRESS=0x... \
PUNK_ID=7502 \
MAX_PRICE_ETH=30 \
npx hardhat run scripts/fork-rehearsal.cjs --network localhost

The rehearsal contributes fork ETH, attempts to buy the specified listed Punk through the real CryptoPunks marketplace on the fork, verifies the creator receives the Punk, verifies the campaign balance is zero, and verifies leftover ETH is sent to the hardcoded Protocol Guild recipient. It refuses non-local networks.

Run a local mainnet-fork rehearsal against a real CryptoPunks V1 PunksMarket listing:

npx hardhat node --fork "$MAINNET_RPC_URL"

In another terminal:

PUNK_ID=1234 \
MAX_PRICE_ETH=10 \
PURCHASE_BUDGET_ETH=10 \
npx hardhat run scripts/fork-v1-rehearsal.cjs --network localhost

The V1 rehearsal deploys a local V1 factory and campaign on the fork, verifies the selected V1 listing is a PunksMarket listing, contributes fork ETH, executes the campaign buy through PunksMarket, verifies the creator receives the Punk, verifies campaign balance is zero, and verifies leftover ETH is sent to Protocol Guild.

Deploy the factories:

npx hardhat run scripts/deploy.cjs --network mainnet
npx hardhat run scripts/deploy-v1.cjs --network mainnet

Verify the factories after deployment:

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
npx hardhat verify --network mainnet 0x95d75D46A32C865CCdfa04490b0A9619bFBA9067

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
npx hardhat verify --network mainnet 0x502424cce237e11af4b276805b67e777caa4706f

Verify campaign contracts after they are created:

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
CAMPAIGN_ADDRESS=0x... \
npx hardhat run scripts/verify-campaign.cjs --network mainnet

MAINNET_RPC_URL=https://... \
ETHERSCAN_API_KEY=... \
CAMPAIGN_ADDRESS=0x... \
npx hardhat run scripts/verify-v1-campaign.cjs --network mainnet

Risk Notes

FundPunks campaigns are donation-based. If a purchase succeeds, the campaign creator receives the Punk and contributors receive no token, fractional ownership, governance rights, claim on the Punk, financial return, or refund.

If no purchase succeeds by the execution deadline, contributors can claim refunds for their recorded contributions. There is no refund-claim expiry in the current contracts.

Before a purchase succeeds, the campaign creator can cancel and immediately enable refunds. This is an emergency exit for cases where it no longer makes sense to keep accepting contributions or attempting buys.

Refunds are pull-based and sent to the original contributor address. If a contributor is a smart contract or wallet that cannot receive plain ETH, its refund claim will revert. Contributors should use an address that can receive ETH.

Anyone can call enableRefundsIfExpired() after the execution deadline to explicitly mark the campaign as refunding. This is mostly useful for interfaces and observers; claimRefund() also enables refunds automatically for the first valid claimant after expiry.

Plain ETH transfers to a campaign during funding are recorded as normal contributions and are subject to the same budget cap and excess-refund behavior. Plain ETH transfers outside the funding state revert. ETH that reaches the campaign without running contract code, such as forced ETH, is treated as untracked surplus: it does not increase the refundable contribution balance or the tracked purchase pool, and it can be donated to Protocol Guild without reducing contributor refunds.

FAQ

Who gets the Punk?

The campaign creator gets the Punk if a purchase succeeds. Contributors are donating to that outcome, not buying a share of the Punk.

Can contributors get a refund?

Yes, if no purchase succeeds before the execution deadline or if the campaign creator cancels before a purchase. After a successful purchase, contributor refunds are no longer available.

Is this audited?

There has been no independent human smart-contract audit. The contracts have been locally tested, reviewed with multiple frontier AI models, and exercised through mainnet-fork simulations. See Status for the current review and deployment context.

Why Protocol Guild?

If a Punk is bought below the amount raised, the leftover ETH goes to Protocol Guild, a well-known Ethereum public-goods recipient. The recipient is hardcoded to the address listed in Active Deployments, so campaign creators and modified frontends cannot redirect the change.