Skip to content

Commit

Permalink
Merge pull request #4 from d3or/add-permit2-slotseeking
Browse files Browse the repository at this point in the history
feat: add permit2 allowance storage slot utils
  • Loading branch information
d3or authored Nov 16, 2024
2 parents fd38367 + ec6f909 commit cd22022
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 6 deletions.
77 changes: 75 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
<a href="https://twitter.com/intent/follow?screen_name=deor"><img src="https://img.shields.io/twitter/follow/deor.svg?style=social&label=Follow%20@deor" alt="Follow on Twitter" /></a>
<a href="https://github.com/d3or/slotseek/actions/workflows/test.yml"><img src="https://github.com/d3or/slotseek/actions/workflows/test.yml/badge.svg" alt="Build Status" /></a>

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.

For example, estimating the gas a transaction will consume when swapping, before the user has approved the contract to spend their tokens.

## 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)

Expand Down Expand Up @@ -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);
```
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.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",
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getErc20BalanceStorageSlot,
} from "./balance";

import { computePermit2AllowanceStorageSlot, getPermit2ERC20Allowance } from "./permit2"

export {
approvalCache,
balanceCache,
Expand All @@ -19,4 +21,6 @@ export {
getErc20BalanceStorageSlot,
getErc20Approval,
getErc20Balance,
getPermit2ERC20Allowance,
computePermit2AllowanceStorageSlot
};
69 changes: 69 additions & 0 deletions src/permit2.ts
Original file line number Diff line number Diff line change
@@ -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<ethers.BigNumber> => {
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;
};


75 changes: 75 additions & 0 deletions tests/integration/mockPermit2Approval.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
2 changes: 1 addition & 1 deletion tests/unit/approvals/getErc20Approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("getErc20Approval", () => {
);
expect(approval).toBeDefined();
expect(approval.toString()).toBe(
"1461501637330902918203684832716283019655932142975"
"1461501637330902918203684832716283019655931142975"
);
}, 30000);

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/balances/getErc20Balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/balances/getErc20BalanceStorageSlot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
27 changes: 27 additions & 0 deletions tests/unit/permit2/computePermit2AllowanceStorageSlot.test.ts
Original file line number Diff line number Diff line change
@@ -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);

});
44 changes: 44 additions & 0 deletions tests/unit/permit2/getPermit2Erc20Allowance.test.ts
Original file line number Diff line number Diff line change
@@ -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);

});

0 comments on commit cd22022

Please sign in to comment.