"Phantom" in your Smart Contract
A Solidity-coded Smart Contract is a class that encapsulates methods that include determined functionality and logic. Invoking a non-existent function within a smart contract typically results in a transaction being reverted. However, Solidity has a feature called "fallback", which means that any function call to the contract that does not match a matching function name is caught by the fallback. While this provides a useful purpose, it also carries a danger. Let's look at why it's important to be careful while using the fallback in the next section.
History of “fallback”
Before the release of version 0.6.0
, Solidity has the following fallback syntax:
function() public payable{}
The above method handles any incoming "calls" that send native tokens (ETH, BNB, etc.) to the contract. It also serves as the receiver for function calls that are not specified in the contract. Perhaps the Ethereum/Solidity team identified a problem, which resulted in the introduction of Breaking Changes in version 0.6.0
. This required a syntax change as well as the separation of two functions with the following functionalities:
receive() external payable
: is triggered when the call data is empty. Its purpose is to handle the receipt of native tokens sent to the smart contract.fallback() external payable
: is triggered whenever no matching function is found. It serves as a replacement for the old fallback and can be made non-payable if there is no intention to receive native tokens.
Risk of “fallback” & Case study
The fallback function is a powerful feature that is frequently utilized in the Proxy Pattern. However, the presence of a fallback inside a contract can pose significant risks in certain scenarios, potentially leading to direct financial losses for users.
To illustrate, consider a situation where Contract A contains a fallback, and an unrelated Contract B from another party interacts with Contract A. When Contract B invokes a function that is unknown to Contract A, the fallback is triggered, and the transaction does not revert. This creates a scenario where the caller has somehow "bypassed" the logic if Contract B calls A with a non-existent function.
While it may appear that multiple conditions must be satisfied for this to occur, real-world examples of such attacks have resulted in severe effects. Explore the specifics of two case studies below to learn why the fallback might be dangerous and what it means for security.
Multiswap (anyswap) — access control vulnerability
The Multiswap Router contract (pre-V6), there are functions known as "permit" that play a role in utilizing delegated authority to bridge or swap tokens on behalf of others. For instance:
function anySwapOutUnderlyingWithPermit(
address from,
address token,
address to,
uint amount,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s,
uint toChainID
) external {
address _underlying = AnyswapV1ERC20(token).underlying();
IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s);
TransferHelper.safeTransferFrom(_underlying, from, token, amount);
AnyswapV1ERC20(token).depositVault(amount, from);
_anySwapOut(from, token, to, amount, toChainID);
}
The anySwapOutUnderlyingWithPermit
function calls the permit
function of the underlying token to delegate authority for transfer to the contract. Following that, the contract calls transferFrom
to move funds, and the bridging procedure transfers the funds to another blockchain.
While the above code looks to be normal, it takes on an abnormal nature if the contract of the underlying token contains a fallback. A classic example of a token always having a fallback is wrapped native tokens (WETH, WBNB, WAVAX, etc.). Here's a potential attack scenario:
Victim Alice has approved 10 WETH to the Router.
Attacker Bob executes the
anySwapOutUnderlyingWithPermit
function with an amount of 10 WETH and arbitraryv
,r
,s
values.The contract executes the
permit
function with the purpose of delegating authorization. However, at this point, the token is WETH, which contains a fallback. There is no logic to check which authorization is taking place, and the call is not reverted. As a result, the contract proceeds to bridge 10 WETH from Alice to Bob.
In the end, Bob was successful in stealing 10 WETH from Alice since the Router contract lacked robust limitations when invoking a function in another contract.
The attack resulted in huge financial losses, with a total damage of $1.44 million USD. This is a significant sum that serves as an important lesson for many project developers, programmers, and security auditors.
Arbitrary make a token loss of another contract via flashloan
This vulnerability was discovered by auditors in Code4rena. I will reproduce the vulnerable contract in a simplified manner along with a script to demonstrate how it can lead to financial losses in other contracts.
interface IFlashBorrower {
function onFlashLoan(uint256, bytes) external;
}
contract VulnerableContract {
IERC20 tokenX = 0x001234;
function flashloan(IFlashBorrower receiver, uint256 amount, bytes calldata data) public payable {
uint256 fee = amnount * 50 / 1_000;
tokenX.mint(address(receiver), amount);
receiver.onFlashLoan(shareAmount, data);
tokenX.burn(address(receiver), amount + fee);
emit Flashloaned(receiver, eusdAmount, burnShare);
}
}
The caller of the flashloan
function has total control over who the receiver
is. There is no validation logic within the function, and it proceeds to mint
and burn
tokenX
while accounting for the receiver's fee. If the recipient is a contract with a fallback function that does not revert, tokenX
within the contract will be burnt, resulting in a loss equal to the fee. Scenario of attack:
The victim is a contract A deployed via
GnosisSafeProxy
, containing a fallback function that doesn't revert when calling a non-existent function. The user stores 100tokenX
in the contract.The attacker calls the
flashloan
function with the receiver set to contract A.At this stage, the logic runs without hitting any constraints that might cause it to revert. After a successful transaction, contract A gets burnt by 5 tokens, which are fees during the attacker's flash loan. This is a major breach of user protection when engaging in the product.
Conclusion
With the two case studies above, having a fallback function inside a contract is similar to a hidden phantom that can causing impact to the contract's users. The direct source of user losses is not the contract itself, but rather third-party contracts that use it. However, as a developer, you must always assume responsibility for user safety in all scenarios.
For contracts containing fallback functions, it is crucial to always check the logic and revert immediately if there is any invalid behavior in the data.
When using a third-party contract, you can never predict whole its internal logic. Always validate the return values and never allow users to control critical data passed to it.
The Verichains team consistently updates the latest vulnerabilities identified in projects they have audited and those currently under audit, as well as information from the blockchain security community.
About Verichains
Verichains is a leading blockchain security firm specializing in code audits, cryptanalysis, perimeter security, and incident investigation. Founded in 2017 by world-class security researchers, the company leverages extensive expertise in security, cryptography, and core blockchain technology and has helped investigate and fix security issues in the largest crypto hacks, including the BNB Bridge and Ronin Bridge. For any inquiries or questions, please contact us at info@verichains.io