tornado-contracts/scripts/testPermitTornado.ts

318 lines
8.1 KiB
TypeScript
Raw Normal View History

2024-10-22 14:37:10 +03:00
import path from 'path';
import { readFile } from 'fs/promises';
import { ethers } from 'hardhat';
import {
BaseContract,
formatEther,
MaxUint256,
parseEther,
Provider,
Signature,
Signer,
solidityPackedKeccak256,
TypedDataEncoder,
TypedDataField,
} from 'ethers';
import * as constants from './constants';
import {
ERC20,
ERC20Mock,
ERC20Mock__factory,
Permit2Mock__factory,
} from '../typechain-types';
const { permit2Address } = constants;
/**
* Deploy Deterministic Deployment Proxy and Permit2
*
* https://github.com/Arachnid/deterministic-deployment-proxy
* https://github.com/Uniswap/permit2
*/
async function deployPermit2() {
const [owner] = await ethers.getSigners();
const { provider } = owner;
const { deterministicProxy, deterministicSigner, deterministicSignerCost } = constants;
const deterministicSignedTx = await readFile(path.join(__dirname, './deterministicTx.txt'), { encoding: 'utf-8' });
// Send deployment cost for deterministicProxyDeploy contract
(await owner.sendTransaction({ to: deterministicSigner, value: parseEther(deterministicSignerCost) })).wait();
(await provider.broadcastTransaction(deterministicSignedTx)).wait();
console.log(`Deterministic Deployment Proxy code: ${await provider.getCode(deterministicProxy)}`);
// Send Permit2 tx
const data = await readFile(path.join(__dirname, './permit2tx.txt'), { encoding: 'utf-8' });
(await owner.sendTransaction({ to: deterministicProxy, data })).wait();
const permit2code = await provider.getCode(permit2Address);
console.log(`Permit2 code: ${permit2code ? permit2code.slice(0, 20) : ''}`);
}
interface PermitValue {
spender: string;
value: bigint;
nonce?: bigint;
deadline?: bigint;
}
async function getPermitSignature(Token: ERC20 | ERC20Mock, { spender, value, nonce, deadline }: PermitValue) {
const signer = Token.runner as (Signer & { address: string });
const provider = signer.provider as Provider;
const [name, { chainId }] = await Promise.all([
Token.name(),
provider.getNetwork(),
]);
const DOMAIN_SEPARATOR = {
name,
version: '1',
chainId,
verifyingContract: Token.target as string,
};
const PERMIT_TYPE = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
return Signature.from(await signer.signTypedData(DOMAIN_SEPARATOR, PERMIT_TYPE, {
owner: signer.address,
spender,
value,
nonce,
deadline: deadline || MaxUint256,
}));
}
/**
* From @uniswap/permit2-sdk ported for ethers.js v6
*/
interface Witness {
witnessTypeName: string;
witnessType: {
[key: string]: TypedDataField[]
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
witness: any;
}
function bytesToHex(bytes: Uint8Array) {
return (
'0x' +
Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
);
}
function bytesToBN(bytes: Uint8Array) {
return BigInt(bytesToHex(bytes));
}
function rBigInt(nbytes: number = 31) {
return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes)));
}
function rHex(nbytes: number = 31) {
return bytesToHex(crypto.getRandomValues(new Uint8Array(nbytes)));
}
async function getPermit2Signature(
Token: BaseContract,
{ spender, value: amount, nonce, deadline }: PermitValue,
witness?: Witness
) {
const signer = Token.runner as (Signer & { address: string });
const provider = signer.provider as Provider;
const domain = {
name: 'Permit2',
chainId: (await provider.getNetwork()).chainId,
verifyingContract: permit2Address,
};
const types: {
[key: string]: TypedDataField[]
} = !witness
? {
PermitTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
}
: {
PermitWitnessTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'witness', type: witness.witnessTypeName },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
...witness.witnessType,
};
const values: {
permitted: {
token: string;
amount: bigint;
};
spender: string;
nonce: bigint;
deadline: bigint;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
witness?: any;
} = {
permitted: {
token: Token.target as string,
amount,
},
spender,
// Sorted nonce are not required for Permit2
nonce: nonce || rBigInt(16),
deadline: deadline || MaxUint256,
};
if (witness) {
values.witness = witness.witness;
}
const hash = new TypedDataEncoder(types).hash(values);
const signature = Signature.from(await signer.signTypedData(domain, types, values));
return {
domain,
types,
values,
hash,
signature,
};
}
async function test() {
await deployPermit2();
const [owner, user] = await ethers.getSigners();
const DAI = await new ERC20Mock__factory(owner).deploy();
(await DAI.mint(owner.address, parseEther('1000000'))).wait();
// Test Permit
const { v, r, s } = await getPermitSignature(DAI, {
spender: user.address,
value: parseEther('1'),
nonce: 0n,
});
(await DAI.permit(owner.address, user.address, parseEther('1'), MaxUint256, v, r, s)).wait();
(await DAI.connect(user).transferFrom(owner.address, user.address, parseEther('1'))).wait();
console.log(`User balance: ${formatEther(await DAI.balanceOf(user.address))}`);
// Test Permit2
{
const Permit2Mock = await new Permit2Mock__factory(owner).deploy(parseEther('1'), DAI.target);
const {
values: {
nonce,
deadline,
},
signature: {
serialized,
},
} = await getPermit2Signature(DAI, {
spender: Permit2Mock.target as string,
value: parseEther('1'),
});
// Should approve Permit2 first
(await DAI.approve(permit2Address, MaxUint256)).wait();
(await Permit2Mock.permit2Test(owner.address, nonce, deadline, serialized)).wait();
console.log(`Permit2Mock balance: ${formatEther(await DAI.balanceOf(Permit2Mock.target))}`);
}
{
const Permit2Mock = await new Permit2Mock__factory(owner).deploy(parseEther('1'), DAI.target);
const commitments = [
rHex(32),
rHex(32),
];
const commitmentsHash = solidityPackedKeccak256(['bytes32[]'], [commitments]);
const {
values: {
nonce,
deadline,
},
signature: {
serialized,
},
} = await getPermit2Signature(
DAI,
{
spender: Permit2Mock.target as string,
value: BigInt(commitments.length) * parseEther('1'),
},
{
witnessTypeName: 'PermitCommitments',
witnessType: {
PermitCommitments: [
{ name: 'instance', type: 'address' },
{ name: 'commitmentsHash', type: 'bytes32' },
]
},
witness: {
instance: Permit2Mock.target,
commitmentsHash,
}
}
);
// Should approve Permit2 first
(await DAI.approve(permit2Address, MaxUint256)).wait();
(await Permit2Mock.permit2Commitments(owner.address, commitments, nonce, deadline, serialized)).wait();
console.log(`Permit2Mock balance: ${formatEther(await DAI.balanceOf(Permit2Mock.target))}`);
console.log(owner.address);
console.log(await Permit2Mock.verifiedCommitments(commitments[0]));
console.log(await Permit2Mock.verifiedCommitments(commitments[1]));
}
}
test();