Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add permit2 allowance storage slot utils #4

Merged
merged 4 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);

});
Loading