diff --git a/README.md b/README.md
index 0bf9d6b..027fec7 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-slotseek is a javascript library that assists with finding the storage slots for the `balanceOf` and `allowance` mappings in an ERC20 token contract. It also provides a way to generate mock data that can be used to override the state of a contract in an `eth_call` or `eth_estimateGas` call.
+slotseek is a javascript library that assists with finding the storage slots for the `balanceOf` and `allowance` mappings in an ERC20 token contract, and the permit2 allowance mapping. It also provides a way to generate mock data that can be used to override the state of a contract in an `eth_call` or `eth_estimateGas` call.
The main use case for this library is to estimate gas costs of transactions that would fail if the address did not have the required balance or approval.
@@ -12,7 +12,7 @@ For example, estimating the gas a transaction will consume when swapping, before
## Features
-- Find storage slots for `balanceOf` and `allowance` mappings in an ERC20 token contract
+- Find storage slots for `balanceOf` and `allowance` mappings in an ERC20 token contract, and permit2 allowance mapping
- Generates mock data that can be used to override the state of a contract in an `eth_call`/`eth_estimateGas` call
- Supports [vyper storage layouts](https://docs.vyperlang.org/en/stable/scoping-and-declarations.html#storage-layout)
@@ -209,3 +209,76 @@ async function findStorageSlot() {
findStorageSlot().catch(console.error);
```
+
+## Example of mocking the permit2 allowance mapping
+
+```javascript
+import { ethers } from "ethers";
+import { computePermit2AllowanceStorageSlot } from "@d3or/slotseek";
+
+async function findStorageSlot() {
+ // Setup - Base RPC
+ const provider = new ethers.providers.JsonRpcProvider(
+ "https://mainnet.base.org"
+ );
+
+ // Constants
+ const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; // USDC on Base
+ const mockAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"; // USDC holder to mock approval for
+ const spenderAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
+
+ // Compute storage slot of where the allowance would be held
+ const { slot } = computePermit2AllowanceStorageSlot(mockAddress, tokenAddress, spenderAddress)
+
+ const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'
+
+ // Prepare state diff object
+ const stateDiff = {
+ [permit2Contract]: {
+ stateDiff: {
+ [slot]: ethers.utils.hexZeroPad(
+ ethers.utils.hexlify(ethers.BigNumber.from("1461501637330902918203684832716283019655932142975")),
+ 32
+ )
+ ,
+ },
+ },
+ };
+
+ // Function selector for allowance(address,address,address)
+ const allowanceSelector = "0x927da105";
+ // Encode the owner and spender addresses
+ const encodedAddresses = ethers.utils.defaultAbiCoder
+ .encode(["address", "address", "address"], [mockAddress, tokenAddress, spenderAddress])
+ .slice(2);
+ const getAllowanceCalldata = allowanceSelector + encodedAddresses;
+
+
+ const callParams = [
+ {
+ to: permit2Contract,
+ data: getAllowanceCalldata,
+ },
+ "latest",
+ ];
+
+ const allowanceResponse = await baseProvider.send("eth_call", [
+ ...callParams,
+ stateDiff,
+ ]);
+
+ // convert the response to a BigNumber
+ const approvalAmount = ethers.BigNumber.from(
+ ethers.utils.defaultAbiCoder.decode(["uint256"], allowanceResponse)[0]
+ );
+
+ console.log(
+ `Mocked balance for ${mockAddress}: ${ethers.utils.formatUnits(
+ approvalAmount,
+ 6
+ )} USDC`
+ );
+
+}
+findStorageSlot().catch(console.error);
+```
diff --git a/package.json b/package.json
index ab21eda..35d6688 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3or/slotseek",
- "version": "1.0.2",
+ "version": "1.1.2",
"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",
diff --git a/src/index.ts b/src/index.ts
index 85f95ac..4c7c58c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,8 @@ import {
getErc20BalanceStorageSlot,
} from "./balance";
+import { computePermit2AllowanceStorageSlot, getPermit2ERC20Allowance } from "./permit2"
+
export {
approvalCache,
balanceCache,
@@ -19,4 +21,6 @@ export {
getErc20BalanceStorageSlot,
getErc20Approval,
getErc20Balance,
+ getPermit2ERC20Allowance,
+ computePermit2AllowanceStorageSlot
};
diff --git a/src/permit2.ts b/src/permit2.ts
new file mode 100644
index 0000000..67c1b63
--- /dev/null
+++ b/src/permit2.ts
@@ -0,0 +1,69 @@
+import { ethers } from "ethers";
+
+
+/**
+ * Compute the storage slot for permit2 allowance.
+ * NOTE: unlike arbitrary erc20 contracts, we know the slot for where this is stored (1) :)
+ *
+ * @param erc20Address - The address of the ERC20 token
+ * @param ownerAddress - The address of the ERC20 token owner
+ * @param spenderAddress - The address of the spender
+ * @returns The slot where the allowance amount is stored, mock this
+ *
+ * - This uses a brute force approach similar to the balance slot search. See the balance slot search comment for more details.
+ */
+export const computePermit2AllowanceStorageSlot = (ownerAddress: string, erc20Address: string, spenderAddress: string): {
+ slot: string;
+} => {
+
+ // Calculate the slot hash, using the owner address and the slot index (1)
+ const ownerSlotHash = ethers.utils.keccak256(
+ ethers.utils.defaultAbiCoder.encode(
+ ["address", "uint256"],
+ [ownerAddress, 1]
+ )
+ );
+
+ // Calcualte the storage slot hash for spender slot
+ const tokenSlotHash = ethers.utils.keccak256(
+ ethers.utils.defaultAbiCoder.encode(
+ ["address", "bytes32"],
+ [erc20Address, ownerSlotHash]
+ )
+ );
+ // Calculate the final storage slot to mock, using the spender address and the slot hash2
+ const slot = ethers.utils.keccak256(
+ ethers.utils.defaultAbiCoder.encode(
+ ["address", "bytes32"],
+ [spenderAddress, tokenSlotHash]
+ )
+ );
+ return { slot }
+}
+
+
+/**
+ * Get the permit2 erc20 allowance for a given ERC20 token and spender
+ * @param provider - The JsonRpcProvider instance
+ * @param permit2Address - The permit2 contract address
+ * @param erc20Address - The address of the ERC20 token
+ * @param ownerAddress - The address of the ERC20 token owner
+ * @param spenderAddress - The address of the spender
+ * @returns The approval amount
+ */
+export const getPermit2ERC20Allowance = async (
+ provider: ethers.providers.JsonRpcProvider,
+ permit2Address: string,
+ ownerAddress: string, erc20Address: string, spenderAddress: string): Promise => {
+ const contract = new ethers.Contract(
+ permit2Address,
+ [
+ "function allowance(address owner, address token, address spender) view returns (uint256)",
+ ],
+ provider
+ );
+ const approval = await contract.allowance(ownerAddress, erc20Address, spenderAddress);
+ return approval;
+};
+
+
diff --git a/tests/integration/mockPermit2Approval.test.ts b/tests/integration/mockPermit2Approval.test.ts
new file mode 100644
index 0000000..7000a68
--- /dev/null
+++ b/tests/integration/mockPermit2Approval.test.ts
@@ -0,0 +1,75 @@
+import { ethers } from "ethers";
+import { computePermit2AllowanceStorageSlot, getPermit2ERC20Allowance } from "../../src";
+
+describe("mockErc20Approval", () => {
+ const baseProvider = new ethers.providers.JsonRpcProvider(
+ process.env.BASE_RPC_URL ?? "https://localhost:8545"
+ );
+
+ const mockAddress = ethers.Wallet.createRandom().address;
+
+ it("should mock a random address to have a permit2 allowance", async () => {
+ const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
+ const spenderAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
+ const mockApprovalAmount =
+ "1461501637330902918203684832716283019655932142975";
+ const mockApprovalHex = ethers.utils.hexZeroPad(
+ ethers.utils.hexlify(ethers.BigNumber.from(mockApprovalAmount)),
+ 32
+ )
+ const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'
+
+
+ const permit2Slot = computePermit2AllowanceStorageSlot(mockAddress, tokenAddress, spenderAddress)
+ expect(permit2Slot.slot).toBeDefined()
+
+ // get approval of spenderAddress before, to make sure its 0 before we mock it
+ const approvalBefore = await getPermit2ERC20Allowance(
+ baseProvider,
+ permit2Contract,
+ mockAddress,
+ tokenAddress,
+ spenderAddress
+ );
+ expect(approvalBefore.toString()).toBe("0");
+
+ // Create the stateDiff object
+ const stateDiff = {
+ [permit2Contract]: {
+ stateDiff: {
+ [permit2Slot.slot]: mockApprovalHex,
+ },
+ },
+ };
+
+ // Function selector for allowance(address,address,address)
+ const allowanceSelector = "0x927da105";
+ // Encode the owner and spender addresses
+ const encodedAddresses = ethers.utils.defaultAbiCoder
+ .encode(["address", "address", "address"], [mockAddress, tokenAddress, spenderAddress])
+ .slice(2);
+ const getAllowanceCalldata = allowanceSelector + encodedAddresses;
+
+
+ const callParams = [
+ {
+ to: permit2Contract,
+ 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);
+});
diff --git a/tests/unit/approvals/getErc20Approval.test.ts b/tests/unit/approvals/getErc20Approval.test.ts
index dab4ebe..3393d0c 100644
--- a/tests/unit/approvals/getErc20Approval.test.ts
+++ b/tests/unit/approvals/getErc20Approval.test.ts
@@ -22,7 +22,7 @@ describe("getErc20Approval", () => {
);
expect(approval).toBeDefined();
expect(approval.toString()).toBe(
- "1461501637330902918203684832716283019655932142975"
+ "1461501637330902918203684832716283019655931142975"
);
}, 30000);
diff --git a/tests/unit/balances/getErc20Balance.test.ts b/tests/unit/balances/getErc20Balance.test.ts
index 3d8bb5a..df4279c 100644
--- a/tests/unit/balances/getErc20Balance.test.ts
+++ b/tests/unit/balances/getErc20Balance.test.ts
@@ -19,7 +19,7 @@ describe("getErc20Balance", () => {
ownerAddress
);
expect(balance).toBeDefined();
- expect(balance.toString()).toBe("9600000");
+ expect(balance.toString()).toBe("8600000");
}, 30000);
it("[vyper] should return the balance for the owner", async () => {
diff --git a/tests/unit/balances/getErc20BalanceStorageSlot.test.ts b/tests/unit/balances/getErc20BalanceStorageSlot.test.ts
index 9ab2e8d..05e9cac 100644
--- a/tests/unit/balances/getErc20BalanceStorageSlot.test.ts
+++ b/tests/unit/balances/getErc20BalanceStorageSlot.test.ts
@@ -23,7 +23,7 @@ describe("getErc20BalanceStorageSlot", () => {
expect(slot).toBeDefined();
expect(balance).toBeDefined();
expect(slot).toBe("0x09");
- expect(balance.toString()).toBe("9600000");
+ expect(balance.toString()).toBe("8600000");
expect(isVyper).toBe(false);
}, 30000);
diff --git a/tests/unit/permit2/computePermit2AllowanceStorageSlot.test.ts b/tests/unit/permit2/computePermit2AllowanceStorageSlot.test.ts
new file mode 100644
index 0000000..346c05c
--- /dev/null
+++ b/tests/unit/permit2/computePermit2AllowanceStorageSlot.test.ts
@@ -0,0 +1,27 @@
+import { ethers } from "ethers";
+import { computePermit2AllowanceStorageSlot } from "../../../src";
+
+describe("computePermit2AllowanceStorageSlot", () => {
+ it("should compute a permit2 allowance storage slot", async () => {
+ const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
+ const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
+ const spenderAddress = "0x0000000000000000000000000000000000000000";
+
+ const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'
+
+ const data = computePermit2AllowanceStorageSlot(ownerAddress, tokenAddress, spenderAddress);
+ expect(data).toBeDefined();
+ expect(data.slot).toBeDefined();
+ expect(data.slot).toBe("0x31c9cad297553b4448680116a2d90c11b601cf1811034cd3bbe54da53c870184")
+
+ const baseProvider = new ethers.providers.JsonRpcProvider(
+ process.env.BASE_RPC_URL ?? "https://localhost:8545"
+ );
+
+ const valueAtStorageSlot = await baseProvider.getStorageAt(permit2Contract, data.slot)
+
+ expect(valueAtStorageSlot).toBeDefined()
+ expect(valueAtStorageSlot).toBe('0x00000000000001aa7be40acd0000000000000000000000000000000000000001')
+ }, 30000);
+
+});
diff --git a/tests/unit/permit2/getPermit2Erc20Allowance.test.ts b/tests/unit/permit2/getPermit2Erc20Allowance.test.ts
new file mode 100644
index 0000000..8eeb540
--- /dev/null
+++ b/tests/unit/permit2/getPermit2Erc20Allowance.test.ts
@@ -0,0 +1,44 @@
+import { computePermit2AllowanceStorageSlot, getPermit2ERC20Allowance } from "../../../src";
+import { ethers } from "ethers";
+
+describe("getPermit2ERC20Allowance", () => {
+ const baseProvider = new ethers.providers.JsonRpcProvider(
+ process.env.BASE_RPC_URL ?? "https://localhost:8545"
+ );
+
+ it("should return the permit2 ERC20 allowance for the owner", async () => {
+ const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
+ const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000";
+ const spenderAddress = "0x0000000000000000000000000000000000000000";
+
+ const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'
+ const allowance = await getPermit2ERC20Allowance(
+ baseProvider,
+ permit2Contract,
+ ownerAddress,
+ tokenAddress,
+ spenderAddress
+ );
+ expect(allowance).toBeDefined();
+ expect(allowance.toString()).toBe("1");
+
+ }, 30000);
+
+ it("should return 0 allowance for permit2 ERC20 allowance for vitalik", async () => {
+ const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
+ const ownerAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
+ const spenderAddress = "0x0000000000000000000000000000000000000000";
+
+ const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'
+ const allowance = await getPermit2ERC20Allowance(
+ baseProvider,
+ permit2Contract,
+ ownerAddress,
+ tokenAddress,
+ spenderAddress
+ );
+ expect(allowance).toBeDefined();
+ expect(allowance.toString()).toBe("0");
+ }, 30000);
+
+});