Ether Transfer is fraught with peril | Beosin Vulnerability Analysis Series VIII
We’ve talked about various vulnerabilities in token contracts. These contracts usually just operate tokens of themselves, not necessarily including the ‘gold’ of Ethereum — Ether. However, with more and more game contracts showing up in the blockchain industry, Ether transferring functions are used directly in smart contracts. Are these smart contracts more secure? Not really. For instance, the Pandemica Ponzi game incident was caused by DoS with exceeding gas limit, resulting in assets being frozen. This type of vulnerability has already been discussed in Phase II. In this Phase, we are going to discuss other vulnerabilities and honeypots that directly relate to ether transfer.
Basics
In Solidity, the official advice for transferring Ether to target address.
<address>.transfer(uint256 amount)
means sending a Ether amount of wei to the target address, and it will throw an exception when failing. It requires 2300 gas and this number is not adjustable.
<address>.send(uint256 amount) returns (bool)
means sending a Ether amount of wei to the target address. It returns false when failing and also requires 2300 gas, not adjustable as well.
<address>.call.value(uint256 amount)() returns (bool)
means sending ether whose amount is wei to the target address, and it returns false when failing. This method, however, sends all available gas and can be adjusted via using (.gas(uint256 gasAmount)
).
Those are the three methods for sending Ether. Now let’s learn something about the invariant-checking. Invariant-checking is a common defensive programming technique that is useful in enforcing correct state transitions or validating operations. This technique involves defining a set of invariants (metrics or parameters that should not change) and checking these invariants remain unchanged after single (or many) operations(s). This is typically good design, provided the invariants being checked are in fact invariant. In ERC20 token contract, we also used this technique. For instance, the totalSupply
is fixed. As no functions should modify this invariant, one could add a check to the transfer()
function that ensures the totalSupply
remains unmodified to ensure the function is working as expected.
Here Comes the Problems
Regarding the three methods of sending Ether, problems arise on the fallback function. If a contract, not a user, receives Ether (no functions are invoked), fallback function will be executed. The function of fallback is receiving Ether, and contract will reject (meanwhile throwing an exception) if such function is missing. If contract uses transfer/send method to send Ether to the target contract address, contract can only rely on the available gas (or 2300 gas) to execute when the target contract is executing fallback function. This gas amount is not enough to complete any method of storage references. This causes the situation where fallback function keeps returning false like a coil spring which can’t go back.
Often when talking about invariant-checking, developers tend to trust the Ether balance, but in fact, it can be manipulated by external users (regardless of the rules put in place in the contract). Moreover, developers first learn Solidity they have misconception that a contract can only accept or obtain ether via payable
functions. There are situations where a contract can accept or obtain ether without using payable
functions or executing any code on the contract. These contracts are vulnerable to those situations where ether are (forcibly) sent to them.
Case Analysis
1. The security risks of sending and accepting ether
a. Security risks when using transfer
We use a level in Ethernaut-King as an example contract
The rule is that whoever send a ether amount larger than the current reward will become the new king, and the previous king receives the rewards.
If the attacker deploys a contract like this:
Thing will go south because fallback function can not accept ether. Specifically speaking, After the attacker becomes the new king, the function will revert every time king.transfer(msg.value)
is executed, which is also emitted when new competitors send ether to the example contract. In fact, this is a type of DoS via Revert which we discussed in Phase II.
b. Security Risks when using send
We use the same example from KingOfTheEtherThrone as we discussed in Phase II.
Since it will return false instead of throwing an exception when failing to execute ‘send’ and there is no checking on return value of send, part of the players will fail to accept the return of ether as the 2300 gas carried by send is not enough to complete fallback operation.
c. Security Risks of using call.value()()
Using call.value()() to send ether will carry the rest of the gas by default. If the implementation of contract contains risks, it will cause the reentrancy attack. Moreover, call.value()() returns false when sending ether fails. If no check on return value, contract will regard all ether sending as success, then start executing the changing of state variables, which obviously contains logic flaws.
Bug-fix
(1) When sending ether to a target address, the difference between normal account and contract account should be considered. If the receiver address is a contract address, gas amount should be enough to ensure the execution of corresponding functions.
(2) The circumstances of failing to send ether should be included, such as ‘revert’ when using ‘transfer’ will possibly cause DoS. Actions need to be taken when using send and call.value()() to send ether as they both return false.
2. Unexpected forcibly receiving Ether
a. Self-destruct
Any contract is able to implement the self-destruct function, which removes all bytecode from the contract address and sends all ether stored there to the parameter-specified address. If this specified address is also a contract, no functions (including the fallback) will be invoked. Therefore the selfdestruct()
function can be used to forcibly send ether to any contract regardless of any conde that may exist in the contract. This is inclusive of contracts without any payable functions. This means any attacker can create a contract with a selfdestruct()
function, send ether to it call selfdestruct(target)
and force ether to be sent to a target contract. Martine Swende has an excellent blog post describing some quirks of the self-destruct opcode (Quirk #2) along with a description of how client nodes were checking incorrect invariants which could have led to rather catastrophic nuking of clients.
b. Pre-sent Ether
The second way a contract can obtain ether without using a selfdestruct()
function or calling any payable functions is to pre-load the contract address with ether. Contract addresses are deterministic, in fact the address is calculated from the keccake256 (sometimes synonymous with SHA3) has of the address creating the contract and the transaction nonce which creates the contract. Specifically, it is of the form:
address = sha3(rlp.encode([account_address,transaction_nonce])
(see Keyless Ether for some fun use cases of this How is the address of an Ethereum contract computed? ). This means, anyone can calculate what a contract address will be before it is created and thus send ether to that address. When the contract does get created it will have a non-zero ether balance.
c. Mining
Currently, contract and external account both cannot prevent someone from sending them ether. A contract can respond to a normal transaction and choose to reject it. However, there are some ways to send ether without creating messages. Mining to contract address is one way to achieve it.
Let’s analyze the following contract:
This contract works for a simple game (beware there is a potential risk of Race-conditions), players can send 0.5 to the contract to become the first play to reach 1/3 of the milestone. Milestone calculates according to the Ether amount. The first one who reaches the milestone can obtain part of the Ethers within contract. The game ends when the last milestone is reached (10 Ether), players can withdraw rewards.
The problem arises in uint currentBalance = this.balance + msg.value
(and related Line 16) and Line 32, which contain the misuse of this.balance
. An attacker can put Ether in a contract using those method mentioned above.
For instance,
a. self-destruct
If attaching 0.1 ether to transaction when deploying this contract, and invoke the attack function to self-destruct, then the attached 0.1 ether will be sent to the case contract. Because the case contract can only accept 0.5 ether, normal players can never reach the milestone, thus no winner. However, someone can forcibly send 0.4 ether to the case contract to win.
b. Pre-sent Ether
The method to use solidity to calculate the deployment address is address(keccak256(0xd6, 0x94, _from, nonce))
. In that calculation, _from
represents the address of the account which deploys the contract, nonce
represents the nonce of deployment, which makes the latest transaction sequence plus 1. If the deployment account has never traded before, nonce=1 when the account is a contract, whereas nonce=0 when it’s a normal user.
Bug-fix
This vulnerability is the abuse of this.balance. The logic of the contract should avoid depending on the exact number of balance if possible, because it could be manipulated regardless of the logic. If the logic has to be based on this.balance, the unexpected change of balance should be considered.
Under the circumstances that accurate value of balance is needed, a state variable should be defined to record the increase of ether which are accepted via payable. Meanwhile, this variable will not be affected when ethers are forcibly sent to the contract. Therefore, we made the following changes to the case contract.
What we suggest
The variety of smart contracts continues to grow as the technology goes further. It appears to be opportunity as well as challenge. The vulnerability we talked about this time constantly appears in the Ethereum games, causing financial losses as well as disappointment of blockchain industry. Therefore, being excelsior in smart contract development is the top task for us.
About Beosin
Beosin is headquartered in Chengdu and focuses on blockchain security field.Founded by Prof. Xia Yang and Prof. Wensheng Guo of UESTC, Beosin’s core team consists of 40 associate professors, doctors, and postdoctoral fellows with experience studying overseas and leading universities as well as laboratories industry elites from Alibaba, Huawei, and other known enterprises. Using formal verification as its core technology, Beosin is the first company in China that applies formal verification technology to blockchain security field.
Beosin has received strategic equity investments from known venture capitals including Fenbushi Capital, Milestone Capital, and Vangoo Capital while building strategic partnerships with over 40 renowned blockchain companies such as Huobi, OKEx, Kucoin, LBank, Seele, ONT, Qtum, Bytom, Wanchain, Scry, Bubi Blockchain, YUNPHANT, QuarkChain, IoTeX, Math Wallet, Seele, etc.
Let’s connect
E-mail:vaas@lianantech.com
Official website:https://www.beosin.com
Twitter: https://twitter.com/Beosin_com
Facebook: https://www.facebook.com/BeosinChengdu/
Telegram Chinese Group:https://t.me/joinchat/IRgNDA4iCF0Rs92sg5qoVg
Telegram English group: https://t.me/joinchat/IRgNDBBpCon-695ATmbA4w