Euler labs
Euler labs

Brute Force Storage Layout Discovery in ERC20 Contracts With Hardhat

Brute Force Storage Layout Discovery in ERC20 Contracts With Hardhat

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.

This content 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, is an independent software development company.

Neither Euler Labs, Ltd. nor any of its 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.

Euler Labs Ltd, does not represent or speak for on or behalf of Euler Finance or the users of Euler Finance. The commentary and opinions provided by Euler Labs Ltd., 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 we believe to be accurate and reliable at the time of publication.

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

Keep up to date

Euler Newsletter

You can unsubscribe at any time. Privacy Policy.

Euler Blog

Related blogs