Let’s take a look on very interesting vulnerability that actually happened quite a while, but the quality is still astonishing till our days.
First and foremost goal of the “AnySwap“ was to allow users to swap between any two chains freely. It reduces fees and makes it easier to move between chains. It worked in such way:
The router accomplishes this by wrapping the original token with an "anyToken." For instance, DAI is wrapped as anyDAI, making DAI the underlying asset of anyDAI. This wrapped token is utilized for internal accounting within Multichain. When a user "transfers" DAI from Ethereum to BSC, anyDAI is added to the Multichain anyDAI contract on BSC and burned (subtracted) from the anyDAI contract on Ethereum.
The previously mentioned exploited function, anySwapOutUnderlyingWithPermit
, facilitates the swapping of an underlying token using the ERC20 permit()
function. This function allows users to authorize a contract to spend their funds by providing a signed approval transaction without needing to submit it directly to the blockchain, thereby reducing the user's gas costs. The signed transaction is represented using the parameters (v, r, s).
So, if we divide the logic by peaces, we would have the following:
First line extracts the address of the underlying token (e.g., "DAI") from its wrapped version (e.g., "anyDAI")
Call the permit
on the underlying token to approve
to the router
Router safeTransferFrom
the token from the user.
The remainder of the function handles the accounting processes associated with the wrapped version of the token and manages its transfer across different chains
It worth to mention, that this function was used only once - during actual exploit. It means that contract had some other entry-point for the users to interact with the protocol.
Now, let’s take a look on the inputs provided by the attacker and try to analyse what has happened exactly.
from is victim address
token is attackers’ deployed contract
to is destination address (attacker wallet)
amount is the value to drain from the respective user
deadline is time till the permit is valid
v,r,s are all zeros
Eventually, how did attacker managed to steal funds? Here is the most interesting part.
Attacker provided the token, as an attackers deployed contract. It was necessary because the AnySwap .underlying()
method was called on it. Due to the lack of validation attacker could return any _underlying
value. He returned WETH address, let’s see why.
The intended behaviour should be: user calls permit on WETH contract with v,r,s
to approve the router the ability of withdrawing tokens. But, the WETH token has no permit method!
However, the WETH has the fallback function. And once the WETH can’t distinguish what function to call, since there is no permit
method, the fallback would be called
The function TransferHelper.safeTransferFrom(_underlying, from, token, amount);
was originally expected to operate under the assumption that the signature verification in the preceding step was successful. This would imply that the approval granted by the signature could be used to transfer the specified amount from the user's account to the router. However, as observed, the signature validation did not occur as intended.
In theory, this should not have been an issue because, although the attacker’s input should not have passed the signature check, the router would not have been approved to transfer the funds on behalf of the victim. However, the AnySwap had requested an almost infinite approval limit from all its users to save on gas fees.
Eventually, the problems were because of the following:
Not useful function, which wasn’t used by the user at all (no code simplicity)
Vulnerable function didn’t validate that the token inputed is indeed a valid AnySwap token.
Vulnerable function didn’t validate that the permit call was actually called