用Hardhat在ERC20合约中强行发现存储结构 (Brute Force Storage Layout Discovery in ERC20 Contracts With Hardhat)

原文:
https://blog.euler.finance/brute-force-storage-layout-discovery-in-erc20-contracts-with-hardhat-7ff9342143ed

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

我们正在分享一个简单的技巧,使用 Hardhat 的主网分叉功能自动查找 ERC20 合约中的账户余额槽。


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

Euler,我们正在构建类似于 Aave 或 Compound 的下一代借贷协议。与所有智能合约开发一样,这需要测试,而且很多。

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!

如果您的代码旨在与以太坊上的其他合约交互,借贷协议就是这种情况,在某些时候您可能希望在 Hardhat 的主网分支。从本质上讲,你在内存中获得了所有“真实”的以太坊,供你的合约进行交互。太棒了!

As a lending protocol, Euler fundamentally interacts with ERC20 tokens. If we want to test lending real BAT against real DAI, the first thing we need are wallets with some token balances. Hardhat allows us 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.

作为一种借贷协议,Euler从根本上与 ERC20 代币进行交互。如果我们想针对 real DAI 测试借贷 real BAT,我们首先需要的是带有一些代币余额的钱包。 Hardhat 允许我们模拟 任何真实的以太坊帐户,但由于某些原因,我们希望使用 ethers 提供的内置钱包。 Hardhat 的另一个很酷的功能是能够手动设置任何存储槽的值 使用hardhat_setStorageAt。我们决定使用它,并为我们的账户手动设置代币余额。

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:

我们首先做一个假设,ERC20 合约很可能会声明从账户地址到余额的映射:

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.

其中 balanceSlot 是声明映射的槽。很酷,但是对于任何给定的代币合约,我们如何找到它是哪个插槽?例如,对于 DAI,我们可以通过 etherscan 上的合约代码 计算声明的变量,直到找到余额映射。然而,这似乎是手动和乏味的工作,特别是如果我们想在我们的测试中使用大量的真实代币。有一些工具可以分析存储布局,但它们仍然需要一些手动工作。

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

如果我们能以某种方式自动找到_balanceSlot_值,并且只需要代币地址呢?

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?

让我们翻转这个问题,而不是问什么是 balanceSlot 值,让我们问:如果我们知道 balanceSlot 值,我们如何验证它实际上是 balances 映射?

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:
然后调用代币的 balanceOf 应该返回相同的值:

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:

所以现在我们可以遍历槽号来找到 balanceSlot。通过处理一些边缘情况并清理存储,最终代码如下:

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.

这种简单的技术有一些明显的局限性。如果账户余额没有存储在顶层映射中,例如在某个结构中,甚至完全不同的合约中,它就不会起作用。但是,它可以扩展到其他 ERC20 数据,例如“津贴”或其他标准。

So that’s it, happy coding!
这就试了,编程愉快!

关于Euler (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.

Euler 是一种资本效率高的无许可借贷协议,可帮助用户从其加密资产中赚取利息或对冲波动的市场,而无需受信第三方。 Euler 具有许多在 DeFi 中前所未有的创新,包括无许可的借贷市场、回应性利率、受保护的抵押品、抗 MEV 清算、多抵押品稳定池、子账户、风险调整贷款等等。有关更多信息,请访问 euler.finance

加入社区 (Join the Community)

Follow us Twitter. Join our Discord. Keep in touch on Telegram (communityannouncements). Check out our website. Connect with us on LinkedIn.

关注我们 Twitter。加入我们的 Discord。在 Telegram 上保持联系(communityannouncements)。查看我们的网站。在 LinkedIn 上与我们联系。

赞赏