Skip to content

Commit 032749b

Browse files
authored
Merge pull request #15 from trustwallet/feat/erc20-permit-visualization-support
feat: add ERC20 permit for both legacy
2 parents ad46c68 + dd0dc43 commit 032749b

File tree

5 files changed

+187
-3
lines changed

5 files changed

+187
-3
lines changed

src/types/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,14 @@ export type Protocol<T> = {
5858
visualize: (message: T, domain: Domain) => Result;
5959
isCorrectDomain: (domain: Domain) => boolean;
6060
};
61+
62+
export type PermitMessage = {
63+
owner?: string;
64+
holder?: string;
65+
spender: string;
66+
nonce: string;
67+
value?: string;
68+
deadline?: string;
69+
expiry?: string;
70+
allowed?: boolean | string;
71+
};

src/utils/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export const isSameAddress = (address1: string, address2: string): boolean => {
1313
);
1414
};
1515

16+
export const MaxUint256 = BigInt(
17+
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
18+
);
19+
1620
export const abiCoder = new AbiCoder();
1721

1822
// export const decodeErrorMessage = (output: string): string => {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ASSET_TYPE, Domain, PermitMessage, Result } from "../../types";
2+
import { PROTOCOL_ID } from "..";
3+
import { MaxUint256 } from "../../utils";
4+
5+
export const visualize = (message: PermitMessage, domain: Domain): Result => {
6+
if (!isERC20Permit(message)) throw new Error("wrong ERC20 Permit message schema");
7+
const amount =
8+
message.value?.toString() ||
9+
(message.allowed?.toString() === "true" ? MaxUint256.toString() : "0");
10+
11+
return {
12+
protocol: PROTOCOL_ID.ERC20_PERMIT,
13+
assetIn: [],
14+
assetOut: [],
15+
approval: [
16+
{
17+
address: domain.verifyingContract,
18+
type: ASSET_TYPE.ERC20,
19+
amounts: [amount],
20+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21+
owner: message.owner || message.holder!, // isERC20Permit makes sure one of theme exist at least
22+
operator: message.spender,
23+
deadline: Number(message.deadline || message.expiry) * 1000,
24+
},
25+
],
26+
};
27+
};
28+
29+
/**
30+
* @dev a function that return if a message is an ERC2612 or pseudo-ERC2612 (DAI example)
31+
* This function handles ERC2612 Permit and DAI Permit
32+
* @see https://eips.ethereum.org/EIPS/eip-2612
33+
* @param message EIP-712 message
34+
* @returns Boolean
35+
*/
36+
export const isERC20Permit = (message: object): boolean => {
37+
if (Object.keys(message).length !== 5) return false;
38+
const hasOwnProperty = Object.prototype.hasOwnProperty.bind(message);
39+
return (
40+
(hasOwnProperty("owner") || hasOwnProperty("holder")) &&
41+
hasOwnProperty("spender") &&
42+
(hasOwnProperty("value") || hasOwnProperty("allowed")) &&
43+
hasOwnProperty("nonce") &&
44+
(hasOwnProperty("deadline") || hasOwnProperty("expiry"))
45+
);
46+
};
47+
48+
const erc20Permit = {
49+
visualize,
50+
isERC20Permit,
51+
};
52+
53+
export default erc20Permit;

src/visualizer/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { Domain, Result } from "../types";
1+
2+
import { Domain, PermitMessage, Result } from "../types";
3+
4+
import { SeaPortPayload } from "../types/seaport";
25
import { BlurIoOrder } from "../types/blur";
36
import { LooksrareMakerOrderWithEncodedParams } from "../types/looksrare";
4-
import { SeaPortPayload } from "../types/seaport";
7+
58
import blurIo from "./blur-io";
9+
import erc20Permit from "./erc20-permit";
610
import looksrare from "./looksrare";
711
import seaport from "./seaport";
812

913
export enum PROTOCOL_ID {
1014
OPENSEA_SEAPORT = "OPENSEA_SEAPORT",
1115
LOOKSRARE_EXCHANGE = "LOOKSRARE_EXCHANGE",
1216
BLUR_IO_MARKETPLACE = "BLUR_IO_MARKETPLACE",
17+
ERC20_PERMIT = "ERC20_PERMIT",
1318
}
1419

