Without Permit: Multichain’s exploit explained

Tal Be'ery
Zengo Wallet
Published in
6 min readJan 23, 2022

A few days ago Multichain’s users were hacked by several attackers groups, all abusing the same vulnerability in Multichain (previously AnySwap) smart contract. The attackers were able to steal tokens valued at millions of US Dollars.

While we have been monitoring the (very interesting!) details of this hack closely, in this piece we would like to focus on the technical aspects of this vulnerability.

The tools:

To analyze this vulnerability we will primarily use tenderly’s debugger, a tool I was just recently made aware of and seems like a great addition to Ethereum’s researchers’ toolbox.

Tenderly debugger allows its users to replay the execution of a smart contract transaction, including all of the parameters, internal function calls and the state of the blockchain at that time, stepping into each line of the open source smart contract’s code.

Let’s jump right in and analyze one of the highest sum exploit transactions, responsible for the theft of 308 ETH ( ~$950K).

The attacking transaction in etherscan

Let’s provide this Transaction hash to the Tenderly Debugger and see what it can tell us about it.

The attacking transaction in Tenderly

The debugger leads us to the function in Multichain code that the attacker probably abused. This is anySwapOutUnderlyingWithPermit() function in the anyswapRouterv4.sol contract.

But in order to understand the abuse, we need to understand its normal functionality first.

anySwapOutUnderlyingWithPermit() explained

Multichain (previously AnySwap, hence the name) router mission, according to website:

Multichain Router allows users to swap between any two chains freely. It reduces fees and makes it easier to move between chains.

To do so, the router wraps the actual token with its “anyToken”. For example, the DAI token is wrapped as anyDAI, or conversely DAI is the underlying asset of anyDAI. The wrapped token is used for Multichain internal accounting and when user “transfers” DAI from Ethereum to BSC, actually anyDAI is added on Multichain anyDAI BSC contract and burned (subtracted)on anyDAI Ethereum contract.

The aforementioned exploited function anySwapOutUnderlyingWithPermit, is swapping an underlying token using the ERC20 permit() function. The permit() function allows its user to supply a signed transaction of approving a contract to spend its funds without actually sending it to the blockchain, which can help in minimizing users’ gas cost . The signed transaction is expressed in (v,r,s) terms.

The vulnerable function (Source github)

After this introduction, we can now understand the anySwapOutUnderlyingWithPermit() functionality:

  1. address _underlying = AnyswapV1ERC20(token).underlying(); unwraps the underlying token (“DAI”) from the its anyToken wrapping (“anyDAI”)
  2. IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s); The underlying token’s (“DAI”) ERC20 contract permit() is called to approve the router’s (this) ability to withdraw an amount from the user’s (from) address, as the user supplied a signed transaction for that, denoted by (v,r,s)
  3. TransferHelper.safeTransferFrom(_underlying, from, token, amount); if we got to this line it means the signature in the line above was verified and now we can use the approve granted by it to the router, to actually move the the amount from the user to the wrapped token account.

The rest of the function deals with its wrapped version accounting and sending across chains.

It’s worth noting that according to a query we created on Dune Analytics this vulnerable function was never in actual use, and its first use was by the exploit on January 18th. This means this function was actually a deadwood that just increased the contract’s attack surface.

anySwapOutUnderlyingWithPermit() was never used before the attacker used it on January 18th (source: Dune Analytics)

The exploit

Now let’s take a look at the parameters the attacker passed to the vulnerable function

The attacking transaction’s function call parameters in Tenderly

from is the address of the victim, the token is the attackers’ deployed contract and so is the to destination address.

We can see that the attacker is not passing a valid signature as v,r,s are all zeros.

The attacker is trying to get 308 ETH to its contract with an invalid signature. How can it work?

  1. address _underlying = AnyswapV1ERC20(token).underlying(); It’s intended to unwrap the underlying token (“DAI”) from the its anyToken wrapping (“anyDAI”). However, token now is now the attacker’s controlled contract. We can see in the debugger, that this contract now returns WETH as its “underlying asset”. Multichain failed here as this function should have checked if the token address is indeed a Multichain token
underlying() output is WETH contract address 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (Tenderly)

2. IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s); Originally, the expected result was that the underlying token’s (“WETH”) ERC20 contract permit() is called to approve the router’s (this) ability to withdraw an amount from the user’s (from) address, as the user supplied a signed transaction for that denoted by (v,r,s). However, WETH contract does not have a permit() function! WETH contract does have a “fallback function” that is called when a function is called but not found. WETH’s fallback function is deposit() that does nothing material in this case, but allows its calling function’s execution to continue as it does not fail.

WETH contract does not have Permit(), fallsback into deposit() (Tenderly)

3. TransferHelper.safeTransferFrom(_underlying, from, token, amount); Originally, we expected that if we got to this line it means the signature in the line above was verified and now we can use the approve granted by it to actually move the the amount from the user to the router. However, the signature was not verified, as seen above. In theory, it should not be a problem, as although the attacker’s input should not have passed the signature validation, it did not approve the router access to transfer the funds on the victim’s behalf. However, Multichain’s dapp requested from all of its users a practically infinite approval sum. This insecure methodology is quite common in dapps, to save user expenses on gas. We had warned in the past that such behavior (we named it baDAPProve) can be hazardous in case of a rogue or a vulnerable dapp, and now this potential threat had materialized. By abusing this excessive approval, the function transfers the WETH amount from the victim account to the attackers’ controlled contract.

Now that the attackers got the victim’s funds, they just need to make sure the function will not fail, and this transfer will not be reverted and that’s what the rest of the code does.

Summing up

As often happens with both accidents and vulnerabilities, the vulnerability discussed here had multiple contributing factors, and probably even getting one of them right would have helped Multichain’s users in not getting exploited:

  1. The vulnerable function was not actually used and could have been removed to begin with.
  2. The vulnerable function did not validate that the token input parameter is indeed of a valid Multichain token.
  3. The vulnerable function did not explicitly verify that it indeed successfully calls the non-mandatory ERC20 permit() function.
  4. The dapp applied the “BaDapprove” behavior, that made its users approve this vulnerable contract infinite access to their funds.

Additionally Multichain did not apply any upgrade mechanism to this contract, and therefore the users’ only line of defense is to individually revoke their previous approvals.

Our hope is that by sharing this research and learnings, we can all enjoy a more secure Web3 environment.

--

--

Tal Be'ery
Zengo Wallet

All things CyberSecurity. Security Research Manager. Co-Founder @ZenGo (KZen). Formerly, VP of Research @ Aorato acquired by @Microsoft ( MicrosoftATA)