Typhoon.Cash Vulnerabilities

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){where withdrawalMax is a constant equal to 10000
and withdrawalFee is set at
function setWithdrawalFee(uint _withdrawalFee) external onlyGovernance{}
Setting withdrawalFee to =(withdrawalMax*X/denomination) and sending _fee of =(2^256-X) and _relayer of =_recipient_address.results in:uint256 _withdraw_fee = denomination.mul(withdrawalFee).div(withdrawalMax);require(_withdraw_fee + _fee <= denomination, "Fee exceeds transfer value"); // pass 0<=denominationthat invokes ERC20Tornado.sol line 39 _processWithdraw(address payable _recipient, uint256 _withdraw_fee, address payable _relayer, uint256 _fee, uint256 _refund)which makes calls to:_safeErc20Transfer(reserve, _withdraw_fee); // lose X
_safeErc20Transfer(_recipient, denomination - _withdraw_fee - _fee); // get 0
if (_fee > 0) _safeErc20Transfer(_relayer, _fee); // get (2^256-X)
Will be able to withdraw (2^256-2*X) to the recipient

[5] Owner access

WithdrawWhiteList.sol - function withdraw(IERC20 token, uint amount, address toAddress) public onlyOwner {}ERC20YFIRewards.sol - function seize(IERC20 _token, uint amount) external onlyOwner {}ERC20Tornado.sol - function seize(IERC20 _token, uint amount) external onlyGovernance {} // not owner but governance here

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

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