Created
September 21, 2024 10:59
-
-
Save satanworker/73fc19a1f50a3e0600a40e381a21feee to your computer and use it in GitHub Desktop.
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
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