Skip to content

Commit

Permalink
Merge pull request #1 from d3or/add-approval-fallback-slot
Browse files Browse the repository at this point in the history
Add approval fallback slot
  • Loading branch information
d3or authored Jul 11, 2024
2 parents f617e7c + 04d6f2f commit b93282d
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 65 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@d3or/slotseek",
"version": "1.0.0",
"version": "1.0.1",
"description": "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 balance or approval",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
178 changes: 123 additions & 55 deletions src/approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ export const generateMockApprovalData = async (
spenderAddress,
mockAddress,
mockApprovalAmount,
maxSlots = 100,
maxSlots = 30,
useFallbackSlot = false
}: {
tokenAddress: string;
ownerAddress: string;
spenderAddress: string;
mockAddress: string;
mockApprovalAmount: string;
maxSlots?: number;
useFallbackSlot?: boolean;
}
): Promise<{
slot: string;
Expand All @@ -40,7 +42,9 @@ export const generateMockApprovalData = async (
tokenAddress,
ownerAddress,
spenderAddress,
maxSlots
maxSlots,
useFallbackSlot

);

// make sure its padded to 32 bytes, and convert to a BigNumber
Expand Down Expand Up @@ -103,76 +107,140 @@ export const getErc20ApprovalStorageSlot = async (
erc20Address: string,
ownerAddress: string,
spenderAddress: string,
maxSlots: number
maxSlots: number,
useFallbackSlot = false
): Promise<{
slot: string;
slotHash: string;
isVyper: boolean;
}> => {
// Get the approval for the spender, that we can use to find the slot
const approval = await getErc20Approval(
let approval = await getErc20Approval(
provider,
erc20Address,
ownerAddress,
spenderAddress
);

// If the approval is 0, we can't find the slot, so throw an error
if (approval.eq(0)) {
throw new Error("Approval does not exist");
}
if (approval.gt(0)) {
for (let i = 0; i < maxSlots; i++) {
// Calculate the slot hash, using the owner address and the slot index
const slot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256"],
[ownerAddress, i]
)
);
// Calculate the storage slot, using the spender address and the slot hash
const storageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "bytes32"],
[spenderAddress, slot]
)
);
// 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.BigNumber.from(storageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(i).toHexString(),
slotHash: slot,
isVyper: false,
};
}

for (let i = 0; i < maxSlots; i++) {
// Caalculate the slot hash, using the owner address and the slot index
const slot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256"],
[ownerAddress, i]
)
);
// Calculate the storage slot, using the spender address and the slot hash
const storageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "bytes32"],
[spenderAddress, slot]
)
);
// 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.BigNumber.from(storageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(i).toHexString(),
slotHash: slot,
isVyper: false,
};
// check via vyper storage layout, which uses keccak256(abi.encode(slot, address(this))) instead of keccak256(abi.encode(address(this), slot))
const vyperSlotHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["uint256", "address"],
[i, ownerAddress]
)
);

const vyperStorageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "address"],
[vyperSlotHash, spenderAddress]
)
);
const vyperStorageValue = await provider.getStorageAt(
erc20Address,
vyperStorageSlot
);
if (ethers.BigNumber.from(vyperStorageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(i).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
}
}
if (!useFallbackSlot)
throw new Error("Approval does not exist");
}

// check via vyper storage layout, which uses keccak256(abi.encode(slot, address(this))) instead of keccak256(abi.encode(address(this), slot))
const vyperSlotHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["uint256", "address"],
[i, ownerAddress]
)
);

const vyperStorageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "address"],
[vyperSlotHash, spenderAddress]
)
);
const vyperStorageValue = await provider.getStorageAt(
erc20Address,
vyperStorageSlot
);
if (ethers.BigNumber.from(vyperStorageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(i).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
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 if contract is solidity/vyper, so we know which storage slot generation method to use
// TODO: add this above check, currently not sure of how to programatically check if a contract is a vyper contract.
const isVyper = false;
if (isVyper) {
const vyperSlotHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["uint256", "address"],
[fallbackSlot, ownerAddress]
)
);

const vyperStorageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "address"],
[vyperSlotHash, spenderAddress]
)
);
const vyperStorageValue = await provider.getStorageAt(
erc20Address,
vyperStorageSlot
);
if (ethers.BigNumber.from(vyperStorageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(fallbackSlot).toHexString(),
slotHash: vyperSlotHash,
isVyper: true,
};
}
} else {

const slot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256"],
[ownerAddress, fallbackSlot]
)
);
// Calculate the storage slot, using the spender address and the slot hash
const storageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["address", "bytes32"],
[spenderAddress, slot]
)
);
// 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.BigNumber.from(storageValue).eq(approval)) {
return {
slot: ethers.BigNumber.from(fallbackSlot).toHexString(),
slotHash: slot,
isVyper: false,
};

}

}

}

