Skip to content

Commit 47a2f32

Browse files
authored
feat(benchmarks): added fluidkey (#146)
1 parent 27ed418 commit 47a2f32

10 files changed

Lines changed: 423 additions & 59 deletions

File tree

gas-benchmarks/src/__tests__/constants.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ describe("constants.ts files", () => {
5757
});
5858
});
5959

60-
it("should have a URL to a .sol source file with line number for each events array indicating the function that emits the events", () => {
60+
it("should have a URL to a .sol source file with line reference for each events array indicating the function that emits the events", () => {
6161
files.forEach(({ filePath, events }) => {
6262
events.forEach(({ name, comment }) => {
6363
expect(
6464
hasSolUrlWithLineNumber(comment),
65-
`JSDoc for ${name} in ${filePath} must have a URL to a .sol file with a line number anchor (e.g., #L123)`,
65+
`JSDoc for ${name} in ${filePath} must have a URL to a .sol file with a line reference (e.g., #L123 or start=30)`,
6666
).toBe(true);
6767
});
6868
});

gas-benchmarks/src/__tests__/utils.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { join } from "node:path";
33

44
import type { AddressEntry, EventsEntry } from "./types.js";
55

6+
const SOL_WITH_HASH_LINE_REF = /https?:\/\/\S+\.sol#L\d+(?=\s|$)/i;
7+
const SOL_WITH_QUERY_LINE_REF = /https?:\/\/\S+\.sol\?\S*[?&]start=\d+(?=\s|$)/i;
8+
const ENCODED_SOL_WITH_QUERY_LINE_REF = /https?:\/\/\S+\?\S*\.sol\S*[?&]start=\d+(?=\s|$)/i;
9+
610
/**
711
* Recursively scan `srcDir` for `constants.ts` files, skipping excluded directories.
812
*/
@@ -81,9 +85,16 @@ export function hasSolUrl(comment: string): boolean {
8185
return /https?:\/\/\S+\.sol(?:#\S*)?(?=\s|$)/i.test(comment);
8286
}
8387

84-
/** Checks that a JSDoc comment contains a URL to a `.sol` file with a line number anchor (e.g., `#L123`). */
88+
/**
89+
* Checks that a JSDoc comment contains a URL to a `.sol` file with a line reference.
90+
* Accepts both hash anchors (e.g., `#L123`) and query params (e.g., `?file=...sol&start=30`).
91+
*/
8592
export function hasSolUrlWithLineNumber(comment: string): boolean {
86-
return /https?:\/\/\S+\.sol#L\d+(?=\s|$)/i.test(comment);
93+
return (
94+
SOL_WITH_HASH_LINE_REF.test(comment) ||
95+
SOL_WITH_QUERY_LINE_REF.test(comment) ||
96+
ENCODED_SOL_WITH_QUERY_LINE_REF.test(comment)
97+
);
8798
}
8899

89100
/** Extracts etherscan transaction URLs from a JSDoc comment. */
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { parseAbiItem, type Address } from "viem";
2+
3+
/**
4+
* USDC ERC20 token contract address used for benchmarking ERC20 shielding and transfers.
5+
* https://github.com/circlefin/stablecoin-evm/blob/master/contracts/v2/FiatTokenV2_2.sol
6+
*/
7+
export const USDC_ERC20_TOKEN_ADDRESS: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
8+
9+
/**
10+
* A normal ERC20 transfer to any address (including stealth addresses) could be a shield/deposit for Fluidkey
11+
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/9cfdccd35350f7bcc585cf2ede08cd04e7f0ec10/contracts/token/ERC20/IERC20.sol#L16
12+
*
13+
* Emits:
14+
* Transfer() - ERC20 token transfer to the stealth address (emitted by the token contract)
15+
*
16+
* Example:
17+
* https://etherscan.io/tx/0xc8a00a361491b878ec68a0f4f452aedc86eea316b0d6e4acee2f53e6719b4fb0
18+
*/
19+
export const SHIELD_ERC20_EVENTS = [
20+
parseAbiItem("event Transfer(address indexed from, address indexed to, uint256 value)"),
21+
] as const;
22+
23+
/**
24+
* Fluidkey relayer send txs to this relayer contract to deploy the Safe contract and execute the transfer to the recipient in a single transaction.
25+
* There is no open source code for the relayer contract (searched for relayer contracts in Safe and Gelato)
26+
* https://etherscan.io/address/0x8090a9db6aca56ffa186c75ca0787b18af1058a0
27+
*
28+
* A sample implementation of the relayer contract can be found here:
29+
* https://www.codeslaw.app/contracts/arbitrum/0x7f3319f55ef4a96ae717c5ac27b5adb0435a9280?file=src%2FSmartAccountRelayer.sol
30+
*/
31+
export const FLUIDKEY_RELAYER_CONTRACT: Address = "0x8090a9DB6Aca56fFA186C75Ca0787B18af1058a0";
32+
33+
/**
34+
* A "Relay Operation" transaction is executed when the user sends funds from the Fluidkey interface. As stated before, there is no open source code.
35+
* The transaction deploys the Safe Singleton 1.3.0 contract and sends funds to the public recipient in one bundled transaction.
36+
* An example of the SmartAccountRelayer.relayOperation function:
37+
* https://www.codeslaw.app/contracts/arbitrum/0x7f3319f55ef4a96ae717c5ac27b5adb0435a9280?file=src%2FSmartAccountRelayer.sol&start=30
38+
*
39+
* Emits:
40+
* EnabledModule() - Safe module enabled (emitted by the Safe stealth address)
41+
* ModuleInitialized() - Safe module initialized (emitted by the Safe)
42+
* ConfigHashChanged() - Safe config hash changed after module setup (emitted by the Safe)
43+
* SafeSetup() - Safe setup with 1 owner and threshold 1 (emitted by the Safe stealth address)
44+
* ProxyCreation() - New Safe proxy deployed at sender's stealth address (emitted by Safe: Proxy Factory 1.3.0)
45+
* ExecutionSuccess() - Safe stealth transfers fee to relayer (emitted by the Safe stealth address)
46+
* SafeReceived() - Relayer (Safe stealth address) receives the fee (emitted by the Safe stealth address)
47+
* ExecutionSuccess() - Safe stealth address executes the transfer to the recipient (emitted by the Safe stealth address)
48+
* OperationRelayed() - Operation executed successfully (emitted by the relayer contract)
49+
*
50+
* Example:
51+
* https://etherscan.io/tx/0x8a63395db9779ab66661653be4ffe2a15bd5df345d9389ec12f7bd44bb07f7d4
52+
*/
53+
export const TRANSFER_ETH_EVENTS = [
54+
parseAbiItem("event EnabledModule(address module)"),
55+
parseAbiItem("event ModuleInitialized(address indexed account)"),
56+
parseAbiItem("event ConfigHashChanged(address indexed account, uint256 oldConfigHash, uint256 newConfigHash)"),
57+
parseAbiItem(
58+
"event SafeSetup(address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler)",
59+
),
60+
parseAbiItem("event ProxyCreation(address proxy, address singleton)"),
61+
parseAbiItem("event ExecutionSuccess(bytes32 txHash, uint256 payment)"),
62+
parseAbiItem("event SafeReceived(address indexed sender, uint256 value)"),
63+
parseAbiItem("event ExecutionSuccess(bytes32 txHash, uint256 payment)"),
64+
parseAbiItem("event OperationRelayed(uint256 indexed operationId, bool indexed success)"),
65+
] as const;
66+
67+
/**
68+
* A "Relay Operation" transaction is executed when the user sends funds from the Fluidkey interface. As stated before, there is no open source code.
69+
* The transaction deploys the Safe Singleton 1.3.0 contract and sends funds to the public recipient in one bundled transaction.
70+
* An example of the SmartAccountRelayer.relayOperation function:
71+
* https://www.codeslaw.app/contracts/arbitrum/0x7f3319f55ef4a96ae717c5ac27b5adb0435a9280?file=src%2FSmartAccountRelayer.sol&start=30
72+
*
73+
* Emits:
74+
* EnabledModule() - Safe module enabled (emitted by the Safe stealth address)
75+
* ModuleInitialized() - Safe module initialized (emitted by the Safe)
76+
* ConfigHashChanged() - Safe config hash changed after module setup (emitted by the Safe)
77+
* SafeSetup() - Safe setup with 1 owner and threshold 1 (emitted by the Safe stealth address)
78+
* ProxyCreation() - New Safe proxy deployed at sender's stealth address (emitted by Safe: Proxy Factory 1.3.0)
79+
* Transfer() - ERC20 transfer to recipient (emitted by the ERC20 token contract)
80+
* ExecutionSuccess() - Safe stealth transfers fee to relayer (emitted by the Safe stealth address)
81+
* Transfer() - ERC20 fee transfer to relayer (emitted by the ERC20 token contract)
82+
* ExecutionSuccess() - Safe stealth address executes the transfer to the recipient (emitted by the Safe stealth address)
83+
* OperationRelayed() - Operation executed successfully (emitted by the relayer contract)
84+
*
85+
* Example:
86+
* https://etherscan.io/tx/0xf10db1f5474b8ef4592fa95abef49e73f53f5c2773297dade0d8e209176f7aec
87+
*/
88+
export const TRANSFER_ERC20_EVENTS = [
89+
parseAbiItem("event EnabledModule(address module)"),
90+
parseAbiItem("event ModuleInitialized(address indexed account)"),
91+
parseAbiItem("event ConfigHashChanged(address indexed account, uint256 oldConfigHash, uint256 newConfigHash)"),
92+
parseAbiItem(
93+
"event SafeSetup(address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler)",
94+
),
95+
parseAbiItem("event ProxyCreation(address proxy, address singleton)"),
96+
parseAbiItem("event Transfer(address indexed from, address indexed to, uint256 value)"),
97+
parseAbiItem("event ExecutionSuccess(bytes32 txHash, uint256 payment)"),
98+
parseAbiItem("event Transfer(address indexed from, address indexed to, uint256 value)"),
99+
parseAbiItem("event ExecutionSuccess(bytes32 txHash, uint256 payment)"),
100+
parseAbiItem("event OperationRelayed(uint256 indexed operationId, bool indexed success)"),
101+
] as const;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { mainnet } from "viem/chains";
2+
3+
import type { FeeMetrics } from "../utils/types.js";
4+
5+
import { BLOCK_WINDOW_ETHEREUM_5_WEEKS, BLOCK_WINDOW_ETHEREUM_10_MINUTES, MIN_SAMPLES } from "../utils/constants.js";
6+
import { getValidEthTransfers, getValidTransactions } from "../utils/rpc.js";
7+
import { getAverageMetrics } from "../utils/utils.js";
8+
9+
import {
10+
FLUIDKEY_RELAYER_CONTRACT,
11+
SHIELD_ERC20_EVENTS,
12+
TRANSFER_ERC20_EVENTS,
13+
TRANSFER_ETH_EVENTS,
14+
USDC_ERC20_TOKEN_ADDRESS,
15+
} from "./constants.js";
16+
17+
export class Fluidkey {
18+
readonly name = "fluidkey";
19+
20+
readonly version = "1.3.0";
21+
22+
async benchmark(): Promise<Record<string, FeeMetrics>> {
23+
const [shieldEth, shieldErc20, transferEth, transferErc20] = await Promise.all([
24+
this.benchmarkShieldETH(),
25+
this.benchmarkShieldERC20(),
26+
this.benchmarkTransferETH(),
27+
this.benchmarkTransferERC20(),
28+
]);
29+
30+
return { shieldEth, shieldErc20, transferEth, transferErc20 };
31+
}
32+
33+
async benchmarkShieldETH(): Promise<FeeMetrics> {
34+
const receipts = await getValidEthTransfers({
35+
chain: mainnet,
36+
blockWindow: BLOCK_WINDOW_ETHEREUM_10_MINUTES, // TODO: fetch native ETH txs using block window
37+
});
38+
39+
if (receipts.length < MIN_SAMPLES) {
40+
throw new Error(`${this.name} shield ETH: receipts (${receipts.length}) < MIN_SAMPLES (${MIN_SAMPLES})`);
41+
}
42+
43+
return getAverageMetrics(receipts);
44+
}
45+
46+
async benchmarkShieldERC20(): Promise<FeeMetrics> {
47+
const receipts = await getValidTransactions({
48+
contractAddress: USDC_ERC20_TOKEN_ADDRESS,
49+
events: SHIELD_ERC20_EVENTS,
50+
chain: mainnet,
51+
blockWindow: BLOCK_WINDOW_ETHEREUM_10_MINUTES, // there are a lot of ERC20 transfers so use a small block window to rate limit
52+
});
53+
54+
if (receipts.length < MIN_SAMPLES) {
55+
throw new Error(`${this.name} shield ERC20: receipts (${receipts.length}) < MIN_SAMPLES (${MIN_SAMPLES})`);
56+
}
57+
58+
return getAverageMetrics(receipts);
59+
}
60+
61+
async benchmarkTransferETH(): Promise<FeeMetrics> {
62+
const receipts = await getValidTransactions({
63+
contractAddress: FLUIDKEY_RELAYER_CONTRACT,
64+
events: TRANSFER_ETH_EVENTS,
65+
chain: mainnet,
66+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
67+
});
68+
69+
if (receipts.length < MIN_SAMPLES) {
70+
throw new Error(`${this.name} transfer ETH: receipts (${receipts.length}) < MIN_SAMPLES (${MIN_SAMPLES})`);
71+
}
72+
73+
return getAverageMetrics(receipts);
74+
}
75+
76+
async benchmarkTransferERC20(): Promise<FeeMetrics> {
77+
const receipts = await getValidTransactions({
78+
contractAddress: FLUIDKEY_RELAYER_CONTRACT,
79+
events: TRANSFER_ERC20_EVENTS,
80+
chain: mainnet,
81+
});
82+
83+
if (receipts.length < MIN_SAMPLES) {
84+
throw new Error(`${this.name} transfer ERC20: receipts (${receipts.length}) < MIN_SAMPLES (${MIN_SAMPLES})`);
85+
}
86+
87+
return getAverageMetrics(receipts);
88+
}
89+
}

gas-benchmarks/src/hinkal/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mainnet } from "viem/chains";
22

33
import type { FeeMetrics } from "../utils/types.js";
44

5-
import { BLOCK_WINDOW_ETHEREUM_4_WEEKS, MIN_SAMPLES } from "../utils/constants.js";
5+
import { BLOCK_WINDOW_ETHEREUM_5_WEEKS, MIN_SAMPLES } from "../utils/constants.js";
66
import { getValidTransactions } from "../utils/rpc.js";
77
import { getAverageMetrics } from "../utils/utils.js";
88

@@ -37,7 +37,7 @@ export class Hinkal {
3737
contractAddress: HINKAL_POOL,
3838
events: SHIELD_ETH_EVENTS,
3939
chain: mainnet,
40-
blockWindow: BLOCK_WINDOW_ETHEREUM_4_WEEKS,
40+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
4141
});
4242

4343
if (receipts.length < MIN_SAMPLES) {
@@ -52,7 +52,7 @@ export class Hinkal {
5252
contractAddress: HINKAL_POOL,
5353
events: UNSHIELD_ETH_EVENTS,
5454
chain: mainnet,
55-
blockWindow: BLOCK_WINDOW_ETHEREUM_4_WEEKS,
55+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
5656
});
5757

5858
if (receipts.length < MIN_SAMPLES) {
@@ -67,7 +67,7 @@ export class Hinkal {
6767
contractAddress: HINKAL_POOL,
6868
events: INTERNAL_TRANSFER_EVENTS,
6969
chain: mainnet,
70-
blockWindow: BLOCK_WINDOW_ETHEREUM_4_WEEKS,
70+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
7171
});
7272

7373
if (receipts.length < MIN_SAMPLES) {
@@ -82,7 +82,7 @@ export class Hinkal {
8282
contractAddress: HINKAL_POOL,
8383
events: SHIELD_ERC20_EVENTS,
8484
chain: mainnet,
85-
blockWindow: BLOCK_WINDOW_ETHEREUM_4_WEEKS,
85+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
8686
});
8787

8888
if (receipts.length < MIN_SAMPLES) {
@@ -97,7 +97,7 @@ export class Hinkal {
9797
contractAddress: HINKAL_POOL,
9898
events: UNSHIELD_ERC20_EVENTS,
9999
chain: mainnet,
100-
blockWindow: BLOCK_WINDOW_ETHEREUM_4_WEEKS,
100+
blockWindow: BLOCK_WINDOW_ETHEREUM_5_WEEKS,
101101
});
102102

103103
if (receipts.length < MIN_SAMPLES) {

gas-benchmarks/src/index.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Fluidkey } from "./fluidkey/index.js";
12
import { Hinkal } from "./hinkal/index.js";
23
import { Intmax } from "./intmax/index.js";
34
import { Monero } from "./monero/index.js";
@@ -12,20 +13,29 @@ const privacyPools = new PrivacyPools();
1213
const intmax = new Intmax();
1314
const monero = new Monero();
1415
const hinkal = new Hinkal();
16+
const fluidkey = new Fluidkey();
1517

1618
await db.read();
1719

1820
const start = Date.now();
1921

20-
const [railgunMetrics, tornadoCashMetrics, privacyPoolsMetrics, intmaxMetrics, moneroMetrics, hinkalMetrics] =
21-
await Promise.all([
22-
railgun.benchmark(),
23-
tornadoCash.benchmark(),
24-
privacyPools.benchmark(),
25-
intmax.benchmark(),
26-
monero.benchmark(),
27-
hinkal.benchmark(),
28-
]);
22+
const [
23+
railgunMetrics,
24+
tornadoCashMetrics,
25+
privacyPoolsMetrics,
26+
intmaxMetrics,
27+
moneroMetrics,
28+
hinkalMetrics,
29+
fluidkeyMetrics,
30+
] = await Promise.all([
31+
railgun.benchmark(),
32+
tornadoCash.benchmark(),
33+
privacyPools.benchmark(),
34+
intmax.benchmark(),
35+
monero.benchmark(),
36+
hinkal.benchmark(),
37+
fluidkey.benchmark(),
38+
]);
2939

3040
await db.update((data) => {
3141
// eslint-disable-next-line no-param-reassign
@@ -67,6 +77,14 @@ await db.update((data) => {
6777
unshield_erc20: hinkalMetrics.unshieldErc20,
6878
transfer_erc20: hinkalMetrics.internalTransfer,
6979
};
80+
81+
// eslint-disable-next-line no-param-reassign
82+
data[`${fluidkey.name}_${fluidkey.version}`] = {
83+
shield_eth: fluidkeyMetrics.shieldEth,
84+
shield_erc20: fluidkeyMetrics.shieldErc20,
85+
transfer_eth: fluidkeyMetrics.transferEth,
86+
transfer_erc20: fluidkeyMetrics.transferErc20,
87+
};
7088
});
7189

7290
const end = Date.now();

gas-benchmarks/src/utils/constants.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export const BLOCK_RANGE = 2_000n;
2828

2929
export const BENCHMARKS_OUTPUT_PATH = "./benchmarks.json";
3030

31+
/**
32+
* The number of Ethereum blocks to scan for events for 10 minutes
33+
* 10 minutes = 600 seconds. 1 Ethereum block = 12 seconds
34+
* 600 / 12 = 50 blocks
35+
*/
36+
export const BLOCK_WINDOW_ETHEREUM_10_MINUTES = 50n;
37+
3138
/**
3239
* The number of Ethereum blocks to scan for events for 1 week
3340
* 1 week = 604800 seconds. 1 Ethereum block = 12 seconds
@@ -36,11 +43,11 @@ export const BENCHMARKS_OUTPUT_PATH = "./benchmarks.json";
3643
export const BLOCK_WINDOW_ETHEREUM_1_WEEK = 50_400n;
3744

3845
/**
39-
* The number of Ethereum blocks to scan for events for 4 weeks
40-
* 4 weeks = 2419200 seconds. 1 Ethereum block = 12 seconds
41-
* 2419200 / 12 = 201600 blocks
46+
* The number of Ethereum blocks to scan for events for 5 weeks
47+
* 5 weeks = 3024000 seconds. 1 Ethereum block = 12 seconds
48+
* 3024000 / 12 = 252000 blocks
4249
*/
43-
export const BLOCK_WINDOW_ETHEREUM_4_WEEKS = 201_600n;
50+
export const BLOCK_WINDOW_ETHEREUM_5_WEEKS = 252_000n;
4451

4552
/**
4653
* The number of Scroll blocks to scan for events for 1 week

0 commit comments

Comments
 (0)