Este artigo foi escrito por Dariusz Glowinski e traduzido por Diogo Jorge. Você pode encontrar o artigo original aqui.
Descoberta de layout de armazenamento de força bruta em contratos ERC20 com Hardhat
Um hack simples para encontrar automaticamente o slot do saldo da conta nos contratos ERC20 usando o recurso mainnet fork do Hardhat.
Euler está construindo um protocolo de empréstimo de última geração, semelhante ao Aave ou Compound. Tal como acontece com todo o desenvolvimento de contratos inteligentes, isso requer testes, e muitos.
Se o seu código deve interagir com outros contratos no Ethereum, que é o caso dos protocolos de empréstimo, em algum momento você pode querer executar alguns testes de integração no fork da mainetl do Hardhat. Em essência, você obtém todo o ethereum “real”, na memória, para interagir com seus contratos. Muito incrível!
Como um protocolo de empréstimo, o Euler interage fundamentalmente com os tokens ERC20. Se alguém quiser testar o empréstimo de BAT real contra DAI real, a primeira coisa necessária são carteiras com alguns saldos de token. O Hardhat permite que os desenvolvedores se passem por qualquer conta Ethereum real, mas por alguns motivos, queríamos usar as carteiras integradas fornecidas por ethers. Ainda outro recurso interessante do Hardhat é a capacidade de definir manualmente o valor de qualquer slot de armazenamento com hardhat_setStorageAt
. Decidimos usar isso e definir manualmente os saldos de token para nossas contas.
Mas como exatamente fazemos isso? Como descobrimos qual slot definir?
Vamos primeiro supor que os contratos ERC20 provavelmente vão declarar um mapeamento de um endereço de conta para o saldo:
mapping (address => uint) balances;
Sabendo como funcionam os mapeamentos, podemos calcular o número do slot onde o saldo de uma conta é mantido:
const valueSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['address', 'uint'],
[account, balanceSlot]
),
);
Onde balanceSlot é
o slot onde o mapeamento é declarado. Legal, mas como descobrimos qual é o slot, para qualquer contrato de token? Para DAI, por exemplo, poderíamos passar pelo código do contrato no etherscan e apenas contar as variáveis declaradas, até encontrarmos o mapeamento dos saldos. No entanto, isso parece um trabalho manual e tedioso, especialmente se quisermos usar um grande número de tokens reais em nossos testes. Existem ferramentas para analisar o layout de armazenamento, mas elas ainda exigiriam algum trabalho manual.
E se pudéssemos automatizar a localização do valor de balanceSlot
de alguma forma, dado apenas o endereço do token?
Vamos inverter a questão, e ao invés de perguntar qual balanceSlot
é o valor, vamos perguntar: Se soubéssemos o valor do balanceSlot
, como verificaríamos que é de fato o mapeamento de saldos?
Se definirmos manualmente algum saldo para a conta.
const probe = '0x' + '1'.padStart(64);
network.provider.send('hardhat_setStorageAt', [valueSlot, probe]);
então chamar o token balanceOf
deve retornar o mesmo valor:
const balance = await token.balanceOf(account);
if (!balance.eq(ethers.BigNumber.from(probe)))
throw 'Nope, it's not the balances slot';
Então agora podemos apenas iterar sobre os números dos slots para encontrar balanceSlot
. Com o manuseio de alguns casos de borda e limpeza do armazenamento, o código final:
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!';
}
Esta técnica simples tem algumas limitações óbvias. Não funcionará se os saldos das contas não estiverem armazenados em um mapeamento de nível superior, por exemplo, em uma estrutura em algum lugar ou até mesmo em um contrato diferente. No entanto, pode ser estendido para outros dados ERC20 como allowances
ou para outros padrões.
Então é isso, boa codificação!
Sobre Euler
Euler é um protocolo de empréstimo sem permissão com eficiência de capital que ajuda os usuários a obter juros sobre seus ativos de criptografia ou se proteger contra mercados voláteis sem a necessidade de um terceiro confiável. A Euler apresenta uma série de inovações nunca vistas antes no DeFi, incluindo mercados de empréstimos sem permissão, taxas de juros reativas, garantias protegidas, liquidações resistentes a MEV, pools de estabilidade multicolateral, subcontas, empréstimos ajustados ao risco e muito mais. Para obter mais informações, visite euler.finance .
Junte-se à Comunidade
Siga-nos no Twitter. Junte-se ao nosso Discord. Mantenha contato no Telegram (comunidade , anúncios). Confira nosso site.
Oldest comments (0)