921 ETH Stuck in zkSync Era: Why Transfer() Function Fails?

Introduce the differences between transfer(), send(), and call() functions.

Digital Tails Group|DTG
Coinmonks
Published in
3 min readMay 8, 2023

--

Introduction

Last month, researcher — , pointed out on Twitter that Gemholic, a new project on zkSync Era, raised approximately 921 ETH through the sale of GEMS tokens, but the funds raised were stuck in the smart contract, GemstoneIDO, due to a problem with the transfer() function.

The main cause of this event was due to the limitations of the transfer() function in Solidity. While transfer() is limited to 2300 gas, which is sufficient for transfers, the fallback() or receive() function of the other contract cannot implement overly complex logic. Additionally, if the transfer() fails, the transaction will automatically revert.

While this method is feasible on EVM-compatible chains, the problem lies in the fact that zkSync Era is not fully compatible with the Ethereum Virtual Machine (EVM). Gas calculations on zkSync Era use a dynamic and divergent gas measurement method. Using transfer() on zkSync Era will exceed the 2300 gas limit, causing the transaction to revert automatically.

Ether Transfer Methods

In Solidity, there are other methods for transferring Ether (or other native tokens) besides transfer(). The following are the different methods:

Transfer() — this method was already introduced above, here is an example of the code:

payable(_address).transfer(1 ether);

Send() — Similarly, send() also has a 2300 gas limit, making it difficult to implement too complex fallback() or receive() functions. However, unlike transfer(), send() returns a bool value indicating whether the transfer was successful or not. This means that even if the transfer fails, the transaction will not automatically revert.

bool success= payable(_address).send(1 ether);

Call() — On the other hand, call() has no gas limit, making it capable of supporting complex logic for the fallback() or receive() function. The return value of call() is a tuple (bool, data), where bool represents whether the transfer was successful or not. This means that even if the transfer fails, the transaction will not automatically revert.

(bool success,)= payable(_address).call{value: 1 ether}("");

Currently, most developers prefer to use call() because it offers more flexibility during development. transfer() is used only when there is a specific need for gas limitation and automatic transaction reversion upon failure. Also, many developers have also urged others to avoid using transfer() as much as possible in development.

On the other hand, send() has both gas limitations and does not automatically revert upon failure. It is less convenient and secure compared to call(), which is why it is rarely used.

Conclusion

The error in this case resulted from the dev team’s insufficient understanding of the differences in development environments between chains and the lack of comprehensive testing on the smart contract. If the team had conducted testing on the testnet and sought smart contract audit services in advance, this problem could have been discovered and fixed.

Smart contracts cannot be modified after deployment, so it is crucial to conduct multiple tests and security audits before deployment to avoid significant financial losses.

If you are looking for a professional auditor, feel free to contact our technical experts.

🌐 AVS Consulting
https://avsconsulting.pro/

--

--

Digital Tails Group|DTG
Coinmonks

digital-tails.group We offering a variety of comprehensive white-label solutions to businesses involved in web3, blockchain, 3D, and immersive AR/VR technology.