#41699 [SC-Insight] Silent Transfer Failures in Native Token Handling
Submitted on Mar 17th 2025 at 16:38:27 UTC by @ZeroXGondar for Audit Comp | Yeet
Report ID: #41699
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/contracts/Zapper.sol
Impacts:
Description
Summary
In Zapper::_sendNativeToken
when transferring native tokens, the contract uses unsafe transfer
method instead of the recommended low level call
method.
This is problematic since transfer
method only passes 2300 gas, which can cause the transaction to be reverted if the interacting contract uses more gas in its receive/fallback function.
Details
The _sendNativeToken()
function uses the deprecated transfer()
method to send native BERA tokens. This approach has multiple security flaws:
No verification of transfer success
Silent failures possible when transfers fail
Limited to 2300 gas, insufficient for contracts with complex receive/fallback functions
Note that this function can be accessed in many flows, making the issue more serious. It can be accessed by directly calling Zapper::zapOutNative
function, or, in more complex scenarios, it can be accessed via StakeV2::claimRewardsInNative
function which in turn calls Zapper::zapOutNative
that calls _sendNativeToken
Impact
Users may believe their funds were successfully transferred when they weren't
Protocol accounting may become inconsistent with actual token balances
Lack of error handling prevents recovery mechanisms from being triggered
Potential permanent loss of funds due to silent failures
Faulty block
function _sendNativeToken(address receiver, uint256 amount) internal {
if (amount > 0) {
wbera.withdraw(amount);
payable(receiver).transfer(amount);
}
}
Recommended Fix
Replace transfer()
with call()
and add success verification:
function _sendNativeToken(address receiver, uint256 amount) internal {
if (amount > 0) {
wbera.withdraw(amount);
(bool success, ) = payable(receiver).call{value: amount}("");
require(success, "Native token transfer failed");
}
}
Proof of Concept
POC
A contract with a gas-intensive receive function (consuming >2300 gas) is deployed on the network.
The vulnerable flow is triggered in one of two ways:
Direct call to
Zapper::zapOutNative
with the gas-intensive contract as the receiverIndirect call through
StakeV2::claimRewardsInNative
which ultimately calls_sendNativeToken
When
_sendNativeToken
executes:It successfully withdraws tokens from wbera with
wbera.withdraw(amount)
It attempts to transfer native tokens using
payable(receiver).transfer(amount)
The transfer silently fails because the receiver's receive function exceeds the 2300 gas limit
No error is thrown or checked since the contract doesn't verify the transfer's success
The transaction completes without reverting, making it appear successful to users and the protocol.
Was this helpful?