ethers-override
Version:
> ⚡🚀 SDK for `eth_call` override.
118 lines (117 loc) • 5.39 kB
JavaScript
import { Interface, JsonRpcProvider, keccak256, solidityPacked, toBeHex, zeroPadValue } from "ethers";
import { AddressAsIf } from "./addressAsIf.js";
import erc20Abis from "./abi/erc20.json" with { type: "json" };
const erc20Interface = new Interface(erc20Abis.abi);
const findSlotKeyInternal = async (toAddress, data, provider) => {
const params = [{
to: toAddress,
gas: "0x4c4b40",
gasPrice: "0x9184e72a000",
value: "0x0",
data,
}];
const res = await provider.send("eth_createAccessList", params);
// console.log(JSON.stringify(res, null, 2));
const access = res.accessList.find(a => a.address.toLowerCase() === toAddress.toLowerCase());
if (access === undefined || access.storageKeys === undefined) {
throw new Error(`Failed to get slot keys for address ${toAddress} with calldata: ${data}.`);
}
return access.storageKeys.filter(key => !key.startsWith("0x000000000000000000000000")).map(key => key.toLowerCase());
};
const getBalanceOfSlotKey = (ownerAddress, mappingSlotNumber) => {
return keccak256(zeroPadValue(ownerAddress, 32) + toBeHex(mappingSlotNumber, 32).slice(2)).toLowerCase();
};
const balanceOfSlotNumberByToken = new Map();
const getAllowanceSlotKey = (ownerAddress, spenderAddress, mappingSlotNumber) => {
return keccak256(zeroPadValue(spenderAddress, 32)
+ keccak256(zeroPadValue(ownerAddress, 32)
+ toBeHex(mappingSlotNumber, 32).slice(2)).slice(2));
};
const allowanceSlotNumberByToken = new Map();
export class TokenAsIf extends AddressAsIf {
constructor(address) {
super(address);
this._method = undefined;
this._provider = undefined;
}
connect(provider) {
this._provider = provider;
return this;
}
balanceOf(ownerAddress) {
this._method = "balanceOf";
const mapSlotNumber = balanceOfSlotNumberByToken.get(this.address);
if (mapSlotNumber === undefined) {
const calldata = erc20Interface.encodeFunctionData("balanceOf", [ownerAddress]);
if (this._provider === undefined) {
throw new Error(`Could not detect balanceOf mapping slot number as no provider is connected to TokenAsIf ${this.address}.`);
}
const promise = findSlotKeyInternal(this.address, calldata, this._provider)
.then(visitedSlotKeys => {
for (let mappingSlotNumber = 0; mappingSlotNumber < 20; mappingSlotNumber++) {
const key = getBalanceOfSlotKey(ownerAddress, mappingSlotNumber);
const slotKey = visitedSlotKeys.find(vk => vk === key);
if (slotKey !== undefined) {
// console.log(`slotNumber: ${mappingSlotNumber}`);
balanceOfSlotNumberByToken.set(this.address, mappingSlotNumber);
super.stateDiff(slotKey);
break;
}
}
});
this._registeredPromises.push(promise);
}
else {
super.stateDiff(getBalanceOfSlotKey(ownerAddress, mapSlotNumber));
}
return this;
}
allowance(ownerAddress, spenderAddress) {
this._method = "allowance";
const mapSlotNumber = allowanceSlotNumberByToken.get(this.address);
if (mapSlotNumber === undefined) {
const calldata = erc20Interface.encodeFunctionData("allowance", [ownerAddress, spenderAddress]);
if (this._provider === undefined) {
throw new Error(`Could not detect allowance mapping slot number as no provider is connected to TokenAsIf ${this.address}.`);
}
const promise = findSlotKeyInternal(this.address, calldata, this._provider)
.then(visitedSlotKeys => {
for (let mappingSlotNumber = 0; mappingSlotNumber < 20; mappingSlotNumber++) {
const key = getAllowanceSlotKey(ownerAddress, spenderAddress, mappingSlotNumber);
const slotKey = visitedSlotKeys.find(vk => vk === key);
if (slotKey !== undefined) {
// console.log(`slotNumber: ${mappingSlotNumber}`);
allowanceSlotNumberByToken.set(this.address, mappingSlotNumber);
super.stateDiff(slotKey);
break;
}
}
});
this._registeredPromises.push(promise);
}
else {
super.stateDiff(getAllowanceSlotKey(ownerAddress, spenderAddress, mapSlotNumber));
}
return this;
}
is(value) {
if (this._method === undefined) {
throw new Error(`Method to call of AsIf at ${this.address} is not set!`);
}
if (this._method === "balanceOf" || this._method === "allowance") {
if (typeof value !== 'bigint') {
throw new Error(`Invalid value for ${this._method} of AsIf at ${this.address}.`);
}
const slotValue = solidityPacked(["uint256"], [value]);
super.is(slotValue);
}
return this;
}
}
;
export const tokenAt = (address) => {
return new TokenAsIf(address);
};
export const exportedForTesting = {
getBalanceOfSlotKey, getAllowanceSlotKey
};