Typhoon.Cash Vulnerabilities

Image for post
Image for post
Photo by Mick Haupt on Unsplash

Typhoon.Cash was released less than six days ago and already achieved more than $73m in TVL and over $400k in generated fees. It is built on Tornado.cash with the additional feature of tokenization to trade and reward.

Given its quick success, it might look like a better solution than tornado.cash. However, the following shows major shortcomings and implementation issues. Hence, we believe that it provides less privacy than tornado and more liabilities.

[1] Staking does not contribute to the anonymity set

One may assume that staking contributes to a larger anonymity-set for other users. Intuitively, a user may stake immediately upon deposit to participate in the reward. However, calling:

function stakeWithdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient) external payable nonReentrant {}

indicates a user’s withdrawal. Therefore, it reduces the anonymity set for all participants.

[2] Unstake users

One can unstake each staked user by calling:

function unstakeAndWithdraw(bytes32 _nullifierHash) external payable nonReentrant {}

with the emitted _nullifierHash (i.e., any _nullifierHash of topic StakedWithdrawal that is not in UnstakedWithdrawal events log).
One can even consider increasing his staking reward by exploiting this.

[3] Front-running displacement

The client-JavaScript generates a SNARK proof in the function “generateProofAndArgs” with the following circuit input:

{root: t,
nullifierHash: n,
recipient: T(m),
relayer: T(0),
fee: T(0),
refund: T(0),
nullifier: r,
secret: h,
pathElements: i,
pathIndices: o}

One can front-run transaction to:

function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant{}

By calling with higher gas transaction to:

function stakeWithdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient) external payable nonReentrant {}

Therefore confuse the user to think that his withdrawal has failed.
One can even use unstakeAndWithdraw(…){} function afterward to time the exact withdrawal (e.g., via the field _stakedCounter) and profile the user (see point [1]).

[4] Governance has the ability to drain the contract through Integer Overflow

function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund){

[5] Owner access

WithdrawWhiteList.sol - function withdraw(IERC20 token, uint amount, address toAddress) public onlyOwner {}

Conclusion

We have identified and shown security issues with typhoon.cash that makes it less secure than tornado. While tornado.cash is also flawed (see reference 1). We think that a perfect solution should incorporate a reward function that considers:

  1. Longevity, for increasing the anonymity set
  2. AMM
  3. Rewarding correct withdrawal/deposit behavior-profile

And it should withdraw the reward itself to an unlink address. Another feature that may help is “random” donations to some selective-profile addresses in the wild to reduce profile detection.

Software Engineer in NYC, interest in blockchain and DeFi

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store