Frank Poncelet

cryptopunks · solidity · smart-contracts

Immutable, including the mistakes

27 Aug 2026

When Matt Hall and John Watkinson released the CryptoPunks in June 2017, there was a bug in the contract. If I bought your punk, I got the punk, and I also got my money back. You got nothing. That was pretty bad for the seller. Great for the buyer.

This is the story of that bug, of the five years the original contract spent asleep, and of the Christmas holiday I spent bringing it back. It is also a story about the one thing every smart contract developer knows and nobody really feels until it happens to them: once you deploy, you live with it. Matt and John live with their bug. I got lucky with mine, and I will explain why.

The bug

The V1 contract went live on 9 June 2017. Ten thousand punks, free to claim, gas at well under a dollar. The claim took about a week; Castor and Gerard trace the rush to a Mashable article on 16 June, after which the rest went within a day. Then people started trading, and Hemba, one of the early claimers, noticed that the withdraw function was handing him more ether than he was owed. He used it. More than once. It made him a bit notorious.

Here is the verified source; the whole problem is in one function.

function buyPunk(uint punkIndex) payable {
    Offer offer = punksOfferedForSale[punkIndex];
    // ... checks ...
    punkIndexToAddress[punkIndex] = msg.sender;
    balanceOf[offer.seller]--;
    balanceOf[msg.sender]++;
    Transfer(offer.seller, msg.sender, 1);

    punkNoLongerForSale(punkIndex);
    pendingWithdrawals[offer.seller] += msg.value;
    PunkBought(punkIndex, msg.value, offer.seller, msg.sender);
}

function punkNoLongerForSale(uint punkIndex) {
    if (punkIndexToAddress[punkIndex] != msg.sender) throw;
    punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, 0x0);
    PunkNoLongerForSale(punkIndex);
}

In Solidity 0.4, Offer offer = punksOfferedForSale[punkIndex] is not a copy. It is a pointer into storage. The function hands the punk to the buyer, then calls punkNoLongerForSale, which overwrites that same storage slot with a fresh Offer whose seller is msg.sender. The buyer. The next line credits pendingWithdrawals[offer.seller], and offer.seller is now the buyer. The ownership check inside punkNoLongerForSale doesn't stop it, because by then the buyer is the owner. The one safeguard in the path is what lets the overwrite through.

Notice that the balances were adjusted before the overwrite, so the token side of the trade was correct. Only the ether went to the wrong address. Even the PunkBought event logs the buyer as the seller. Everything looked fine until you counted your money.

The V2 contract fixes it with one line, address seller = offer.seller;, cached before anything changes. That is the whole fix. It is the kind of mistake you look at and think: I could have made that.

Why didn't they catch it? They did test. They tested the way two developers test a marketplace: I buy yours for one ether, you buy it back for one ether, we both hit withdraw, we both get a nice round number. They were getting their own money back and the numbers looked right.

Five years asleep

Larva Labs did what you do when you cannot patch: they deployed again. Castor and Gerard have Larva Labs tweeting a warning on 17 June and, five days later, on 22 June, re-minting all ten thousand punks in a new contract and airdropping each one to the wallet holding its V1, unless that wallet had got it through the buggy buyPunk. The new contract was a copy of the old one with the bug fixed and one feature added: bidding, which the first version never had. Matt Hall's own words about that week, as Castor and Gerard quote him: "We thought we had blown it."

The punks everybody knows, the ones that sold for millions, are the V2s. The first contract was forgotten. Not deleted, because nothing on Ethereum gets deleted. It sat there for about five years, and the ten thousand tokens in it sat with it, in the wallets of people who had claimed them and, in some cases, never touched that wallet again.

Most articles about the history of the punks give 23 June 2017 as the launch date. Castor and Gerard put it well: the media write it up "as if the initial botched launch never happened." But it did happen, and the evidence is in block 3842489, and it will be there after all of us are gone.

A Christmas well spent

