Skip to content

Commit

Permalink
Merge pull request #15 from trustwallet/feat/erc20-permit-visualizati…
Browse files Browse the repository at this point in the history
…on-support

feat: add ERC20 permit for both legacy
  • Loading branch information
a6-dou authored Mar 24, 2023
2 parents ad46c68 + dd0dc43 commit 032749b
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 3 deletions.
11 changes: 11 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,14 @@ export type Protocol<T> = {
visualize: (message: T, domain: Domain) => Result;
isCorrectDomain: (domain: Domain) => boolean;
};

export type PermitMessage = {
owner?: string;
holder?: string;
spender: string;
nonce: string;
value?: string;
deadline?: string;
expiry?: string;
allowed?: boolean | string;
};
4 changes: 4 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export const isSameAddress = (address1: string, address2: string): boolean => {
);
};

export const MaxUint256 = BigInt(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
);

export const abiCoder = new AbiCoder();

// export const decodeErrorMessage = (output: string): string => {
Expand Down
53 changes: 53 additions & 0 deletions src/visualizer/erc20-permit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ASSET_TYPE, Domain, PermitMessage, Result } from "../../types";
import { PROTOCOL_ID } from "..";
import { MaxUint256 } from "../../utils";

export const visualize = (message: PermitMessage, domain: Domain): Result => {
if (!isERC20Permit(message)) throw new Error("wrong ERC20 Permit message schema");
const amount =
message.value?.toString() ||
(message.allowed?.toString() === "true" ? MaxUint256.toString() : "0");

return {
protocol: PROTOCOL_ID.ERC20_PERMIT,
assetIn: [],
assetOut: [],
approval: [
{
address: domain.verifyingContract,
type: ASSET_TYPE.ERC20,
amounts: [amount],
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
owner: message.owner || message.holder!, // isERC20Permit makes sure one of theme exist at least
operator: message.spender,
deadline: Number(message.deadline || message.expiry) * 1000,
},
],
};
};

/**
* @dev a function that return if a message is an ERC2612 or pseudo-ERC2612 (DAI example)
* This function handles ERC2612 Permit and DAI Permit
* @see https://eips.ethereum.org/EIPS/eip-2612
* @param message EIP-712 message
* @returns Boolean
*/
export const isERC20Permit = (message: object): boolean => {
if (Object.keys(message).length !== 5) return false;
const hasOwnProperty = Object.prototype.hasOwnProperty.bind(message);
return (
(hasOwnProperty("owner") || hasOwnProperty("holder")) &&
hasOwnProperty("spender") &&
(hasOwnProperty("value") || hasOwnProperty("allowed")) &&
hasOwnProperty("nonce") &&
(hasOwnProperty("deadline") || hasOwnProperty("expiry"))
);
};

const erc20Permit = {
visualize,
isERC20Permit,
};

export default erc20Permit;
18 changes: 15 additions & 3 deletions src/visualizer/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Domain, Result } from "../types";

import { Domain, PermitMessage, Result } from "../types";

import { SeaPortPayload } from "../types/seaport";
import { BlurIoOrder } from "../types/blur";
import { LooksrareMakerOrderWithEncodedParams } from "../types/looksrare";
import { SeaPortPayload } from "../types/seaport";

import blurIo from "./blur-io";
import erc20Permit from "./erc20-permit";
import looksrare from "./looksrare";
import seaport from "./seaport";

export enum PROTOCOL_ID {
OPENSEA_SEAPORT = "OPENSEA_SEAPORT",
LOOKSRARE_EXCHANGE = "LOOKSRARE_EXCHANGE",
BLUR_IO_MARKETPLACE = "BLUR_IO_MARKETPLACE",
ERC20_PERMIT = "ERC20_PERMIT",
}

export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => {
Expand All @@ -26,7 +31,10 @@ export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => {
* @returns {Result} assets impact and message liveness
* @throws {Error}
*/
export default async function visualize<T>(message: T, domain: Domain): Promise<Result> {
export default async function visualize<T extends object>(
message: T,
domain: Domain
): Promise<Result> {
const protocolId = getProtocolId(domain);

switch (protocolId) {
Expand All @@ -40,6 +48,10 @@ export default async function visualize<T>(message: T, domain: Domain): Promise<
return blurIo.visualize(message as BlurIoOrder, domain);

default:
if (erc20Permit.isERC20Permit(message)) {
return erc20Permit.visualize(message as PermitMessage, domain);
}

throw new Error("Unrecognized/Unsupported Protocol Domain");
}
}
104 changes: 104 additions & 0 deletions test/visualizer/erc20-permit/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { PermitMessage } from "../../../src/types";
import visualize from "../../../src/visualizer";
import erc20Permit from "../../../src/visualizer/erc20-permit";

describe("ERC20 Permit", () => {
const erc20DaiPermitMessage = {
holder: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
spender: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
allowed: true,
nonce: "1",
expiry: "167960665",
};

const ERC2612Message = {
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
spender: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
value: "100000000000",
nonce: "1",
deadline: "167960665",
};
const erc20PermitDomain = {
name: "Dai Stablecoin",
version: "1.1",
chainId: "1",
verifyingContract: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
};

it("should revert if erc20Permit module called with wrong message payload", () => {
const deepCopiedMessage = JSON.parse(JSON.stringify(erc20DaiPermitMessage));
delete deepCopiedMessage["expiry"];

expect(() => {
erc20Permit.visualize(deepCopiedMessage, erc20PermitDomain);
}).toThrowError("wrong ERC20 Permit message schema");
});

it("should successfully visualize DAI approval", async () => {
const result = await visualize<PermitMessage>(
erc20DaiPermitMessage,
erc20PermitDomain
);

expect(result).toEqual({
protocol: "ERC20_PERMIT",
assetIn: [],
assetOut: [],
approval: [
{
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
type: "ERC20",
amounts: [
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
],
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
deadline: 167960665000,
},
],
});
});

it("should successfully visualize DAI approval with zero amount", async () => {
const result = await visualize<PermitMessage>(
{ ...erc20DaiPermitMessage, allowed: false, nonce: "2" },
erc20PermitDomain
);

expect(result).toEqual({
protocol: "ERC20_PERMIT",
assetIn: [],
assetOut: [],
approval: [
{
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
type: "ERC20",
amounts: ["0"],
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
deadline: 167960665000,
},
],
});
});

it("should successfully visualize ERC2612 approval", async () => {
const result = await visualize<PermitMessage>(ERC2612Message, erc20PermitDomain);

expect(result).toEqual({
protocol: "ERC20_PERMIT",
assetIn: [],
assetOut: [],
approval: [
{
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
type: "ERC20",
amounts: ["100000000000"],
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
deadline: 167960665000,
},
],
});
});
});

0 comments on commit 032749b

Please sign in to comment.