throw new Error("Unable to find approval slot");
Expand Down
2 changes: 1 addition & 1 deletion src/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const generateMockBalanceData = async (
holderAddress,
mockAddress,
mockBalanceAmount,
maxSlots = 100,
maxSlots = 30,
}: {
tokenAddress: string;
holderAddress: string;
Expand Down
68 changes: 68 additions & 0 deletions tests/integration/mockErc20Approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,72 @@ describe("mockErc20Approval", () => {
// check the approval
expect(approval.toString()).toBe(mockApprovalAmount);
}, 30000);

it("should mock the approval of an address for an ERC20 token, using the fallback slot", async () => {
const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const ownerAddress = ethers.Wallet.createRandom().address;
const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const mockApprovalAmount =
"1461501637330902918203684832716283019655932142975";
const maxSlots = 30;

// get approval of spenderAddress before, to make sure its 0 before we mock it
const approvalBefore = await getErc20Approval(
baseProvider,
tokenAddress,
mockAddress,
spenderAddress
);
expect(approvalBefore.toString()).toBe("0");

const data = await generateMockApprovalData(baseProvider, {
tokenAddress,
ownerAddress,
spenderAddress,
mockAddress,
mockApprovalAmount,
maxSlots,
useFallbackSlot: true
});

// Create the stateDiff object
const stateDiff = {
[tokenAddress]: {
stateDiff: {
[data.slot]: data.approval,
},
},
};

// Function selector for allowance(address,address)
const allowanceSelector = "0xdd62ed3e";
// Encode the owner and spender addresses
const encodedAddresses = ethers.utils.defaultAbiCoder
.encode(["address", "address"], [mockAddress, spenderAddress])
.slice(2);
const getAllowanceCalldata = allowanceSelector + encodedAddresses;

const callParams = [
{
to: tokenAddress,
data: getAllowanceCalldata,
},
"latest",
];

const allowanceResponse = await baseProvider.send("eth_call", [
...callParams,
stateDiff,
]);

// convert the response to a BigNumber
const approval = ethers.BigNumber.from(
ethers.utils.defaultAbiCoder.decode(["uint256"], allowanceResponse)[0]
);

// check the approval
expect(approval.eq(mockApprovalAmount)).toBe(true);
}, 30000);


});
4 changes: 2 additions & 2 deletions tests/unit/approvals/generateMockApprovalData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("generateMockApprovalData", () => {
const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const mockApprovalAmount = "1000000";
const maxSlots = 100;
const maxSlots = 30;

const data = await generateMockApprovalData(baseProvider, {
tokenAddress,
Expand All @@ -38,7 +38,7 @@ describe("generateMockApprovalData", () => {
const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const mockAddress = ethers.Wallet.createRandom().address;
const mockApprovalAmount = "1000000";
const maxSlots = 100;
const maxSlots = 30;

const data = await generateMockApprovalData(ethProvider, {
tokenAddress,
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/approvals/getErc20ApprovalStorageSlot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("getErc20ApprovalStorageSlot", () => {
const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const maxSlots = 100;
const maxSlots = 30;
const { slot, slotHash, isVyper } = await getErc20ApprovalStorageSlot(
baseProvider,
tokenAddress,
Expand All @@ -34,7 +34,7 @@ describe("getErc20ApprovalStorageSlot", () => {
const tokenAddress = "0xD533a949740bb3306d119CC777fa900bA034cd52";
const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const maxSlots = 100;
const maxSlots = 30;
const { slot, slotHash, isVyper } = await getErc20ApprovalStorageSlot(
ethProvider,
tokenAddress,
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/balances/generateMockBalanceData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("generateMockBalanceData", () => {
const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const mockAddress = "0x3e34b27a9bf37D8424e1a58aC7fc4D06914B76B9";
const mockBalanceAmount = "9600000";
const maxSlots = 100;
const maxSlots = 30;

const data = await generateMockBalanceData(baseProvider, {
tokenAddress,
Expand All @@ -34,7 +34,7 @@ describe("generateMockBalanceData", () => {
const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const mockAddress = ethers.Wallet.createRandom().address;
const mockBalanceAmount = "1000000";
const maxSlots = 100;
const maxSlots = 30;

const data = await generateMockBalanceData(ethProvider, {
tokenAddress,
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/balances/getErc20BalanceStorageSlot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("getErc20BalanceStorageSlot", () => {
it("should return the slot and balance for the holder", async () => {
const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const maxSlots = 100;
const maxSlots = 30;
const { slot, balance, isVyper } = await getErc20BalanceStorageSlot(
baseProvider,
tokenAddress,
Expand All @@ -30,7 +30,7 @@ describe("getErc20BalanceStorageSlot", () => {
it("[vyper] should return the slot and balance for the holder", async () => {
const tokenAddress = "0xD533a949740bb3306d119CC777fa900bA034cd52";
const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
const maxSlots = 100;
const maxSlots = 30;
const { slot, balance, isVyper } = await getErc20BalanceStorageSlot(
ethProvider,
tokenAddress,
Expand Down

0 comments on commit b93282d

Please sign in to comment.