-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from d3or/add-permit2-slotseeking
feat: add permit2 allowance storage slot utils
- Loading branch information
Showing
10 changed files
with
298 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
tests/unit/permit2/computePermit2AllowanceStorageSlot.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
}); |