Skip to content

Instantly share code, notes, and snippets.

@satanworker
Created September 21, 2024 10:59
Show Gist options
  • Save satanworker/73fc19a1f50a3e0600a40e381a21feee to your computer and use it in GitHub Desktop.
Save satanworker/73fc19a1f50a3e0600a40e381a21feee to your computer and use it in GitHub Desktop.
import {
getContract,
keccak256,
toHex,
encodeAbiParameters,
parseAbiParameters,
pad,
encodePacked,
} from "viem";
import { Address, prepareWriteContract } from "@wagmi/core";
import { abis } from "../../changelog/bindings/human-readable-abis";
import {
viemClient,
DEFAULT_NONCE_WORD_POS,
PERMIT2_MESSAGE_TYPES,
PERMIT_MESSAGE_TYPES,
PermitExceptions,
emptySignedPermitParams,
} from "../constants";
import { toWei } from "@common/utils/numbers";
import { PERMIT2_ADDRESS } from "@uniswap/permit2-sdk";
import { ApprovalType } from "../../positions/store/store";
import { Hash } from "viem";
import { MaxUint256 } from "@uniswap/permit2-sdk";
import { hexToBinary } from "@common/utils/numbers";
import { includesCaseInsensitive } from "@common/utils/tokens";
import { signTypedData } from "@wagmi/core";
import { ethers, utils } from "ethers";
type uint256 = string;
type address = string;
type TokenPermissions = {
token: address;
amount: uint256;
};
type PermitTransferFrom = {
permitted: TokenPermissions;
spender: address;
nonce: uint256;
deadline: uint256;
};
export type SignedPermitParams = {
approvalType: ApprovalType;
nonce: bigint;
deadline: bigint;
v: number;
r: Hash;
s: Hash;
};
type Permit1ApprovalParams = {
approvalType: ApprovalType.PERMIT,
permit1Nonces: bigint,
tokenName: string,
domainSeparator: Address
}
type Permit2ApprovalParams = {
approvalType: ApprovalType.PERMIT2,
permit2Allowance: bigint,
permit2Nonces: string
}
type EmptyPermitApprovalParams = {
approvalType: ApprovalType.STANDARD
}
export type PermitApprovalParams = Permit1ApprovalParams | Permit2ApprovalParams | EmptyPermitApprovalParams
export type Permit2Payload = {
nonce: BigInt,
value: PermitTransferFrom
type: ApprovalType.PERMIT2,
deadline: bigint
domain: {
name: "Permit2",
chainId: number,
verifyingContract: typeof PERMIT2_ADDRESS
}
}
export type Permit1Payload = {
type: ApprovalType.PERMIT,
nonces: bigint,
domain: {
name: string,
version: string,
chainId: number,
verifyingContract: Address
},
value: {
owner: Address,
spender: Address,
value: bigint,
nonce: bigint,
deadline: bigint
}
}
export type EIP2612Domain = {
name: string;
version: string;
chainId: number;
verifyingContract: Address;
};
export class ERC20Service {
constructor() { }
getDefaultPermitDeadline(): bigint {
return BigInt(Math.floor(Date.now() / 1000) + 4200);
}
async getSymbol(address: Address) {
const ERC20Contract = getContract({
address,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.symbol();
}
async getName(address: Address) {
const ERC20Contract = getContract({
address,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.name();
}
async getNonces(walletAddress: Address, tokenAddress: Address) {
const ERC20Contract = getContract({
address: tokenAddress,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.nonces([walletAddress]);
}
async getDomainSeparator(tokenAddress: Address) {
const ERC20Contract = getContract({
address: tokenAddress,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.DOMAIN_SEPARATOR();
}
async getDecimals(address: Address) {
const ERC20Contract = getContract({
address,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.decimals();
}
async getNonceBitmap(
walletAddress: Address,
nonceWordPos: bigint
): Promise<bigint> {
const SignatureTransferContract = getContract({
address: PERMIT2_ADDRESS,
abi: abis["ISignatureTransfer"],
publicClient: viemClient,
});
return SignatureTransferContract.read.nonceBitmap([
walletAddress,
nonceWordPos,
]);
}
async getBalanceOf(tokenAddress: Address, accountAddress: Address) {
const ERC20Contract = getContract({
address: tokenAddress,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.balanceOf([accountAddress]);
}
async getAllowance(
tokenAddress: Address,
accountAddress: Address,
spender: Address
) {
const ERC20Contract = getContract({
address: tokenAddress,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
return ERC20Contract.read.allowance([accountAddress, spender]);
}
async convertAmountToWei({
tokenAddress,
amount,
}: {
tokenAddress: Address;
amount: bigint;
}): Promise<bigint> {
const ERC20Contract = getContract({
address: tokenAddress,
abi: abis["ERC20Permit"],
publicClient: viemClient,
});
const decimals = await ERC20Contract.read.decimals();
return toWei(amount.toString(), decimals);
}
async calculateApprovalType({
walletAddress,
vault
}: {
vault: {
tokenAddress: Address,
underlyingTokenAddress?: Address
}
walletAddress: Address
}): Promise<ApprovalType> {
const { tokenAddress, underlyingTokenAddress } = vault
if (!walletAddress) return ApprovalType.STANDARD;
if (includesCaseInsensitive(PermitExceptions, tokenAddress) || (underlyingTokenAddress && includesCaseInsensitive(PermitExceptions, underlyingTokenAddress))) {
console.log("permit 2")
return ApprovalType.PERMIT2
};
try {
await this.getNonces(walletAddress, tokenAddress);
console.log("permit 1")
return ApprovalType.PERMIT;
} catch (e) {
console.log("catch permit 2")
return ApprovalType.PERMIT2;
}
}
async getApprovalPermitParams({
vault,
walletAddress,
}: {
vault: {
tokenAddress: Address,
underlyingTokenAddress?: Address
},
walletAddress: Address,
}): Promise<PermitApprovalParams> {
const { tokenAddress } = vault
const approvalType = await this.calculateApprovalType({
vault,
walletAddress
})
if (approvalType == ApprovalType.PERMIT) {
const [permit1Nonces, tokenName, domainSeparator] = await Promise.all([
this.getNonces(walletAddress, tokenAddress),
this.getName(tokenAddress),
this.getDomainSeparator(tokenAddress)
])
return {
approvalType: ApprovalType.PERMIT,
domainSeparator,
permit1Nonces,
tokenName
}
} else if (approvalType == ApprovalType.PERMIT2) {
const [permit2Allowance, permit2Nonces] = await Promise.all([
this.getAllowance(
tokenAddress,
walletAddress,
PERMIT2_ADDRESS
),
this.getPermit2Nonces({ walletAddress }),
])
return {
approvalType: ApprovalType.PERMIT2,
permit2Allowance,
permit2Nonces
}
}
return {
approvalType: ApprovalType.STANDARD
}
}
async getPermit2Nonces({
walletAddress
}: { walletAddress: Address }): Promise<string> {
let wordPos = DEFAULT_NONCE_WORD_POS;
let bitmap = await this.getNonceBitmap(walletAddress, wordPos);
while (bitmap === BigInt(MaxUint256.toString())) {
wordPos = (wordPos + 1n) % BigInt(`0x1${"0".repeat(248 / 4)}`);
bitmap = await this.getNonceBitmap(walletAddress, wordPos);
}
const nonceBitmapStr = hexToBinary(bitmap.toString(16)).padStart(256, "0");
const nonceBitPos = BigInt(255 - nonceBitmapStr.lastIndexOf("0"));
const nonce = ((wordPos << 8n) + nonceBitPos).toString();
return nonce
}
generatePermit2Payload({
tokenAddress,
amount,
proxyAddress,
chainId,
deadline,
nonce
}: {
tokenAddress: Address,
amount: bigint,
proxyAddress: Address,
chainId: number,
deadline: bigint,
nonce: string
}): Permit2Payload {
const value: PermitTransferFrom = {
permitted: {
token: tokenAddress,
amount: amount.toString(),
},
spender: proxyAddress,
nonce,
deadline: deadline.toString(),
};
const domain = {
name: "Permit2",
chainId,
verifyingContract: PERMIT2_ADDRESS,
} as const;
return {
nonce: BigInt(nonce),
domain,
deadline,
value,
type: ApprovalType.PERMIT2
};
}
async generatePermit2Params(
tokenAddress: Address,
amount: bigint,
walletAddress: Address,
proxyAddress: Address,
chainId: number,
deadline: bigint
): Promise<SignedPermitParams> {
let wordPos = DEFAULT_NONCE_WORD_POS;
let bitmap = await this.getNonceBitmap(walletAddress, wordPos);
while (bitmap === BigInt(MaxUint256.toString())) {
wordPos = (wordPos + 1n) % BigInt(`0x1${"0".repeat(248 / 4)}`);
bitmap = await this.getNonceBitmap(walletAddress, wordPos);
}
const nonceBitmapStr = hexToBinary(bitmap.toString(16)).padStart(256, "0");
const nonceBitPos = BigInt(255 - nonceBitmapStr.lastIndexOf("0"));
const nonce = ((wordPos << 8n) + nonceBitPos).toString();
const value: PermitTransferFrom = {
permitted: {
token: tokenAddress,
amount: amount.toString(),
},
spender: proxyAddress,
nonce,
deadline: deadline.toString(),
};
const domain = {
name: "Permit2",
chainId,
verifyingContract: PERMIT2_ADDRESS,
} as const;
// TODO: move signing from service to wallet slice
const signature = await signTypedData({
domain,
types: PERMIT2_MESSAGE_TYPES,
value,
});
const sig = utils.splitSignature(signature);
return {
approvalType: 2,
nonce: BigInt(nonce),
deadline,
v: sig.v,
r: sig.r as Hash,
s: sig.s as Hash,
};
}
computeDomainSeparator(domain: EIP2612Domain) {
let params: string[] = [];
let types: ("bytes32" | "uint256" | "address")[] = ["bytes32"];
let args: ("bytes32" | "uint256" | "address")[] = [];
const EIP712TypeHash = keccak256(
toHex(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
)
);
const name = keccak256(toHex(domain.name));
const version = keccak256(toHex(domain.version));
const chainId = BigInt(domain.chainId.toFixed(0));
const verifyingContract = domain.verifyingContract;
return keccak256(
encodeAbiParameters(
parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"),
[EIP712TypeHash, name, version, chainId, verifyingContract]
)
);
}
generatePermit1Payload({
nonces,
tokenName,
domainSeparator,
chainId,
tokenAddress,
walletAddress,
proxyAddress,
amount,
deadline
}: {
nonces: bigint,
tokenName: string,
domainSeparator: Address,
chainId: number,
tokenAddress: Address,
walletAddress: Address,
proxyAddress: Address,
amount: bigint,
deadline: bigint
}): Permit1Payload {
const domain = {
name: tokenName,
version: "1",
chainId,
verifyingContract: tokenAddress,
};
for (const tryVersion of ["1", "2", "3"]) {
domain.version = tryVersion;
if (this.computeDomainSeparator(domain) === domainSeparator) {
break;
}
}
const value = {
owner: walletAddress,
spender: proxyAddress,
value: amount,
nonce: nonces,
deadline: deadline,
};
return {
type: ApprovalType.PERMIT,
domain,
value,
nonces,
}
}
async generatePermit1Params(
tokenAddress: Address,
amount: bigint,
walletAddress: Address,
proxyAddress: Address,
chainId: number,
deadline: bigint
): Promise<SignedPermitParams> {
const [nonces, tokenName, tokenDomainSeparator] = await Promise.all([
this.getNonces(walletAddress, tokenAddress),
this.getName(tokenAddress),
this.getDomainSeparator(tokenAddress),
]);
const domain = {
name: tokenName,
version: "1",
chainId,
verifyingContract: tokenAddress,
};
for (const tryVersion of ["1", "2", "3"]) {
domain.version = tryVersion;
if (this.computeDomainSeparator(domain) === tokenDomainSeparator) {
break;
}
}
const value = {
owner: walletAddress,
spender: proxyAddress,
value: amount,
nonce: nonces,
deadline: deadline,
};
// TODO: move signing from service to wallet slice
const signature = await signTypedData({
domain,
types: PERMIT_MESSAGE_TYPES,
value,
});
const sig = utils.splitSignature(signature);
return {
approvalType: 1,
nonce: nonces,
deadline,
v: sig.v,
r: sig.r as Hash,
s: sig.s as Hash,
};
}
public approveMax(token: Address, account: Address) {
return prepareWriteContract({
abi: abis["ERC20Permit"],
functionName: "approve",
address: token,
args: [account, ethers.constants.MaxUint256],
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment