DEV Community

Cover image for Infinite summarizes the known attacks that should be paid attention to and defended when writing smart contracts
infinite_cycle
infinite_cycle

Posted on

Infinite summarizes the known attacks that should be paid attention to and defended when writing smart contracts

Alt Text

One of the main dangers of calling external contracts is that they can take over the flow of control and make changes to data that the calling function does not expect. Such errors can take many forms, and the two main errors that cause DAO to crash are both such errors.

Reentrancy of a single function

The first version of this error to be aware of involves functions that can be called repeatedly before the first call of the function is complete. This may cause the different calls of the function to interact in a destructive way.

// Insecure mapping (address => uint) private userBalances; function withdrawBalance () public {
uint amountToWithdraw = userBalances [msg.sender ];
(bool succeeded,) = msg.sender. Call. Value (amountToWithdraw )( "" ); // At this point the caller's code is executed, you can call withdrawBalance again
require (success );
User balance [msg.sender] = 0;}
//

Since the user's balance is not set to 0 until the end of the function, the second (and subsequent) call will still succeed, and the balance will be withdrawn over and over again.

DAO is a decentralized autonomous organization. The goal is to compile the rules and decision-making mechanisms of the organization, eliminate the need for documents and personnel in management, and create a structure with decentralized control.

On June 17, 2016, the DAO was hacked and 3.6 million ether ($50 million) was stolen using the first reentry attack.

The Ethereum Foundation released an important update to roll back the hacking attack. This resulted in Ethereum being forked into Ethereum Classic and Ethereum.

In the example given, the best way to prevent this attack is to ensure that no external functions are called until all the internal work that needs to be performed is completed:
Mapping (address => uint) private user balance; function withdrawBalance () public {
uint amountToWithdraw = userBalances [msg.sender ];
User balance [msg.sender] = 0;
(bool succeeded,) = msg.sender. Call. Value (amountToWithdraw )( "" ); // The user's balance is already 0, so future calls will not extract anything
require (success );}
Please note that if you have another function called withdrawBalance(), it may be subject to the same attack, so you must treat any function calling an untrusted contract as untrusted itself. For further discussion of potential solutions, see below.

Cross-functional reentrancy
Attackers can also use two different functions that share the same state to carry out similar attacks.
// Insecure mapping (address => uint) private userBalances; function transfer (address to, UINT amount) {
If (userBalances [msg.sender] >= amount) {
userBalances [to] + = amount;
User balance [msg.sender] -= amount;
}} Function withdrawBalance () public {
uint amountToWithdraw = userBalances [msg.sender ];
(bool succeeded,) = msg.sender. Call. Value (amountToWithdraw )( "" ); // At this point, the caller's code is executed, and transfer() can be called
require (success );
User balance [msg.sender] = 0;}

In this case, the attacker calls withdrawBalance while executing their code in an external call to transfer(). Since their balance has not been set to 0, they can transfer tokens even if they have received the withdrawal. This vulnerability has also been used in DAO attacks.

The same solution will work, but with the same warning. Also note that in this example, both functions are part of the same contract. However, if multiple contracts share state, the same error may occur in multiple contracts.

Pitfalls in reentrant solutions
Since reentrancy may occur in multiple functions or even multiple contracts, any solution designed to prevent reentrance of a single function is not enough.

Instead, we recommend completing all internal work (that is, state changes) before calling external functions. If you follow this rule carefully, it will allow you to avoid loopholes due to reentrancy. However, not only do you need to avoid calling external functions prematurely, you also need to avoid calling functions that call external functions. For example, the following is not safe:

// Insecure mapping (address => uint) private userBalances; mapping (address => bool) private claimed bonus; mapping (address => uint) private reward ForA; function withdrawReward (address recipient) public {
uint amountToWithdraw = RewardForA [Recipient];
Reward ForA [Recipient] = 0;
(bool succeeded,) = recipient. Call. Value( amountToWithdraw )( "" );
Request (success);} function getFirstWithdrawalBonus (address recipient) public {
Requirements (! claimed bonus [recipient]); // each recipient can only receive the bonus once
RewardForA [Recipient] += 100;
Withdraw the reward (recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
claimBonus [receiver] = true;}

Even if getFirstWithdrawalBonus() does not directly call an external contract, calling withdrawReward() is enough to make it vulnerable to reentrancy. Therefore, you need to treat its withdrawReward() as untrusted.

Mapping (address => uint) private user balance; mapping (address => bool) private claimed bonus; mapping (address => uint) private reward ForA; function untrustedWithdrawReward (address recipient) public {
uint amountToWithdraw = RewardForA [receiver];
Reward ForA [Recipient] = 0;
(bool succeeded,) = recipient. Call. Value( amountToWithdraw )( "" );
Request (success);} function untrustedGetFirstWithdrawalBonus (address recipient) public {
Requirement (! ClaimBonus [recipient]); // Each recipient can only claim the bonus once
claimBonus [receiver] = true;
RewardForA [Recipient] += 100;
untrustedWithdrawReward (receiver); // claimBonus has been set to true, so it cannot be re-entered}

With the exception of fixes that cannot be re-entered, untrusted features have been marked as. The same pattern is repeated at all levels: because untrustedGetFirstWithdrawalBonus() calls untrustedWithdrawReward() and requires an external contract, you must also treat untrustedGetFirstWithdrawalBonus() as unsafe.

Another solution that is usually recommended is mutual exclusion locks. This allows you to "lock" certain states, so it can only be changed by the owner of the lock. A simple example might look like this:

// Note: This is a basic example. Mutex locks are particularly useful when there is a large amount of logic and/or shared state mapping (address => uint) private balance; bool private lock balance; function deposit() for public returns ( bool) {
Requirements (! lockBalances);
Lock balance = true;
Balance [msg.sender] += msg.value;
Lock balance = false;
Return true;} function withdraw (uint amount) payable public return (bool) {
Requirements (! lockBalances && amount> 0 && balance [msg.sender] >= amount);
Lock balance = true;
(bool succeeded,) = msg.sender. Call (amount) ("");
If (success) {//Usually unsafe, but the mutex saves it
Balance [msg.sender]-= amount;
}
Lock balance = false;
Return true;}

If the user withdraw() tries to call again before the first call is completed, the lock will prevent it from having any effect. This may be an effective model, but it can become tricky when you have multiple contracts that require cooperation. The following are unsafe:

//Insecure contract StateHolder {
uint private n;
Address private lock;
Function getLock () {
Need( lockHolder == address( 0 ));
lockHolder = msg.sender;
}
Function releaseLock () {
Request (msg.sender == lockHolder );
lockHolder = address (0 );
}
Function set (uint newState) {
Requirements (msg.sender == lockHolder);
Ñ = newState;
}}

The attacker can call getLock() and then never call releaseLock(). If they do, then the contract will be permanently locked and no further changes can be made. If you use mutex locks to prevent reentry, you will need to carefully ensure that there is no way to declare the lock and never release it. (There are other potential dangers when programming with mutexes, such as deadlocks and livelocks. If you decide to go this way, you should consult the extensive literature on mutexes that has been written.)

Discussion (0)