Brute Force Storage Layout Discovery in ERC20 Contracts With Hardhat

Dariusz Glowinski
Euler Labs
Published in
5 min readOct 12, 2021

A simple hack to automatically find the account balance slot in ERC20 contracts using Hardhat’s mainnet fork feature.

Euler is building a next gen lending protocol, similar to Aave or Compound. As with all smart contract development, that requires testing, and lots of it.

If your code is meant to interact with other contracts on Ethereum, which is the case for lending protocols, at some point you might want to run a few integration tests on Hardhat’s mainnet fork. In essence, you get all of the “real” ethereum, in memory, for your contracts to interact with. Pretty awesome!

As a lending protocol, Euler fundamentally interacts with ERC20 tokens. If someone wants to test lending real BAT against real DAI, the first thing needed are wallets with some token balances. Hardhat allows developers to impersonate any real Ethereum account, but because of reasons, we wanted to use the built in wallets provided by ethers. Yet another cool feature of Hardhat is the ability to manually set the value of any storage slot with hardhat_setStorageAt. We decided to use that, and manually set token balances for our accounts.

How exactly do we do that though? How do we find which slot to set?

Let’s first make an assumption, that ERC20 contracts will most likely declare a mapping from an account address to balance:

mapping (address => uint) balances;

Knowing how mappings work, we can calculate the slot number where the balance of an account is held:

const valueSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['address', 'uint'],
[account, balanceSlot]
),
);

Where balanceSlotis the slot where the mapping is declared. Cool, but how do we find which slot it is, for any given token contract? For DAI, for example, we could go through the contract code on etherscan and just count the variables declared, until we find the balances mapping. However, this seems like manual and tedious work, especially if we want to use a large number of real tokens in our tests. There are tools to analyze storage layout, but they would still require some manual work.

What if we could automate finding thebalanceSlotvalue somehow, given just the token address?

Let’s flip the question, and instead of asking what the balanceSlotvalue is, let’s ask: If we knew the balanceSlotvalue, how would we verify that it is in fact the balances mapping?

If we manually set some balance for the account.

const probe = '0x' + '1'.padStart(64);network.provider.send('hardhat_setStorageAt', [valueSlot, probe]);

then calling the token’s balanceOf should return that same value:

const balance = await token.balanceOf(account);if (!balance.eq(ethers.BigNumber.from(probe)))
throw 'Nope, it’s not the balances slot';

So now we can just iterate over the slot numbers to find balanceSlot. With handling of some edge cases and cleaning up the storage, the final code:

async function findBalancesSlot(tokenAddress) {
const encode = (types, values) =>
ethers.utils.defaultAbiCoder.encode(types, values);
const account = ethers.constants.AddressZero;
const probeA = encode(['uint'], [1]);
const probeB = encode(['uint'], [2]);
const token = await ethers.getContractAt(
'ERC20',
tokenAddress
);
for (let i = 0; i < 100; i++) {
let probedSlot = ethers.utils.keccak256(
encode(['address', 'uint'], [account, i])
);
// remove padding for JSON RPC
while (probedSlot.startsWith('0x0'))
probedSlot = '0x' + probedSlot.slice(3);
const prev = await network.provider.send(
'eth_getStorageAt',
[tokenAddress, probedSlot, 'latest']
);
// make sure the probe will change the slot value
const probe = prev === probeA ? probeB : probeA;

await network.provider.send("hardhat_setStorageAt", [
tokenAddress,
probedSlot,
probe
]);

const balance = await token.balanceOf(account);
// reset to previous value
await network.provider.send("hardhat_setStorageAt", [
tokenAddress,
probedSlot,
prev
]);
if (balance.eq(ethers.BigNumber.from(probe)))
return i;
}
throw 'Balances slot not found!';
}

This simple technique has some obvious limitations. It won’t work if the account balances are not stored in a top level mapping, for example in a struct somewhere, or even in a different contract altogether. It can be extended however for other ERC20 data like allowances or to other standards.

So that’s it, happy coding!

About Euler

Euler is a capital-efficient permissionless lending protocol that helps users to earn interest on their crypto assets or hedge against volatile markets without the need for a trusted third-party. Euler features a number of innovations not seen before in DeFi, including permissionless lending markets, reactive interest rates, protected collateral, MEV-resistant liquidations, multi-collateral stability pools, sub-accounts, risk-adjusted loans and much more. For more information, visit euler.finance.

Join the Community

Follow us on Twitter. Join our Discord. Keep in touch on Telegram (community, announcements). Check out our website.

Dislcaimer:

This piece is provided by Euler Labs Ltd. for informational purposes only and should not be interpreted as investment, tax, legal, insurance, or business advice. Euler Labs Ltd. and The Euler Foundation are independent entities.

Neither Euler Labs Ltd., The Euler Foundation, nor any of their owners, members, directors, officers, employees, agents, independent contractors, or affiliates are registered as an investment advisor, broker-dealer, futures commission merchant, or commodity trading advisor or are members of any self-regulatory organization.

The information provided herein is not intended to be, and should not be construed in any manner whatsoever, as personalized advice or advice tailored to the needs of any specific person. Nothing on the Website should be construed as an offer to sell, a solicitation of an offer to buy, or a recommendation for any asset or transaction.

This post reflects the current opinions of the authors and is not made on behalf of Euler Labs, The Euler Foundation, or their affiliates and does not necessarily reflect the opinions of Euler Labs, The Euler Foundation, their affiliates, or individuals associated with Euler Labs or The Euler Foundation.

Euler Labs Ltd. and The Euler Foundation do not represent or speak for or on behalf of the users of Euler Finance. The commentary and opinions provided by Euler Labs Ltd. or The Euler Foundation are for general informational purposes only, are provided “AS IS,” and without any warranty of any kind. To the best of our knowledge and belief, all information contained herein is accurate and reliable and has been obtained from public sources believed to be accurate and reliable at the time of publication.

The information provided is presented only as of the date published or indicated and may be superseded by subsequent events or for other reasons. As events and markets change continuously, previously published information and data may not be current and should not be relied upon.

The opinions reflected herein are subject to change without being updated.

--

--

Dariusz Glowinski
Euler Labs

Blockchain dev @ Euler. Developing with EVM and Solidity since 2017.