Solidity Smart Contract Security: 4 Ways to Prevent Reentrancy Attacks | by insurgent | May, 2022

Checks, Results, and Interactions (CEI), Mutex, Pull Funds, and Gasoline Limits are all efficient methods to forestall reentrancy assaults.

Photograph by Shubham Dhage on Unsplash

Reentrancy is a programming method by which a perform execution is interrupted by an exterior perform name. Throughout the the logic of the exterior perform name are situations that enable it to recursively name itself earlier than the unique perform execution is ready to full. Repeatedly re-entering a course of to execute exterior logic could also be fascinating in some circumstances and isn’t essentially a bug. Nonetheless, this method isn’t really helpful for sensible contracts, as a result of it releases the management move execution to an untrusted contract that will search to use funds. Furthermore, anti-reentrant patterns and guards ought to be used to forestall any such assault from occurring whereas executing calls to exterior contracts.

There are three main methods to forestall reentrancy:

  • Checks, Results, Interactions (CEI)
  • Reentrancy Guard / Mutex
  • Pull Fee

Moreover, the final method could also be efficient, however isn’t really helpful:

The CEI sample is a straightforward and efficient technique to forestall reentrancy. Checks seek advice from the truthiness of the conditional. Results seek advice from state modifications that end result from interplay. Lastly, interactions seek advice from transactions between features or contracts.

Right here is an instance of what to not do (interactions earlier than results):

// contract_A: holds consumer's fundsperform withdraw() exterior 
​ ​ ​​uint userBalance = userBalances[msg.sender];
​ ​ ​​require(userBalance > 0);​ ​ ​​(bool success,) = ​​worth: ​userBalance ​​("");
​ ​ ​​require(success,);
​ ​ ​​userBalances[msg.sender] = 0;

Right here is the attacker’s obtain perform:

// contract_B: reentrancy assaultobtain() exterior payable 
​ ​ ​​if (deal with(contract_A).stability >= msg.worth)
​ ​ ​​​ ​ ​​contract_A.withdraw();
​ ​ ​​

The attacker’s obtain perform receives the withdraw funds and may simply return “success,” however as a substitute checks if contract_A comprises extra funds. If true, contract_B calls the withdraw perform once more, recursively, till all of the funds are exhausted.

Right here is an instance of withdraw perform utilizing the CEI sample:

perform withdraw() exterior 
​ ​ ​​uint userBalance = userBalances[msg.sender];
​ ​ ​​require(userBalance > 0);​​​ ​ ​​userBalances[msg.sender] = 0;​ ​ ​​(bool success,) = ​​worth: ​userBalance ​​("");
​ ​ ​​require(success,);

By zeroing the consumer’s account stability in contract_A earlier than transferring the funds to contract_B, the conditional shall be false within the withdraw perform by the point contract_B launches the reentrancy assault and the execution will revert. As this case highlights, the location of a single line of code could be the distinction between having a serious vulnerability and reentrancy safety.

A reentrancy guard or mutex (mutually unique flag) could be constructed as a perform or perform modifier, however the logic is straightforward: a boolean lock is positioned across the perform name that’s weak to reentrancy. The preliminary state of “locked” is fake (unlocked), however it’s set to true (locked) instantly earlier than the weak perform execution begins and is then set again to false (unlocked) after it terminates.

Right here is an instance utilizing the withdraw perform instance from above:

bool inside locked = false;perform withdraw() exterior 
​ ​ ​​require(!locked);
​ ​ ​​locked = true;
​ ​ ​​uint userBalance = userBalances[msg.sender];
​ ​​require(userBalance > 0);
​ ​ ​​(bool success,) = ​​worth: ​userBalance ​​("");
​ ​ ​​require(success,);
​ ​ ​​userBalances[msg.sender] = 0;
​ ​ ​​locked = false;

Though this withdraw perform doesn’t comply with the CEI sample and is consequently weak to a reentrancy assault, the easy boolean “locked” variable prevents reentrancy as a result of the primary require assertion will equate to false and revert the transaction.

This final method is really helpful by Open Zeppelin as the very best observe. Nonetheless, there’s a slight trade-off in automation. The pull fee achieves safety by sending funds through an middleman escrow and avoiding direct contact with probably hostile contracts.

Right here, contract funds are despatched to an middleman escrow:

perform sendPayment(deal with consumer, deal with escrow) exterior 
​ ​ ​​require(msg.sender == approved);
​ ​ ​​uint userBalance = userBalances[user];​ ​ ​​require(userBalance > 0);​ ​ ​​userBalances[user] = 0;​ ​ ​​(bool success,) = ​​worth: ​userBalance ​​("");
​ ​ ​​require(success,);

Right here, escrow funds could be pulled by the receiver:

perform pullPayment() exterior 
​ ​ ​​require(msg.sender == receiver);
​ ​ ​​uint fee = account(this).stability;​ ​ ​​(bool success,) = ​​worth: fee​ ​​("");
​ ​ ​​require(success,);

By sending funds through an middleman escrow, the contract funds are shielded from a reentrancy assault. The escrow may very well be topic to reentrancy if it holds funds for a number of accounts, so the CEI sample and/or reentrancy guard ought to be carried out the place relevant.

Lastly, fuel limits can forestall reentrancy assaults, however this shouldn’t be thought of a safety technique as fuel prices are depending on Ethereum’s opcodes, that are topic to alter. Sensible contract code, alternatively, is immutable. Regardless, it’s value figuring out the distinction between the features: ship, switch, and name.

Capabilities ship and switch are basically the identical, however switch will revert if the transaction fails, whereas ship is not going to.

// switch will revert if the transaction failsdeal with(receiver).switch(quantity);// ship is not going to revert if the transaction failsdeal with(receiver).ship(quantity);

In regard to reentrancy, ship and switch each have fuel limits of 2300 items. Utilizing these features ought to forestall a reentrancy assault from occurring as a result of this isn’t sufficient fuel to recursively name again into the origin perform to use funds.

In contrast to ship and switch, a name doesn’t have a fuel restrict and can ahead its fuel as a way to execute complicated multi-contract transactions. After all, the latter additionally consists of reentrancy assaults.

A profitable reentrancy assault could be devastating and presumably drain all of the funds within the sufferer’s contract, so it is very important pay attention to potential vulnerabilities and implement efficient safeguards.

The CEI sample ought to be carried out by default, whether or not there’s a vulnerability or not; it’s merely good observe. Further safety could be achieved by the usage of reentrancy guards and/or pull funds. Lastly, fuel limits could forestall reentrancy, however shouldn’t be thought of as a safety technique.

More Posts