🌮 Tacoclicker

A simple blog setup to keep everyone up to date with whats happening with tacoclicker

Created by @mork1e

  • By: @mork1e

    TLDR:

    • V1 contract had 2 critical vulnerabilities (gas bias + CPFP abuse)
    • V2 fixes both, makes the contract upgradable, and restores fairness.
    • 1.08 million tortilla burned to offset the impact of one of these issues.
    • Economic design has been improved: salsa rewards are now vested, winners have to wait 7 days before participating in another salsa block, and a minimum of 1 salsa bar is now required to participate in the salsa block.
    • $MIST airdrop was fixed.

    Lets jump straight into the why…

    Why is Tacoclicker migrating the $TORTILLA Alkane id? There are numerous reasons, but here are the biggest ones:

    • Vulnerabilities: The contract had 2 very high-risk vulnerabilities which I discovered privately in the following days after deployment. The deployment of the V2 contract represents my response to finding out about these. It is important to note: none of these vulnerbilities were exploited during the lifespan of the V1 contract (one was taken advantage of, but that situation has been fully taken care of as discussed further down this post). This post will cover the details of each one of these, including the technicals of the exploit path that could have been taken, and how the new contract mitigates these.

    • Playtesting did not match my initial market dynamics assumptions: When making Taco Clicker, I had to make a lot of assumptions on how some game mechanics would influence market dynamics, without actually seeing the effects live. Some of these initial assumptions on how the game mechanics would infuence the market turned out to be wrong. The V2 contract contains community proposals that fix these gaps that my assumptions when making the V1 contract overlooked.

    • Contract was non upgradable: I made the assumption that the Taco Clicker contract wouldnt have to be upgraded, which leads us to the situation we are now where a new Tortilla alkane is required. The V2 contract now contains an upgradable path, which would allow future upgrades (if they are needed) to happen without the deployment another new Tortilla Alkane ID.

    • Missing airdrop for $MIST holders: The holders list for $MIST holders was incorrectly built, causing any merkle proofs to claim the $TORTILLA airdrop for $MIST holders to be invalid. This contract contains the fixed merkle root, and the fixed merkle tree that correctly accounts for $MIST holders. Those who reached out to me saying they couldn’t claim after holding $MIST i’ve dmed by now about this fix, and if you didnt reach out but ran into this issue I urge you to try claiming again through https://tacoclicker.com/airdrop

    V1 vulnerabilities and how V2 fixes them

    Both of the vulnerabilities im about to touch on were not made public for a pretty obvious reason:

    to prevent people exploiting them while the V1 contract was live.

    Now that these are fixed, it is safe for me to explain what these were – and how I went about fixing each one of these.

    Vulnerability 1: Highest gas payer wins the salsa block

    This was unintended, the winner of the salsa block from the beginning was supposed to be purely random. The logic in the V1 contract for this was almost completely correct, except for one line of code that was causing all the trouble.

    See, the way the salsa block winner is chosen deterministically is through a hash competition. Every bet transaction generates a hash in the following manner:

    whats happening here is that a unique hash is generated for each transaction by doing

    sha256(txid XOR blockhash)

    then, one by one, each transaction checks if their current hash has a lower value than the current winner for the block, and if it does, this transaction becomes the new current winner for the block (that is, until another transaction beats it)

    By doing this for every transaction, by the time the last transaction has been processed in a block a “winner” would have been determined, as no other transaction was able to replace this hash.

    So what went wrong? It all has to do with the job the first transaction of the block has:

    The first transaction of the block is supposed update the lottery state from the last salsa block and create a new lottery for the current block. This would setup the lottery for the current block so every other subsequent transaction in the block would be able to participate.

    However the way its implemented above in the V1 contract is wrong and unintentional. What the code snippet above does instead is update the current block for the lottery state, but also causes all subsequent transactions in the block to prematurely end. This is because this statement is true for every other transaction following the first tx:

    salsa.current_block == current_height 

    The reason this is true is because the first transaction has already set this. The issue lies in that the logic for this IF/ELSE clause was only intended for immediatley aborting if the current block wasnt the salsa block, and by also pairing the check for the current block here, it incorrectly caused every transaction after the first one to abort prematurely.

    What I had actually intended to write here was this (this is a snippet from the V2 contract)

    First check if the current block is the salsa block, and THEN check if the lottery needs to be initialized by the first transaction. This alone stops the effect that was happening with the V1 contract, where subsequent transactions to the first one were prematurely terminated (meaning that the Salsa block winner is no longer the transaction that pays the highest fee).

    The aftermath (and what I did to make things right):
    Because of this bug, a bot was able to RBF its way to winning 2 salsa blocks – unfairly winning what was supposed to be a purely deterministic random lottery two times – for a total balance of 1.08M $TORTILLA (2x salsa rewards @ 540k tortilla per salsa block). This attacker then later listed 1.08M tortilla @ an average of 0.95 SATOSHI (a very steep discount from the 6-7 sat range tortilla was trading at the time) – causing the price of $TORTILLA to plummet because of the sell pressure.

    To offset this, two large $TORTILLA burns have been orchestrated:

    ✅ These two burns together FULLY offset the 1.08M tortilla bought by the attacker. Further more, this bug has been fully fixed in V2 and no more attack surfaces related to gas paid exist anymore.

    Vulnerability 2: CPFP chain re-entrency attack could’ve led to exponential emissions on the bet_on_block function

    This one is more catastrophic, and thankfully I caught this early and patched it before anyone was able to find it and exploit it. To understand what the issue was here we first have to understand how the multipliers for the “betting” game in Taco Clicker are generated.

    Multipliers are determinisitcally generated through this PRNG function, which takes in the current block hash as a seed and determinisitcally produces a random u128 representing the multiplier for that block. That means, EVERY bet on that block and their target multipliers will be compared against this block multiplier.

    If you have played the betting game on Taco Clicker, you’ll know that you can only bet once per block with your Taqueria, and that what you are wagering is your unclaimed tortilla. The reason you can only bet once is because your auth alkane (which is your taqueria alkane id) is being used in an unconfirmed transaction, and has to be confirmed before you can use it again.

    AND that right there, was where my assumption was wrong, because of CPFP (Child pays for parent)

    CPFP is a feature on bitcoin that lets you use UTXOs produced by unconfirmed transactions in a new transaction, given that this new transaction (the parent) has a higher gas fee than the child transaction. Below is a snippet on what the bet_on_block function used to look like

    This is bad, because there is no check being done to see if this taqueria has already bet for this block. This means that through a CPFP chain, given that the multiplier is a winner (above the users target multiplier), the user could rebet and rebet and rebet their unclaimed tortilla, compounding exponentially their unclaimed tortilla on each winning bet – all in one block.

    The attack would go like this:

    • Attacker submits a bet for target multiplier of 2x for their unclaimed tortilla, and the block multiplier hits 3x. This puts the attackers bet in a win state.
    • The attacker then uses the UTXO from this first transaction to rebet that SAME unclaimed tortilla, on a multiplier that will already be 2x. This means the attacker is actually earning 4x from their initial bet.
    • The attacker keeps doing this, creating a CPFP chain that compounds their winnings from their last bet:
    • TX1: 2x bet, 1000 ->2000 (vins: anything, doesnt matter) ->
      TX2: 2x bet, 2000 -> 4000 (vins: [TX1]) ->
      TX3: 2x bet, 4000 -> 8000 (vins: [TX2]) ->
      TX4: 2x bet, 8000 -> 16000 (vins: [TX3]) -> etc.. etc..

    The attacker can do this because their is no guard clause that prematurely ends the transaction if a Taqueria has already bet. V2 fixes this by adding that missing guard clause and making taquerias aware of that state (whether theyve bet on that block yet):

    This completley removes this attack vector, as taquerias can now forcefully only bet once per block.

    The aftermath?
    ✅ Nothing happened. I caught this early while auditing my own code before anyone could take advantage of this.

    V2 upgrades

    The following are suggestions from the Taco Clicker community that have been implemented in the V2 contract, along with the rationale for doing so.

    🌶️ Upgrade 1: Salsa rewards are now linearly VESTED to the winner

    In the V1 contract, salsa winners would receive a lump payout of 25% of the entire production of the game for a day. For the first halving of the game, this equates to a whopping 540k $TORTILLA for the salsa winner. Quickly it became apparent to me and the community that such a large lump sum payout could be devastating for the market, as there was not enough liquidity to absorb so much sell pressure at once if the salsa winner decided to sell.

    V2 mitigates this by linearly vesting the Salsa winner their $TORTILLA reward over 7 days. While the final sell amount (given that the winner decides to sell their reward) is the same, the market has a much better chance at absorbing the amount.

    How it was implemented:

    On V2, if you win a salsa block your $TORTILLA/BLOCK will be boosted as your linear vesting is occurring. Linear vesting lasts 1008 blocks (7 days roughly), and during this time every block you will recieve 1/1008th of your salsa reward in your unclaimed tortillas every block.

    On the first halving, where the salsa reward is 540,000 TORTILLA this equates to 535.714286 tortilla/block.

    This achieves two things:

    1. Significantly reduces sell pressure as users have to wait 7 days before fully cashing out their tortilla. Interestingly – it also makes the winner log back in periodically to the site to check how much has been “unvested”, any of which times they might buy an upgrade. On tacoclicker a conversion is someone buying an upgrade, as it benefits everyone. If a user has to keep logging back for their linear vesting, any of these times is a window in which they might claim a portion of their winnings through their unclaimed tortillas and convert by buying an upgrade.
    2. Salsa winners become emotionally attached to this new temporary TORTILLA/BLOCK they are granted as a result of the salsa reward boost. The vesting window is long enough for a user to equate their boosted tortilla/block as if it were “theirs”, only to lose it once the 1008 block vesting window is over. The assumption im making here is that when the boost finally ends because vesting is completed, they will want BACK the tortilla/block they were getting with the boost – driving them to buy upgrades off the market.
    🌶️ Upgrade 2: Timeout of 7 days before a Salsa winner can participate in another salsa block

    This is a positive by product of upgrade 1. Salsa winners attempting to participate in a new salsa block will not be able to do so until their vested amount of $TORTILLA rewards haas been fully paid out. This opens the door so other players that haven’t won yet have a greater chance of doing so.

    🌶️ Upgrade 3: Taquerias must have a minimum of 1 salsa bar (or higher) to participate in a Salsa block

    This was suggested by the community so new taquerias drive buy pressure to $TORILLA as they need 3,000 $TORILLA to buy a salsa bar (around $30-40 USD ATT). It also makes it harder for people trying to take advantage of the game by buying many taquerias to skew the chances in their favor of winning the salsa block. If someone wants to do this now, they also have to prop up the market by buying and burning $TORTILLA for their Salsa Bar upgrade.

    V2 Fixes

    Ill keep these short as I already talked about them in the headnote of this post.

    • The contract is now upgradable, meaning that the taco clicker contract can be swapped out without changing the Tortilla Alkane ID. This means that the Taco Clicker community wont have to go through this process again in the scenario the Taco Clicker contract requires another upgrade.
    • Airdrop for $MIST holders has been fixed.

    On an ending note..

    This upgrade represents a community effort to stabalize taco clickers game mechanics and contains bits and pieces suggested by different players of the game. If you are a frequent player of Taco Clicker and want to have an impact on the games future, you should join the community telegram and speak your thoughts out: https://t.me/tacoclicker

    And finally, thank you for playing my game 🙂