@d3or/slotseek
Version:
A library for finding the storage slots on an ERC20 token for balances and approvals, which can be used to mock the balances and approvals of an address when estimating gas costs of transactions that would fail if the address did not have the required bal
136 lines • 7.32 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getErc20Balance = exports.getErc20BalanceStorageSlot = exports.generateMockBalanceData = void 0;
const ethers_1 = require("ethers");
const cache_1 = require("./cache");
/**
* Generate mock data for a given ERC20 token balance
* @param provider - The JsonRpcProvider instance
* @param tokenAddress - The address of the ERC20 token
* @param holderAddress - The address of the holder, used to find the balance slot
* @param mockAddress - The user address to mock the balance for
* @param mockBalance - The balance to mock the balance for, if not provided, defaults to the balance of the holder
* @param maxSlots - The maximum number of slots to search
* @returns An object containing the slot and balance
*
*/
const generateMockBalanceData = async (provider, { tokenAddress, holderAddress, mockAddress, mockBalanceAmount, maxSlots = 30, }) => {
// get the slot for token balance mapping: mapping(address => uint256)
const { slot, balance, isVyper } = await (0, exports.getErc20BalanceStorageSlot)(provider, tokenAddress, holderAddress, maxSlots);
// make sure its padded to 32 bytes, and convert to a BigNumber
const mockBalanceHex = ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.utils.hexlify(mockBalanceAmount ? ethers_1.ethers.BigNumber.from(mockBalanceAmount) : balance), 32);
// Calculate the storage slot key
let index;
if (!isVyper) {
index = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [mockAddress, slot]));
}
else {
// if vyper, we need to use the keccak256(abi.encode(slot, address(this))) instead of keccak256(abi.encode(address(this), slot))
index = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["uint256", "address"], [slot, mockAddress]));
}
return {
slot: index,
balance: mockBalanceHex,
isVyper,
};
};
exports.generateMockBalanceData = generateMockBalanceData;
/**
* Get the storage slot for a given ERC20 token balance
* @param provider - The JsonRpcProvider instance
* @param erc20Address - The address of the ERC20 token
* @param holderAddress - The address of the holder, used to find the balance slot
* @param maxSlots - The maximum number of slots to search
* @returns An object containing the slot and balance
*
* - This uses a brute force approach to find the storage slot for the balance of the holder, so we can mock it. There are better ways to do this outside of just interacting directly with the contract over RPC, but its difficult to do so without needing to setup more tools/infra, especially for multi chain supoprt and gas estimation at runtime.
*/
const getErc20BalanceStorageSlot = async (provider, erc20Address, holderAddress, maxSlots = 30) => {
// check the cache
const cachedValue = cache_1.balanceCache.get(erc20Address.toLowerCase());
if (cachedValue) {
if (cachedValue.isVyper) {
const { vyperSlotHash } = calculateBalanceVyperStorageSlot(holderAddress, cachedValue.slot);
const vyperBalance = await provider.getStorageAt(erc20Address, vyperSlotHash);
return {
slot: ethers_1.ethers.BigNumber.from(cachedValue.slot).toHexString(),
balance: ethers_1.ethers.BigNumber.from(vyperBalance),
isVyper: true,
};
}
else {
const { slotHash } = calculateBalanceSolidityStorageSlot(holderAddress, cachedValue.slot);
const balance = await provider.getStorageAt(erc20Address, slotHash);
return {
slot: ethers_1.ethers.BigNumber.from(cachedValue.slot).toHexString(),
balance: ethers_1.ethers.BigNumber.from(balance),
isVyper: false,
};
}
}
// Get the balance of the holder, that we can use to find the slot
const userBalance = await (0, exports.getErc20Balance)(provider, erc20Address, holderAddress);
// If the balance is 0, we can't find the slot, so throw an error
if (userBalance.eq(0)) {
throw new Error("User has no balance");
}
// We iterate over maxSlots, maxSlots is set to 100 by default, its unlikely that an erc20 token will be using up more than 100 slots tbh
// For each slot, we compute the storage slot key [holderAddress, slot index] and get the value at that storage slot
// If the value at the storage slot is equal to the balance, return the slot as we have found the correct slot for balances
for (let i = 0; i < maxSlots; i++) {
const { slotHash } = calculateBalanceSolidityStorageSlot(holderAddress, i);
const balance = await provider.getStorageAt(erc20Address, slotHash);
if (ethers_1.ethers.BigNumber.from(balance).eq(userBalance)) {
cache_1.balanceCache.set(erc20Address.toLowerCase(), {
slot: i,
isVyper: false,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(i).toHexString(),
balance: ethers_1.ethers.BigNumber.from(balance),
isVyper: false,
};
}
const { vyperSlotHash } = calculateBalanceVyperStorageSlot(holderAddress, i);
const vyperBalance = await provider.getStorageAt(erc20Address, vyperSlotHash);
if (ethers_1.ethers.BigNumber.from(vyperBalance).eq(userBalance)) {
cache_1.balanceCache.set(erc20Address.toLowerCase(), {
slot: i,
isVyper: true,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(i).toHexString(),
balance: ethers_1.ethers.BigNumber.from(vyperBalance),
isVyper: true,
};
}
}
throw new Error("Unable to find balance slot");
};
exports.getErc20BalanceStorageSlot = getErc20BalanceStorageSlot;
const calculateBalanceSolidityStorageSlot = (holderAddress, slotNumber) => {
const slotHash = ethers_1.ethers.utils.solidityKeccak256(["uint256", "uint256"], [holderAddress, slotNumber]);
return { slotHash };
};
const calculateBalanceVyperStorageSlot = (holderAddress, slotNumber) => {
// create hash via vyper storage layout, which uses keccak256(abi.encode(slot, address(this))) instead of keccak256(abi.encode(address(this), slot))
const vyperSlotHash = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["uint256", "address"], [slotNumber, holderAddress]));
return { vyperSlotHash };
};
/**
* Get the balance of a given address for a given ERC20 token
* @param provider - The JsonRpcProvider instance
* @param address - The address of the ERC20 token
* @param addressToCheck - The address to check the balance of
* @returns The balance of the address
*
*/
const getErc20Balance = async (provider, address, addressToCheck) => {
const contract = new ethers_1.ethers.Contract(address, ["function balanceOf(address owner) view returns (uint256)"], provider);
const balance = await contract.balanceOf(addressToCheck);
return balance;
};
exports.getErc20Balance = getErc20Balance;
//# sourceMappingURL=balance.js.map
;