1520
export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => {
@@ -26,7 +31,10 @@ export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => {
2631
* @returns {Result} assets impact and message liveness
2732
* @throws {Error}
2833
*/
29-
export default async function visualize<T>(message: T, domain: Domain): Promise<Result> {
34+
export default async function visualize<T extends object>(
35+
message: T,
36+
domain: Domain
37+
): Promise<Result> {
3038
const protocolId = getProtocolId(domain);
3139

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

4250
default:
51+
if (erc20Permit.isERC20Permit(message)) {
52+
return erc20Permit.visualize(message as PermitMessage, domain);
53+
}
54+
4355
throw new Error("Unrecognized/Unsupported Protocol Domain");
4456
}
4557
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { PermitMessage } from "../../../src/types";
2+
import visualize from "../../../src/visualizer";
3+
import erc20Permit from "../../../src/visualizer/erc20-permit";
4+
5+
describe("ERC20 Permit", () => {
6+
const erc20DaiPermitMessage = {
7+
holder: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
8+
spender: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
9+
allowed: true,
10+
nonce: "1",
11+
expiry: "167960665",
12+
};
13+
14+
const ERC2612Message = {
15+
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
16+
spender: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
17+
value: "100000000000",
18+
nonce: "1",
19+
deadline: "167960665",
20+
};
21+
const erc20PermitDomain = {
22+
name: "Dai Stablecoin",
23+
version: "1.1",
24+
chainId: "1",
25+
verifyingContract: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
26+
};
27+
28+
it("should revert if erc20Permit module called with wrong message payload", () => {
29+
const deepCopiedMessage = JSON.parse(JSON.stringify(erc20DaiPermitMessage));
30+
delete deepCopiedMessage["expiry"];
31+
32+
expect(() => {
33+
erc20Permit.visualize(deepCopiedMessage, erc20PermitDomain);
34+
}).toThrowError("wrong ERC20 Permit message schema");
35+
});
36+
37+
it("should successfully visualize DAI approval", async () => {
38+
const result = await visualize<PermitMessage>(
39+
erc20DaiPermitMessage,
40+
erc20PermitDomain
41+
);
42+
43+
expect(result).toEqual({
44+
protocol: "ERC20_PERMIT",
45+
assetIn: [],
46+
assetOut: [],
47+
approval: [
48+
{
49+
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
50+
type: "ERC20",
51+
amounts: [
52+
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
53+
],
54+
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
55+
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
56+
deadline: 167960665000,
57+
},
58+
],
59+
});
60+
});
61+
62+
it("should successfully visualize DAI approval with zero amount", async () => {
63+
const result = await visualize<PermitMessage>(
64+
{ ...erc20DaiPermitMessage, allowed: false, nonce: "2" },
65+
erc20PermitDomain
66+
);
67+
68+
expect(result).toEqual({
69+
protocol: "ERC20_PERMIT",
70+
assetIn: [],
71+
assetOut: [],
72+
approval: [
73+
{
74+
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
75+
type: "ERC20",
76+
amounts: ["0"],
77+
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
78+
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
79+
deadline: 167960665000,
80+
},
81+
],
82+
});
83+
});
84+
85+
it("should successfully visualize ERC2612 approval", async () => {
86+
const result = await visualize<PermitMessage>(ERC2612Message, erc20PermitDomain);
87+
88+
expect(result).toEqual({
89+
protocol: "ERC20_PERMIT",
90+
assetIn: [],
91+
assetOut: [],
92+
approval: [
93+
{
94+
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
95+
type: "ERC20",
96+
amounts: ["100000000000"],
97+
owner: "0x36C1625E5Ee6FBFa9fadd4f75790275e5eaB7107",
98+
operator: "0x21C1625E5Ee6FBFa9fadd4f75790275e5eaB7009",
99+
deadline: 167960665000,
100+
},
101+
],
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)