In September 2021 we released We Are Dorkis, a collection of hand drawn characters, pen and coloured pencil on paper, by an artist in New Zealand. I wrote the contract and I wrote a custom art assembly engine for it, because the usual generators could not cope with pencil drawings.

One of our biggest holders was Psyborg, who at one point held around two hundred Dorkis. He was also, it turned out, an original punks claimer with 83 V1s in his wallet that he could not sell. In November 2021 he came to me with the story you just read and one question. Can you fix it?

I told him he had just ruined my Christmas holiday. Then I went and spent the rest of the day reading up on the punks.

I was not the first. There had been an earlier attempt at a wrapper that never rendered a token. Then in March 2021 a developer called 0xfoobar deployed one that did work: a contract that takes your V1 punk into custody, untouched, and mints you an ERC-721 in exchange, so the thing becomes tradable on OpenSea like any modern token.

0xfoobar's wrapper had several problems, and none of them was in the code. The first was that it put a red ribbon on the art. On a 24 by 24 pixel image a ribbon is a disaster. The second was that there was no community, no marketplace, and nobody telling the story. It had also been hit with a DMCA, and 0xfoobar never countered it, so OpenSea had taken it down.

We did ask 0xfoobar to come on board. Working with us meant dropping the ribbon, and he refused. His contract is still there, and still holds nine V1s.

So we made a new one. We were, at that point, about ten people in a Discord that Psyborg had set up: Leonidas, who was writing the early history of NFTs on Twitter, Hemba, a few others, and me as the engineer. We voted on the background colour and lavender won. And we made one decision I still think was the right one: we did not touch the art. Both contracts point to the same image hash, and changing it would have made the V1s a derivative.

The verified V1 and V2 contract sources side by side, both declaring the identical imageHash value

My wrapper is based on 0xfoobar's code, and says so in the smart contract header. What I added was code to make it work efficiently with a marketplace of our own. I wrote the marketplace contract and a helper contract next to it; the front end was built by a second developer, Simstone, from my instructions. The marketplace pays the seller in the same transaction as the sale. There is no withdrawal function, unlike the V2 contract. That is simply how you build a marketplace in 2021, more compatible and more efficient. It also means there is nothing sitting in the contract for anyone to withdraw incorrectly.

The reason we built our own marketplace at all is less obvious. We expected a DMCA. We assumed that at some point Larva Labs would try to have the wrapper removed from OpenSea, and we wanted somewhere to trade when that happened. So the backup existed well before the legal attack did.

Nobody reviewed the code except me. I want to say that plainly, because it matters for what comes later.

The wrapper went live on 17 January 2022, in block 14022431. The marketplace followed a week later.

Six weeks

What happened next happened fast, and most of it was not done by us.

On 25 January Larva Labs tweeted, in the words Decrypt recorded: "PSA: 'V1 Punks' are not official CryptoPunks. We don't like them, and we've got 1,000 of them... so draw your own conclusions." I had about six thousand followers on Twitter at the time. They had about a hundred thousand. Every time they told their audience not to look at the V1s, a hundred thousand people looked. Adam McBride, quoted by Castor and Gerard, said Larva Labs "fumbled the ball completely, and actually did the V1 community huge favor." The Streisand effect in action.

The Larva Labs PSA tweet of 25 January 2022, ending: Any proceeds will be used to purchase real Cryptopunks!

Then it got stranger. One of the two founders had been wrapping punks from his own stock, using my wrapper, and selling them from a side wallet, starting before that tweet went out. The community noticed within days, because the blockchain doesn't lie and there is no Tornado Cash for NFTs. On 2 February Matt Hall apologised on the Larva Labs Discord. Decrypt has the statement: "We made a mistake by interacting with this contract. We thought that by announcing our intentions and selling some of the tokens, we would signal our distaste for it, and maybe others would follow. That was a bad decision. We regret it, and we apologize to the community." Hall put the proceeds at 210 ETH and promised a matching donation to the Rainforest Foundation. The community calls those tokens the rainforest punks.

