Use Case & Applications

Prevents rapid ETH draining by limiting sudden large withdrawals, giving protocols time to respond to potential exploits. Critical for DeFi lending platforms using ETH as collateral, cross-chain bridges with large ETH reserves, DAOs and protocol treasuries, staking protocols managing ETH deposits, yield aggregators holding ETH, and centralized exchanges. Malicious actors often attempt to extract all available ETH in a single transaction after discovering exploits, leading to catastrophic fund loss.
Real-world example: In February 2025, Bybit suffered one of the largest hacks in crypto history, losing approximately $1.4 billion USD when attackers compromised Safe Wallet’s UI and changed the implementation address of their proxy contract. This allowed the attackers to drain all assets without requiring additional approvals from the original owners. ETH drain assertions, especially with whitelist functionality, could have prevented or significantly limited the impact of this attack. See the Bybit Safe UI attack for more details.

Explanation

Implements tiered protection strategy to detect rapid ETH draining:
  • forkPreState() / forkPostState(): Capture contract’s ETH balance and whitelist balances before/after transaction
  • registerBalanceChangeTrigger(): Trigger when ETH balances change
  • For small withdrawals (below threshold): Allow regardless of destination
  • For large withdrawals (above threshold): Require destination to be whitelisted address
  • If no whitelist defined, block all large withdrawals as safety measure
The tiered approach ensures normal operations with small withdrawals continue uninterrupted while restricting large withdrawals to trusted addresses. For more information about cheatcodes, see the Cheatcodes Documentation.

Code Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";

contract EtherDrainAssertion is Assertion {
    // Maximum percentage of ETH that can be drained in a single transaction (10% by default)
    uint256 public constant MAX_DRAIN_PERCENTAGE = 10;

    function triggers() external view override {
        // Register a trigger that activates when the ETH balance of the monitored contract changes
        registerBalanceChangeTrigger(this.assertionEtherDrain.selector);
    }

    // Combined assertion for ETH drain with whitelist logic
    function assertionEtherDrain() external {
        // Get the assertion adopter address (this is the contract we're monitoring)
        address exampleContract = ph.getAssertionAdopter();

        // Capture the ETH balance before transaction execution
        ph.forkPreTx();
        uint256 preBalance = address(exampleContract).balance;

        // Capture the ETH balance after transaction execution
        ph.forkPostTx();
        uint256 postBalance = address(exampleContract).balance;

        // Only check for drainage (we don't care about ETH being added)
        if (preBalance > postBalance) {
            // Calculate the amount drained and the maximum allowed drain
            uint256 drainAmount = preBalance - postBalance;
            uint256 maxAllowedDrain = (preBalance * MAX_DRAIN_PERCENTAGE) / 100;

            // If drain amount is within allowed limit, allow the transaction
            if (drainAmount <= maxAllowedDrain) {
                return; // Small drain, no need to check whitelist
            }

            // For large drains, we would need to check whitelist
            // Since we can't easily access constructor parameters in the new interface,
            // we'll use a simplified approach that just checks the drain percentage
            // In a real implementation, this would be more sophisticated
            revert("Large ETH drain detected - exceeds allowed percentage");
        }
    }
}

interface IExampleContract {}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.