@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
182 lines • 9.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getErc20Approval = exports.getErc20ApprovalStorageSlot = exports.generateMockApprovalData = void 0;
const ethers_1 = require("ethers");
const cache_1 = require("./cache");
/**
* Generate mock approval data for a given ERC20 token
* @param provider - The JsonRpcProvider instance
* @param tokenAddress - The address of the ERC20 token
* @param ownerAddress - The address of the owner
* @param spenderAddress - The address of the spender
* @param mockAddress - The address to mock the approval for
* @param mockApprovalAmount - The amount to mock the approval for
* @param maxSlots - The maximum number of slots to search
* @returns An object containing the slot and approval data
*
*/
const generateMockApprovalData = async (provider, { tokenAddress, ownerAddress, spenderAddress, mockAddress, mockApprovalAmount, maxSlots = 30, useFallbackSlot = false }) => {
// get the slot for the approval mapping, mapping(address account => mapping(address spender => uint256))
const { slot, isVyper } = await (0, exports.getErc20ApprovalStorageSlot)(provider, tokenAddress, ownerAddress, spenderAddress, maxSlots, useFallbackSlot);
// make sure its padded to 32 bytes, and convert to a BigNumber
const mockApprovalHex = ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.utils.hexlify(ethers_1.ethers.BigNumber.from(mockApprovalAmount)), 32);
let index;
if (!isVyper) {
const newSlotHash = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [mockAddress, slot]));
// Calculate the storage slot key
index = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["address", "bytes32"], [spenderAddress, newSlotHash]));
}
else {
const newSlotHash = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["uint256", "address"], [slot, mockAddress]));
// Calculate the storage slot key
index = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["bytes32", "address"], [newSlotHash, spenderAddress]));
}
return {
slot: index,
approval: mockApprovalHex,
isVyper,
};
};
exports.generateMockApprovalData = generateMockApprovalData;
/**
* Get the storage slot for a given ERC20 token approval
* @param provider - The JsonRpcProvider instance
* @param erc20Address - The address of the ERC20 token
* @param ownerAddress - The address of the owner, used to find the approval slot
* @param spenderAddress - The address of the spender, used to find the approval slot
* @param maxSlots - The maximum number of slots to search
* @returns The slot for the approval
*
* - This uses a brute force approach similar to the balance slot search. See the balance slot search comment for more details.
*/
const getErc20ApprovalStorageSlot = async (provider, erc20Address, ownerAddress, spenderAddress, maxSlots, useFallbackSlot = false) => {
// check the cache
const cachedValue = cache_1.approvalCache.get(erc20Address.toLowerCase());
if (cachedValue) {
if (cachedValue.isVyper) {
const { vyperSlotHash } = calculateApprovalVyperStorageSlot(ownerAddress, spenderAddress, cachedValue.slot);
return {
slot: ethers_1.ethers.BigNumber.from(cachedValue.slot).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
}
else {
const { slotHash } = calculateApprovalSolidityStorageSlot(ownerAddress, spenderAddress, cachedValue.slot);
return {
slot: ethers_1.ethers.BigNumber.from(cachedValue.slot).toHexString(),
slotHash: slotHash,
isVyper: false,
};
}
}
// Get the approval for the spender, that we can use to find the slot
let approval = await (0, exports.getErc20Approval)(provider, erc20Address, ownerAddress, spenderAddress);
if (approval.gt(0)) {
for (let i = 0; i < maxSlots; i++) {
const { storageSlot, slotHash } = calculateApprovalSolidityStorageSlot(ownerAddress, spenderAddress, i);
// Get the value at the storage slot
const storageValue = await provider.getStorageAt(erc20Address, storageSlot);
// If the value at the storage slot is equal to the approval, return the slot as we have found the correct slot for approvals
if (ethers_1.ethers.BigNumber.from(storageValue).eq(approval)) {
cache_1.approvalCache.set(erc20Address.toLowerCase(), {
slot: i,
isVyper: false,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(i).toHexString(),
slotHash: slotHash,
isVyper: false,
};
}
const { vyperStorageSlot, vyperSlotHash } = calculateApprovalVyperStorageSlot(ownerAddress, spenderAddress, i);
const vyperStorageValue = await provider.getStorageAt(erc20Address, vyperStorageSlot);
if (ethers_1.ethers.BigNumber.from(vyperStorageValue).eq(approval)) {
cache_1.approvalCache.set(erc20Address.toLowerCase(), {
slot: i,
isVyper: false,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(i).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
}
}
if (!useFallbackSlot)
throw new Error("Approval does not exist");
}
if (useFallbackSlot) {
// if useFallBackSlot = true, then we are just going to assume the slot is at the slot which is most common for erc20 tokens. for approvals, this is slot #10
const fallbackSlot = 10;
// check solidity, then check vyper.
// (dont have an easy way to check if a contract is solidity/vyper)
const { storageSlot, slotHash } = calculateApprovalSolidityStorageSlot(ownerAddress, spenderAddress, fallbackSlot);
// Get the value at the storage slot
const storageValue = await provider.getStorageAt(erc20Address, storageSlot);
// If the value at the storage slot is equal to the approval, return the slot as we have found the correct slot for approvals
if (ethers_1.ethers.BigNumber.from(storageValue).eq(approval)) {
cache_1.approvalCache.set(erc20Address.toLowerCase(), {
slot: fallbackSlot,
isVyper: false,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(fallbackSlot).toHexString(),
slotHash: slotHash,
isVyper: false,
};
}
// check vyper
const { vyperStorageSlot, vyperSlotHash } = calculateApprovalVyperStorageSlot(ownerAddress, spenderAddress, fallbackSlot);
const vyperStorageValue = await provider.getStorageAt(erc20Address, vyperStorageSlot);
if (ethers_1.ethers.BigNumber.from(vyperStorageValue).eq(approval)) {
cache_1.approvalCache.set(erc20Address.toLowerCase(), {
slot: fallbackSlot,
isVyper: true,
ts: Date.now()
});
return {
slot: ethers_1.ethers.BigNumber.from(fallbackSlot).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
}
}
throw new Error("Unable to find approval slot");
};
exports.getErc20ApprovalStorageSlot = getErc20ApprovalStorageSlot;
// Generates approval solidity storage slot data
const calculateApprovalSolidityStorageSlot = (ownerAddress, spenderAddress, slotNumber) => {
// Calculate the slot hash, using the owner address and the slot index
const slotHash = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [ownerAddress, slotNumber]));
// Calculate the storage slot, using the spender address and the slot hash
const storageSlot = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["address", "bytes32"], [spenderAddress, slotHash]));
return { storageSlot, slotHash };
};
// Generates approval vyper storage slot data
const calculateApprovalVyperStorageSlot = (ownerAddress, spenderAddress, slotNumber) => {
// create 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, ownerAddress]));
const vyperStorageSlot = ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.defaultAbiCoder.encode(["bytes32", "address"], [vyperSlotHash, spenderAddress]));
return { vyperStorageSlot, vyperSlotHash };
};
/**
* Get the approval for a given ERC20 token
* @param provider - The JsonRpcProvider instance
* @param address - The address of the ERC20 token
* @param ownerAddress - The address of the owner
* @param spenderAddress - The address of the spender
* @returns The approval amount
*/
const getErc20Approval = async (provider, address, ownerAddress, spenderAddress) => {
const contract = new ethers_1.ethers.Contract(address, [
"function allowance(address owner, address spender) view returns (uint256)",
], provider);
const approval = await contract.allowance(ownerAddress, spenderAddress);
return approval;
};
exports.getErc20Approval = getErc20Approval;
//# sourceMappingURL=approval.js.map