The same statement announced they were coming for us. Also from Decrypt, Hall's words: "Let there be no confusion about the legitimacy of this 'V1' project. It has no right to use the art or the name. We will be taking appropriate steps in the coming days." Within the week the DMCA notice reached OpenSea, and by 11 February CryptoSlate was reporting that OpenSea had delisted the collection. LooksRare and the others kept trading.

Yes, we were a target now. Hemba paid for a lawyer in the United States, whose view came down to one sentence: you cannot DMCA your own art. We filed a counter-notice. Under the DMCA process the other side then has two weeks to sue, or the listing comes back. And our own marketplace, the one we had built for exactly this, was already live.

In those two weeks the ground moved. Yuga Labs bought the CryptoPunks IP from Larva Labs, in March 2022. Larva Labs did not sue. In the Discord, the word was that Yuga would not pursue it, and by mid March we were back on OpenSea. We had been off it for four or five weeks. It was a change of ownership that saved our skin, not a successful legal battle.

For the record: I hold several V1s, including a full pair, the V1 and V2 of the same punk. The V1 community's campaign to "get the story straight" is also, whatever else it is, people talking up something they own. And Hemba, who used the bug in 2017, has since returned most of the punks he took, the chain confirms it.

My own mistake

In late October 2022 the original marketplace site stopped working. I called Simstone.

The marketplace was two contracts, both mine. The marketplace itself, which does the buying and selling, and a helper, which does nothing but aggregate data so the website does not have to make a thousand calls one at a time. Give it a list of punk ids and it returns everything about them in one blob. And it had stopped responding.

Someone had burned a wrapped V1. Sent it to the zero address. The helper, working through its list, asked the wrapper who owned that token, and the wrapper, being an ERC-721 that follows the standard, threw an exception, because a token with no owner does not exist. The helper did not expect an exception from a read call. It gave up, and the site went down with it.

I had not foreseen that. It was stupid. It took a day or two to find, fix and redeploy a helper that checks for the zero address first; the new one went live on 27 October 2022. Nobody lost anything.

To be precise about it: the bug was not in the wrapper. The wrapper did exactly what the ERC-721 standard says it should do with a token that has no owner. The bug was in my helper, which assumed a read call could not fail. And a helper can be replaced. The wrapper, where the punks are, cannot.

Where it stands

It is August 2026. The V1s are tradable and will stay tradable, because nobody can switch the wrapper off, including me. The marketplace we built has since been retired; Rarible offered to build us a new one for free, and we took it. In 2022 I could not have walked into a punks brunch, they would have hanged me from a big tree. Now I am "welcome" again. I still feel some knives in my back, and I assume some V2 holders will never forgive me. That is fine. I did not do it for them.

The CryptoPunks IP has since changed hands twice: to Yuga Labs in 2022 and, in May 2025, to the Infinite Node Foundation, a nonprofit whose advisory board includes Matt Hall and John Watkinson, as Decrypt reported. The history as it is usually told starts on 23 June 2017. I think each of those owners would prefer that the first two weeks did not exist.

They do exist. Somewhere in a Gnosis Safe that passed from Larva Labs to Yuga, and that Yuga still holds, whatever the IP paperwork now says, there is a single V1 punk that one of the founders wrapped in January 2022 and never unwrapped. OpenSea hides it. It is still there. It is a small thing, but not many people can say that something they wrote is sitting in that vault.

No end

I called this piece "immutable, including the mistakes" and I meant both words.

The contract Matt and John deployed on 9 June 2017 will outlive Larva Labs, Yuga, the Node Foundation and me. So will the fix they deployed two weeks later, and so will my wrapper. That is the deal you make with this chain. It keeps your work and it keeps your errors, and it does not let anyone, including the people who made them, decide which parts of the history count.

This story does not have an ending, because the chain does not provide one. It just has a next block.