Forking Cheeze Wizards Smart Contracts, All Funds (and Wizards!) are Secure
Some players will get an extra Wizard in the vulnerable contract, Cheeze Wizards: Unpasteurized
Within 24 hours of letting our player community know about Cheeze Wizards hitting Ethereum mainnet, one of our community members alerted our security auditors to a bug in the smart contract. The best part of the crypto community is how collaborative people are: A big thanks to @samczsun for responsibly disclosing the issue and making the game better for everyone. We’ve got a free Wizard for you, you’ve earned it!
The great news is that as a player, you’re mostly unaffected. Sales for the main tournament reopens tomorrow October 18th, your Wizards will be waiting for you: just head to cheezewizards.com and log in with your wallet.
The Cheeze Wizards smart contacts were carefully designed so that we have no ability to withdraw the prize pool (or otherwise alter any of the Tournament state) once they are published. As a result, the only way we can fix the bug is by deploying a new contract.
There are wizards belonging to 150 wallet addresses already in the vulnerable smart contract, totaling 175 ETH worth of power. This ~$40,000 is now locked in this contract, and the only way anyone can get it out is by fighting a Cheeze Wizards tournament with the current battle logic.
Last affected block: 8,759,737
Last affected wizard: 6,133
Txhash for reference: https://etherscan.io/tx/0x0d497ea959406909edad945d332d0aa1ed2a41273c694ad385910720af2f86f3
The good news is that the bug isn’t a complete deal-breaker. We can still let the vulnerable tournament run, it’ll just be one where fighting dirty is allowed. We call it Cheeze Wizards: Unpasteurized.
Unpasteurized is what we are calling the version of the Cheeze Wizards smart contracts we deployed on October 14, 2019.
CW: Unpasteurized includes a bug that can be exploited to steal power from honest players, especially those who access the game using a web interface. However, it occurred to us that this bug may also make the game more fun to play for certain classes of technically-minded folks who like to get in touch with their dark side from time to time.
Here’s what we mean.
- Player A challenges Player B
- Player B accepts and submits a move commitment
- Player A responds and submits a move commitment
- Player A reveals moves
- Player B reveals moves
- Duel is resolved on the smart contract
- Duel animation is generated on cheezewizards.com and the players find out the results.
- Player A challenges Player B
- Player B accepts and submits a move commitment
- Player A responds and submits a move commitment
- Player A reveals moves
- Player B deliberately times out without revealing (a 90-minute window)
- Player B calls the
resolveTimedOutDuel(rTOD) function with malformed parameters, providing Player A’s Cheeze Wizard in both the slots.
NORMAL: function resolveTimedOutDuel(WIZARD-A, WIZARD-B)EXPLOIT: function resolveTimedOutDuel(WIZARD-A, WIZARD-A)
^^^^^^^^
THE BAD PART
This will wipe out Wizard A’s power while also putting Wizard B in an invalid state. However, calling resolveTimedOutDuel(WIZARD-B, WIZARD-B) would fix that bad state. So, Wizard A’s power is wiped out, while Wizard B’s power is untouched. (Note that there is no power transfer from A to B during the exploit).
Interestingly enough, a malicious third party could also call rTOD to wipe out Wizard A’s power. It doesn’t necessarily have to be Player B who triggers the exploit!
As described above, it seems like Player A is always the victim here. They reveal their moves correctly and get their power drained. Sure, Player B doesn’t get to absorb that power, but they’ve wiped out their opponent and get to live on to fight another day.
Except…
Player B is taking a big risk by not revealing any moves. By the rules of CW, not revealing your moves (once committed) is an automatic loss. If an honest call to rTOD — one that correctly uses Wizard A and Wizard B as arguments — is processed before the malformed call to rTOD, Wizard B will be the one that gets drained… and Wizard A will absorb all of its power!
So we have a game of Wizard Chicken here. If a player suspects that their opponent will try to use the Dead Ringer Attack against them, they can reveal their moves and then it’s a race to see who can get their rTOD in first. And the honest player has a bigger reward if they win that race, because they get to absorb their opponent’s power; someone using the exploit can only drain their opponent and won’t ever increase their power.
Oh, and don’t forget that we are running a set of server daemons that will fire honest rTODs automatically. 😼
Let me be absolutely clear: CW: Unpasteurized is not for everyone. You should be fully aware of the risks. We suspect many folks will be running automated scripts to try to play both angles of this issue.
Think you’ve got the guts to handle unpasteurized milk? Suit yourself. We hope you will grow strong bones from this experience. Join our Discord and look for the #Unpasteurized channel to share your experience.
Starting now you can buy Unpasteurized Wizards on unp.cheezewizards.com. (Buyers beware: these Wizards will be a part of the tournament that has the known exploit!)
Sales on CheezeWizards.com (using the patched contract) will restart next week.
Strap in, we’re going deep!
To handle the case where a user either accidentally or maliciously doesn’t submit their revealed moves after committing to a battle, our smart contract allows a user to do a “one-sided reveal” where they submit their moves independently from the other user. This is an exceptional occurrence, only used when one user goes dark. By default, our interface uses a “double reveal”, which is more gas efficient.
The rTOD exploit can only be triggered in the case where a duel has a one-sided reveal and has also timed out. It is very easy for a participant of a duel to arrange this: They just need to provide their commitment and then drop off the network.
Once there is a timed-out duel with a single reveal, any malicious user can submit a malformed transaction to drain the Wizard that revealed their moves.
Assume that Honest Hermione is using Wizard #1000 to battle Byzantine Belinda, who is using Wizard #2000 and uses the Dead Ringer Attack. Both Wizards commit their moves and enter into a duel. Honest Hermione reveals her moves, while Belinda waits for the duel to time out and calls resolveTimedOutDuel(1000, 1000). Let’s walk through that function call in the smart contract.
function resolveTimedOutDuel(uint256 wizardId1, uint256 wizardId2)
Finally, the smart contract executes a power transfer where it thinks it is giving all power to the winning Wizard, then draining it all from the losing Wizard. However, since both wiz1 and wiz2 are references to the same Wizard in storage, so we are first doubling its power, and then draining its power to zero. 🤦♀ 🤦♂
Thankfully, this bug is easily fixed by adding a simple require statement at the top of the function to make sure that the two Wizards IDs are different.
require(wizardId1 != wizardId2, “Same Wizard”);
It’s worth noting that these smart contracts have undergone formal security reviews by Sigma Prime and we’re confident there are no further issues to keep the tournament from running as expected.
If you’re a hacker looking to get a little dirty (or excited about the opportunity to turn the tables on someone trying to get dirty themselves!), jump over to unp.cheezewizards.com. But don’t say we didn’t warn you… this is dangerous stuff! There will be upset stomachs and wallets.
For you hackers who prefer to keep it clean, the best way to participate is to join the CheezyVerse! Join the dozens of other teams and build useful tools for players in the main tournament.
If you’re not a developer, sign up for an account on cheezewizards.com and just hang tight — we’ll email you once sales reopen for the main tournament.
Published at Thu, 17 Oct 2019 17:08:06 +0000
{flickr|100|campaign}
