mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-13 07:47:56 -05:00
Compare commits
272 Commits
auditIssue
...
tx-data-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac01049d7 | ||
|
|
8dce0947b0 | ||
|
|
f87cc7e6cb | ||
|
|
77fb5f1263 | ||
|
|
7aa4ffe10b | ||
|
|
e409094050 | ||
|
|
5a32f14ff6 | ||
|
|
0f5a0656e2 | ||
|
|
04359f7518 | ||
|
|
9b4d1c5401 | ||
|
|
1ce28b9c2c | ||
|
|
54227a57b0 | ||
|
|
f9ce7be5b5 | ||
|
|
2a20bfeb8d | ||
|
|
275d593b5c | ||
|
|
00d948376a | ||
|
|
0e51ecb5fe | ||
|
|
576e778855 | ||
|
|
4d170f73dd | ||
|
|
cd010324a5 | ||
|
|
29542e4c98 | ||
|
|
1ff60d5dd1 | ||
|
|
ebf415c573 | ||
|
|
b296a01a80 | ||
|
|
0e7e42154b | ||
|
|
0940333e3c | ||
|
|
831632ce8a | ||
|
|
a58dcdaee7 | ||
|
|
162072155d | ||
|
|
e675062a53 | ||
|
|
95a107f6b8 | ||
|
|
a6f844b822 | ||
|
|
d7f1917ed7 | ||
|
|
8edaab3a71 | ||
|
|
c5d83f4650 | ||
|
|
ddb8f72864 | ||
|
|
32c9c6c7ed | ||
|
|
48e472389d | ||
|
|
72c7b7c3c5 | ||
|
|
9c5c3ba950 | ||
|
|
d7369af0cf | ||
|
|
fc5a668ed6 | ||
|
|
40a3335a38 | ||
|
|
9b2ab4bb48 | ||
|
|
a609cb1f8e | ||
|
|
553d9ba81d | ||
|
|
ddbe420f53 | ||
|
|
fbab084b52 | ||
|
|
fff8dd397f | ||
|
|
926637130a | ||
|
|
0047f14a0f | ||
|
|
0d52ddb20f | ||
|
|
ffd7037573 | ||
|
|
daa51bce52 | ||
|
|
69fa24daff | ||
|
|
01a908e26f | ||
|
|
8c91d2faa3 | ||
|
|
b2ab3b766d | ||
|
|
c2afe790cf | ||
|
|
c736179e75 | ||
|
|
dc488ecc00 | ||
|
|
3032cf0917 | ||
|
|
e29c18c63f | ||
|
|
e42ccf2f1f | ||
|
|
ef8f2c7a13 | ||
|
|
0da9350fd6 | ||
|
|
5cacd017d1 | ||
|
|
02c8565e98 | ||
|
|
52d49e16d8 | ||
|
|
b84aebfaa9 | ||
|
|
bdca7833d3 | ||
|
|
56fa5b6e82 | ||
|
|
2cf961ff13 | ||
|
|
9e8269b841 | ||
|
|
1139b4e4f1 | ||
|
|
f5026347e1 | ||
|
|
638e1a1fb2 | ||
|
|
fb5537a118 | ||
|
|
4eeb9afb25 | ||
|
|
1e86c15892 | ||
|
|
f8a8c490e8 | ||
|
|
28f3983fb2 | ||
|
|
b653eaf5ab | ||
|
|
84a89cb5b6 | ||
|
|
9922c3a79c | ||
|
|
f4fdf7148d | ||
|
|
51f4a9588a | ||
|
|
0b1f037ba7 | ||
|
|
000126d226 | ||
|
|
d401e745cf | ||
|
|
21e0532fc0 | ||
|
|
6d1f96ab2e | ||
|
|
008dcf0b09 | ||
|
|
1eed20fa6e | ||
|
|
126d82266b | ||
|
|
ff1f253012 | ||
|
|
c34db600cd | ||
|
|
a37ae25990 | ||
|
|
b48dc20db4 | ||
|
|
4d20166d6f | ||
|
|
7a63b3aa4d | ||
|
|
bc0d57007f | ||
|
|
28226f71e5 | ||
|
|
5ad1314efc | ||
|
|
10c7d54d12 | ||
|
|
0cdc6430e4 | ||
|
|
e1906cdbb7 | ||
|
|
a3b4877c11 | ||
|
|
b7ac4fd77c | ||
|
|
c88b05d0ec | ||
|
|
f6ab5d93ed | ||
|
|
c07dc63896 | ||
|
|
70ca00f089 | ||
|
|
6156b86b22 | ||
|
|
639f6133bf | ||
|
|
1ec0330adb | ||
|
|
cb5932776c | ||
|
|
115907c74f | ||
|
|
5529e078d9 | ||
|
|
4fd593ac1d | ||
|
|
ff27bd0469 | ||
|
|
cac4669cb9 | ||
|
|
5fe9170c3e | ||
|
|
ad9350eb68 | ||
|
|
513df2229e | ||
|
|
f08fa1e9ff | ||
|
|
aa8cd1d681 | ||
|
|
e6326835bb | ||
|
|
cb216fa7d7 | ||
|
|
7978ed0690 | ||
|
|
d86cf09716 | ||
|
|
dc6ebc24d6 | ||
|
|
7b07df3aba | ||
|
|
2aa7e352f5 | ||
|
|
de12b3c62f | ||
|
|
e1248c6b63 | ||
|
|
b988fbc92c | ||
|
|
4c9c0ed898 | ||
|
|
fcbfd7cc62 | ||
|
|
13f34dd02d | ||
|
|
b565c33193 | ||
|
|
c1404502f3 | ||
|
|
74f9d9020c | ||
|
|
4da348d9e2 | ||
|
|
51d7681626 | ||
|
|
7c3fbd4d40 | ||
|
|
fdf80fc5fb | ||
|
|
b0ba263eb4 | ||
|
|
b04281cdac | ||
|
|
e196a101ff | ||
|
|
995ec24d1f | ||
|
|
96c61e9932 | ||
|
|
8b76734316 | ||
|
|
519e6f88c4 | ||
|
|
7f803bd10a | ||
|
|
1e71ef3c78 | ||
|
|
708a6d0c2d | ||
|
|
42ea205f79 | ||
|
|
52974e01f6 | ||
|
|
13837e6729 | ||
|
|
7a911a3634 | ||
|
|
4ac580d1df | ||
|
|
30b16185fb | ||
|
|
a9e511c3f0 | ||
|
|
1468c91dc0 | ||
|
|
5ce0943f00 | ||
|
|
8ccad86fdf | ||
|
|
bc0c272d65 | ||
|
|
3a0a1e5f41 | ||
|
|
6de210b81b | ||
|
|
7239d51a57 | ||
|
|
5ce1723ed5 | ||
|
|
547bb7f34d | ||
|
|
059b1d4b1a | ||
|
|
a01d7426ea | ||
|
|
80d551d14e | ||
|
|
e2d8399dbf | ||
|
|
7522fb655f | ||
|
|
fa489ab75f | ||
|
|
41ac18fb86 | ||
|
|
c390ac0321 | ||
|
|
2adb7ace4c | ||
|
|
f90e79d306 | ||
|
|
f3b5552fb2 | ||
|
|
4130335f5a | ||
|
|
43cf586131 | ||
|
|
f4c2e21c9b | ||
|
|
f8223693fa | ||
|
|
e35b160e87 | ||
|
|
cd570155da | ||
|
|
7a9f26d218 | ||
|
|
13779ffe3b | ||
|
|
699f84cede | ||
|
|
e858c3c826 | ||
|
|
4052997b15 | ||
|
|
c260e60443 | ||
|
|
480ee1d3d0 | ||
|
|
25c73d6697 | ||
|
|
1052cc6c28 | ||
|
|
af420d9a7b | ||
|
|
fde1c61e93 | ||
|
|
8bc62681f6 | ||
|
|
bac557a113 | ||
|
|
1464476cec | ||
|
|
306b90e83d | ||
|
|
d7348e581d | ||
|
|
5b529facd1 | ||
|
|
4d7a83705b | ||
|
|
8e9cd18934 | ||
|
|
87eb27053a | ||
|
|
dd1a19b8d8 | ||
|
|
a9bc94c641 | ||
|
|
e416c13635 | ||
|
|
2b57673de4 | ||
|
|
edae8c17f0 | ||
|
|
e1e654eac5 | ||
|
|
9202cee106 | ||
|
|
1d887e696e | ||
|
|
51e6666776 | ||
|
|
74d31d561f | ||
|
|
a666a2b9c9 | ||
|
|
d2da8d8e7e | ||
|
|
1018131c27 | ||
|
|
e1a306f9f2 | ||
|
|
847bbddd25 | ||
|
|
a4aaa42964 | ||
|
|
07da3694f2 | ||
|
|
a968e86a20 | ||
|
|
c5e3461d60 | ||
|
|
81c3d1c965 | ||
|
|
30ddc6f8ab | ||
|
|
0ca6f4f8fa | ||
|
|
365084ed86 | ||
|
|
2daff8b542 | ||
|
|
027f899fe3 | ||
|
|
9f76ceb9f8 | ||
|
|
a348962e42 | ||
|
|
3894fe68a2 | ||
|
|
8972ccbc5c | ||
|
|
4b42d2d960 | ||
|
|
16a043279b | ||
|
|
59473044a0 | ||
|
|
a800935f51 | ||
|
|
a54b677720 | ||
|
|
2d6ee184a6 | ||
|
|
0b2cca4816 | ||
|
|
e4cde3496d | ||
|
|
29d8884754 | ||
|
|
28ebbe62f4 | ||
|
|
b4b757ce8b | ||
|
|
470ac78044 | ||
|
|
cf16d8f833 | ||
|
|
1e666325ba | ||
|
|
78646e8d11 | ||
|
|
ae7886aba3 | ||
|
|
86fc7c5895 | ||
|
|
0afefe9c1e | ||
|
|
71a3520a0b | ||
|
|
b97c8b5e6d | ||
|
|
73967cef4e | ||
|
|
3506fb6117 | ||
|
|
5918b12a75 | ||
|
|
fef73de225 | ||
|
|
321a2556b1 | ||
|
|
03d61443e8 | ||
|
|
77d9ff8823 | ||
|
|
f0cfe0d941 | ||
|
|
711f898d96 | ||
|
|
7def3c9b2e | ||
|
|
139d8e04e4 | ||
|
|
59388de662 | ||
|
|
1928d59c58 |
@@ -8,10 +8,6 @@ runs:
|
||||
shell: bash
|
||||
run: yarn hardhat node &
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat fundDeployer --network gethDev
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
|
||||
2
.github/workflows/aggregator.yml
vendored
2
.github/workflows/aggregator.yml
vendored
@@ -73,6 +73,8 @@ jobs:
|
||||
run: yarn start &
|
||||
- working-directory: ./contracts
|
||||
run: ./scripts/wait-for-rpc.sh
|
||||
- working-directory: ./contracts
|
||||
run: ./scripts/wait-for-contract-deploy.sh
|
||||
|
||||
- run: cp .env.local.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "0.8.2-1452ef5",
|
||||
"bls-wallet-clients": "0.9.0-2a20bfe",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -9,6 +9,7 @@ const BundleDto = io.type({
|
||||
),
|
||||
operations: io.array(io.type({
|
||||
nonce: io.string,
|
||||
gas: io.string,
|
||||
actions: io.array(io.type({
|
||||
ethValue: io.string,
|
||||
contractAddress: io.string,
|
||||
|
||||
@@ -887,10 +887,10 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@0.8.2-1452ef5:
|
||||
version "0.8.2-1452ef5"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
|
||||
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
|
||||
bls-wallet-clients@0.9.0-2a20bfe:
|
||||
version "0.9.0-2a20bfe"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-2a20bfe.tgz#2e39757a18df3ba78d816ae15f6b88000443a2a6"
|
||||
integrity sha512-w4efcArPBEowrAkIdVYc2mOLlkN8E5O9eIqEhoo6IrRVrN21p/JVNdoot4N3o5MAKFbeaYfid/u9lL6p2DNdiw==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
64120
aggregator/data/blocksSample.json
Normal file
64120
aggregator/data/blocksSample.json
Normal file
File diff suppressed because one or more lines are too long
@@ -41,6 +41,7 @@ export const keccak256 = ethers.utils.keccak256;
|
||||
export const shuffled: <T>(array: T[]) => T[] = ethers.utils.shuffled;
|
||||
|
||||
export type {
|
||||
ActionData,
|
||||
AggregatorUtilities,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
@@ -53,23 +54,29 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletWrapper,
|
||||
BundleCompressor,
|
||||
ContractsConnector,
|
||||
decodeError,
|
||||
ERC20__factory,
|
||||
Erc20Compressor,
|
||||
ERC20Factory,
|
||||
FallbackCompressor,
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
MockERC20Factory,
|
||||
VerificationGatewayFactory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
export * as sqlite from "https://deno.land/x/sqlite@v3.7.0/mod.ts";
|
||||
export { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
|
||||
export { mapValues, once } from "npm:@s-libs/micro-dash@15.2.0";
|
||||
|
||||
@@ -9,7 +9,7 @@ const client = new AggregatorClient(env.ORIGIN);
|
||||
const fx = await Fixture.create(import.meta.url);
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -17,6 +17,7 @@ const bundle: Bundle = {
|
||||
senderPublicKeys: [[dummyHex(32), dummyHex(32), dummyHex(32), dummyHex(32)]],
|
||||
operations: [{
|
||||
nonce: BigNumber.from(0),
|
||||
gas: BigNumber.from(0),
|
||||
actions: [{
|
||||
ethValue: BigNumber.from(0),
|
||||
contractAddress: dummyHex(20),
|
||||
|
||||
49
aggregator/manualTests/analysis/ByteStream.ts
Normal file
49
aggregator/manualTests/analysis/ByteStream.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
|
||||
export default class ByteStream {
|
||||
pos = 2;
|
||||
|
||||
constructor(public data: string) {
|
||||
assert(/^0x[0-9a-fA-F]*/.test(data));
|
||||
assert(data.length % 2 === 0);
|
||||
}
|
||||
|
||||
getN(len: number): string {
|
||||
const newPos = this.pos + 2 * len;
|
||||
assert(newPos <= this.data.length);
|
||||
|
||||
const res = `0x${this.data.slice(this.pos, newPos)}`;
|
||||
this.pos = newPos;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
peekN(len: number) {
|
||||
const res = this.getN(len);
|
||||
this.pos -= 2 * len;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
get(): number {
|
||||
return parseInt(this.getN(1).slice(2), 16);
|
||||
}
|
||||
|
||||
peek(): number {
|
||||
const res = this.get();
|
||||
this.pos -= 2;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getTail(): string {
|
||||
const res = `0x${this.data.slice(this.pos)}`;
|
||||
this.pos = this.data.length;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bytesRemaining() {
|
||||
return (this.data.length - this.pos) / 2;
|
||||
}
|
||||
}
|
||||
125
aggregator/manualTests/analysis/Calculator.ts
Normal file
125
aggregator/manualTests/analysis/Calculator.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { mapValues, once } from "../../deps.ts";
|
||||
|
||||
import blocks from "../../data/blocksSample.json" assert { type: "json" };
|
||||
import { sum } from "./util.ts";
|
||||
import MultiEncoder from "./MultiEncoder.ts";
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
|
||||
export default class Calculator {
|
||||
constructor(
|
||||
public multiEncoder: MultiEncoder,
|
||||
) {}
|
||||
|
||||
transactions = once(() => blocks.map((b) => b.transactions).flat());
|
||||
transactionData = once(() => this.transactions().map((tx) => tx.input));
|
||||
|
||||
encodedTransactionData = once(() =>
|
||||
this.transactionData().map((data) => this.multiEncoder.encode(data))
|
||||
);
|
||||
|
||||
decodedTransactionData = once(() =>
|
||||
this.encodedTransactionData().map(
|
||||
(input) => this.multiEncoder.decode(input),
|
||||
)
|
||||
);
|
||||
|
||||
checkDecodedTransactionData = once(() => {
|
||||
const transactionData = this.transactions().map((tx) => tx.input);
|
||||
const decodedTransactionData = this.decodedTransactionData();
|
||||
|
||||
const len = transactionData.length;
|
||||
assert(decodedTransactionData.length === len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
assert(
|
||||
transactionData[i] === decodedTransactionData[i],
|
||||
`tx ${i}: ${transactionData[i]} !== ${decodedTransactionData[i]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
txDataByMethodId = once(() => {
|
||||
const txDataByMethodId: Record<string, string[]> = {};
|
||||
|
||||
for (const data of this.transactionData()) {
|
||||
txDataByMethodId[data.slice(0, 10)] ??= [];
|
||||
txDataByMethodId[data.slice(0, 10)].push("0x" + data.slice(10));
|
||||
}
|
||||
|
||||
return txDataByMethodId;
|
||||
});
|
||||
|
||||
txDataStatsByMethodId = once(() =>
|
||||
mapValues(
|
||||
this.txDataByMethodId(),
|
||||
(dataArray, methodId) => {
|
||||
const count = dataArray.length;
|
||||
|
||||
const baselineLen = dataArray
|
||||
.map((data) => 1 + (methodId.length / 2 - 1) + (data.length / 2 - 1))
|
||||
.reduce(sum);
|
||||
|
||||
const avgBaselineLen = baselineLen / count;
|
||||
|
||||
const encodedLen = dataArray
|
||||
.map((data) =>
|
||||
this.multiEncoder.encode(methodId + data.slice(2)).length / 2 - 1
|
||||
)
|
||||
.reduce(sum);
|
||||
|
||||
const avgEncodedLen = encodedLen / count;
|
||||
|
||||
return {
|
||||
count,
|
||||
baselineLen,
|
||||
avgBaselineLen,
|
||||
encodedLen,
|
||||
avgEncodedLen,
|
||||
};
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
popularMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].count - a[1].count
|
||||
);
|
||||
});
|
||||
|
||||
biggestMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].baselineLen -
|
||||
a[1].baselineLen
|
||||
);
|
||||
});
|
||||
|
||||
biggestEncodedMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].encodedLen -
|
||||
a[1].encodedLen
|
||||
);
|
||||
});
|
||||
|
||||
totalLength = once(() =>
|
||||
this.transactions().map((t) => t.input.length / 2 - 1).reduce(sum)
|
||||
);
|
||||
|
||||
baselineEncodedLength = once(() => {
|
||||
let len = this.totalLength();
|
||||
|
||||
for (const txData of this.transactionData()) {
|
||||
len += VLQ.encode(txData.length / 2 - 1).length / 2 - 1;
|
||||
}
|
||||
|
||||
return len;
|
||||
});
|
||||
|
||||
totalEncodedLength = once(() =>
|
||||
this.encodedTransactionData().map((data) => data.length / 2 - 1).reduce(sum)
|
||||
);
|
||||
|
||||
compressionRatio = once(() =>
|
||||
this.totalEncodedLength() / this.baselineEncodedLength()
|
||||
);
|
||||
}
|
||||
61
aggregator/manualTests/analysis/MultiEncoder.ts
Normal file
61
aggregator/manualTests/analysis/MultiEncoder.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
import nil from "../../src/helpers/nil.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
export type Encoder = {
|
||||
encode(data: string): string | nil;
|
||||
decode(encodedData: string): string;
|
||||
};
|
||||
|
||||
export default class MultiEncoder {
|
||||
encoders: {
|
||||
id: number;
|
||||
encoder: Encoder;
|
||||
}[] = [];
|
||||
|
||||
encodersById: Record<number, Encoder> = {};
|
||||
|
||||
register(id: number, encoder: Encoder) {
|
||||
assert(id !== 0);
|
||||
this.encoders.push({ id, encoder });
|
||||
this.encodersById[id] = encoder;
|
||||
}
|
||||
|
||||
encode(data: string): string {
|
||||
for (const { id, encoder } of this.encoders) {
|
||||
const encoded = encoder.encode(data);
|
||||
|
||||
if (encoded === nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(id),
|
||||
encoded,
|
||||
]);
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(0),
|
||||
VLQ.encode(data.length / 2 - 1),
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
decode(data: string): string {
|
||||
const stream = new ByteStream(data);
|
||||
const id = VLQ.decode(stream);
|
||||
|
||||
if (id.eq(0)) {
|
||||
const len = VLQ.decode(stream);
|
||||
return stream.getN(len.toNumber());
|
||||
}
|
||||
|
||||
const encoder = this.encodersById[id.toNumber()];
|
||||
assert(encoder !== nil);
|
||||
|
||||
return encoder.decode(stream.getTail());
|
||||
}
|
||||
}
|
||||
50
aggregator/manualTests/analysis/PseudoFloat.ts
Normal file
50
aggregator/manualTests/analysis/PseudoFloat.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace PseudoFloat {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
if (x.eq(0)) {
|
||||
return "0x00";
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
|
||||
while (x.mod(10).eq(0) && exponent < 30) {
|
||||
x = x.div(10);
|
||||
exponent++;
|
||||
}
|
||||
|
||||
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
|
||||
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
|
||||
|
||||
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return hexJoin([`0x${firstByte}`, VLQ.encode(x.div(8))]);
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
const firstByte = stream.get();
|
||||
|
||||
if (firstByte == 0) {
|
||||
return BigNumber.from(0);
|
||||
}
|
||||
|
||||
const exponent = ((firstByte & 0xf8) >> 3) - 1;
|
||||
|
||||
let mantissa = VLQ.decode(stream);
|
||||
|
||||
mantissa = mantissa.shl(3);
|
||||
mantissa = mantissa.add(firstByte & 0x07);
|
||||
|
||||
return mantissa.mul(BigNumber.from(10).pow(exponent));
|
||||
}
|
||||
}
|
||||
|
||||
export default PseudoFloat;
|
||||
28
aggregator/manualTests/analysis/RegIndex.ts
Normal file
28
aggregator/manualTests/analysis/RegIndex.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace RegIndex {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const vlqValue = x.div(0x10000);
|
||||
const remainder = x.mod(0x10000);
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(vlqValue),
|
||||
remainder.toNumber().toString(16).padStart(4, "0"),
|
||||
]);
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
const vlqValue = VLQ.decode(stream);
|
||||
const remainder = parseInt(stream.getTail().slice(2), 16);
|
||||
|
||||
return vlqValue.mul(0x10000).add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
export default RegIndex;
|
||||
57
aggregator/manualTests/analysis/VLQ.ts
Normal file
57
aggregator/manualTests/analysis/VLQ.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace VLQ {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
while (true) {
|
||||
const segment = x.mod(128);
|
||||
segments.unshift(segment.toNumber());
|
||||
x = x.sub(segment);
|
||||
x = x.div(128);
|
||||
|
||||
if (x.eq(0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = "0x";
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const keepGoing = i !== segments.length - 1;
|
||||
|
||||
const byte = (keepGoing ? 128 : 0) + segments[i];
|
||||
result += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
let value = BigNumber.from(0);
|
||||
|
||||
while (true) {
|
||||
const currentByte = stream.get();
|
||||
|
||||
// Add the lowest 7 bits to the value
|
||||
value = value.add(currentByte & 0x7f);
|
||||
|
||||
// If the highest bit is zero, stop
|
||||
if ((currentByte & 0x80) === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We're continuing. Shift the value 7 bits to the left (higher) to
|
||||
// make room.
|
||||
value = value.shl(7);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export default VLQ;
|
||||
@@ -0,0 +1,30 @@
|
||||
import nil from "../../../src/helpers/nil.ts";
|
||||
import ByteStream from "../ByteStream.ts";
|
||||
import { Encoder } from "../MultiEncoder.ts";
|
||||
import PseudoFloat from "../PseudoFloat.ts";
|
||||
import { bigNumberToWord, hexJoin } from "../util.ts";
|
||||
|
||||
export default class ERC20TransferEncoder implements Encoder {
|
||||
encode(data: string): string | nil {
|
||||
const stream = new ByteStream(data);
|
||||
|
||||
if (stream.bytesRemaining() !== 68 || stream.getN(4) !== "0xa9059cbb") {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
"0x" + stream.getN(32).slice(26),
|
||||
PseudoFloat.encode(stream.getN(32)),
|
||||
]);
|
||||
}
|
||||
|
||||
decode(encodedData: string): string {
|
||||
const stream = new ByteStream(encodedData);
|
||||
|
||||
return hexJoin([
|
||||
"0xa9059cbb",
|
||||
"0x000000000000000000000000" + stream.getN(20).slice(2),
|
||||
bigNumberToWord(PseudoFloat.decode(stream)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
104
aggregator/manualTests/analysis/encoders/FallbackEncoder.ts
Normal file
104
aggregator/manualTests/analysis/encoders/FallbackEncoder.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import assert from "../../../src/helpers/assert.ts";
|
||||
import nil from "../../../src/helpers/nil.ts";
|
||||
import ByteStream from "../ByteStream.ts";
|
||||
import { Encoder } from "../MultiEncoder.ts";
|
||||
import PseudoFloat from "../PseudoFloat.ts";
|
||||
import VLQ from "../VLQ.ts";
|
||||
import { bigNumberToWord, getDataWords, hexJoin } from "../util.ts";
|
||||
|
||||
export default class FallbackEncoder implements Encoder {
|
||||
encode(data: string): string {
|
||||
const len = data.length / 2 - 1;
|
||||
|
||||
if ((data.length / 2 - 1) % 32 !== 4) {
|
||||
return hexJoin([
|
||||
VLQ.encode(2 * len),
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
const res: string[] = [];
|
||||
|
||||
const words = getDataWords(`0x${data.slice(10)}`);
|
||||
res.push(VLQ.encode(2 * words.length + 1));
|
||||
|
||||
res.push(data.slice(0, 10));
|
||||
|
||||
for (const word of words) {
|
||||
let encoding = hexJoin(["0x00", word]);
|
||||
|
||||
const altEncodings = [
|
||||
hexJoin(["0x01", VLQ.encode(word)]),
|
||||
hexJoin(["0x02", PseudoFloat.encode(word)]),
|
||||
word.startsWith("0x000000000000000000000000")
|
||||
? hexJoin(["0x03", `0x${word.slice(26)}`])
|
||||
: nil,
|
||||
];
|
||||
|
||||
for (const altEncoding of altEncodings) {
|
||||
if (altEncoding === nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (altEncoding.length < encoding.length) {
|
||||
encoding = altEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
res.push(encoding);
|
||||
}
|
||||
|
||||
return hexJoin(res);
|
||||
}
|
||||
|
||||
decode(encodedData: string): string {
|
||||
const stream = new ByteStream(encodedData);
|
||||
|
||||
const leadingVlq = VLQ.decode(stream);
|
||||
|
||||
if (leadingVlq.mod(2).eq(0)) {
|
||||
const len = leadingVlq.div(2);
|
||||
return stream.getN(len.toNumber());
|
||||
}
|
||||
|
||||
const wordLen = leadingVlq.div(2).toNumber();
|
||||
|
||||
const methodId = stream.getN(4);
|
||||
|
||||
const words: string[] = [];
|
||||
|
||||
for (let i = 0; i < wordLen; i++) {
|
||||
const typeId = stream.get();
|
||||
|
||||
switch (typeId) {
|
||||
case 0: {
|
||||
words.push(stream.getN(32));
|
||||
break;
|
||||
}
|
||||
|
||||
case 1: {
|
||||
words.push(bigNumberToWord(VLQ.decode(stream)));
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
words.push(bigNumberToWord(PseudoFloat.decode(stream)));
|
||||
break;
|
||||
}
|
||||
|
||||
case 3: {
|
||||
words.push(`0x000000000000000000000000${stream.getN(20).slice(2)}`);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert(false, `Unrecognized typeId ${typeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
methodId,
|
||||
...words,
|
||||
]);
|
||||
}
|
||||
}
|
||||
18
aggregator/manualTests/analysis/run.ts
Normal file
18
aggregator/manualTests/analysis/run.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Calculator from "./Calculator.ts";
|
||||
import MultiEncoder from "./MultiEncoder.ts";
|
||||
import ERC20TransferEncoder from "./encoders/ERC20TransferEncoder.ts";
|
||||
import FallbackEncoder from "./encoders/FallbackEncoder.ts";
|
||||
|
||||
const multiEncoder = new MultiEncoder();
|
||||
|
||||
multiEncoder.register(2, new ERC20TransferEncoder());
|
||||
multiEncoder.register(1, new FallbackEncoder());
|
||||
|
||||
const calc = new Calculator(multiEncoder);
|
||||
|
||||
// calc.checkDecodedTransactionData();
|
||||
|
||||
console.log(
|
||||
"biggestEncodedMethods",
|
||||
calc.biggestEncodedMethods().slice(0, 10),
|
||||
);
|
||||
31
aggregator/manualTests/analysis/util.ts
Normal file
31
aggregator/manualTests/analysis/util.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { BigNumber } from "../../deps.ts";
|
||||
|
||||
export function sum(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function getDataWords(data: string) {
|
||||
const res = [];
|
||||
|
||||
for (let i = 2; i < data.length; i += 64) {
|
||||
res.push("0x" + data.slice(i, i + 64));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function hexJoin(hexStrings: string[]) {
|
||||
return "0x" + hexStrings.map(remove0x).join("");
|
||||
}
|
||||
|
||||
export function remove0x(hexString: string) {
|
||||
if (!hexString.startsWith("0x")) {
|
||||
throw new Error("Expected 0x prefix");
|
||||
}
|
||||
|
||||
return hexString.slice(2);
|
||||
}
|
||||
|
||||
export function bigNumberToWord(x: BigNumber) {
|
||||
return "0x" + x.toHexString().slice(2).padStart(64, "0");
|
||||
}
|
||||
@@ -19,7 +19,7 @@ await (await adminWallet.sendTransaction({
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 1,
|
||||
|
||||
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ethers } from "../../deps.ts";
|
||||
|
||||
export default async function receiptOf(
|
||||
responsePromise: Promise<ethers.providers.TransactionResponse>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
const response = await responsePromise;
|
||||
const receipt = await response.wait();
|
||||
|
||||
return receipt;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -22,7 +22,7 @@ const walletIndex = Number(walletIndexStr);
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const wallet = await TestBlsWallet(provider, walletIndex);
|
||||
@@ -48,21 +48,23 @@ const mintAction: ActionData = {
|
||||
),
|
||||
};
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
console.log({ feeEstimation });
|
||||
|
||||
@@ -91,7 +93,7 @@ const feeAction: ActionData = {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [mintAction, feeAction],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { delay, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { delay, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -14,16 +14,14 @@ const ethereumService = await EthereumService.create(
|
||||
(evt) => {
|
||||
console.log(evt);
|
||||
},
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
const startBalance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -29,10 +29,10 @@ if (!Number.isFinite(walletN)) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
@@ -67,17 +67,19 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
assert(feeEstimation.feeType === "ether");
|
||||
|
||||
@@ -104,10 +106,12 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
bundles.push(wallet.sign({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}));
|
||||
bundles.push(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Sending mint bundles to aggregator");
|
||||
|
||||
34
aggregator/manualTests/registerWallet.ts
Executable file
34
aggregator/manualTests/registerWallet.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ContractsConnector, ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import receiptOf from "./helpers/receiptOf.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const connector = await ContractsConnector.create(adminWallet);
|
||||
|
||||
const addressRegistry = await connector.AddressRegistry();
|
||||
const blsPublicKeyRegistry = await connector.BLSPublicKeyRegistry();
|
||||
|
||||
await receiptOf(
|
||||
addressRegistry.register("0xCB1ca1e8DF1055636d7D07c3099c9de3c65CAAB4"),
|
||||
);
|
||||
|
||||
await receiptOf(
|
||||
blsPublicKeyRegistry.register(
|
||||
// You can get this in Quill by running this in the console of the wallet
|
||||
// page (the page you get by clicking on the extension icon)
|
||||
// JSON.stringify(debug.wallets[0].blsWalletSigner.getPublicKey())
|
||||
|
||||
[
|
||||
"0x0ad7e63a4bbfdad440beda1fe7fdfb77a59f2a6d991700c6cf4c3654a52389a9",
|
||||
"0x0adaa93bdfda0f6b259a80c1af7ccf3451c35c1e175483927a8052bdbf59f801",
|
||||
"0x1f56aa1bb1419c741f0a474e51f33da0ffc81ea870e2e2c440db72539a9efb9e",
|
||||
"0x2f1f7e5d586d6ca5de3c8c198c3be3b998a2b6df7ee8a367a1e58f8b36fd524d",
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -43,7 +43,7 @@ const { addresses } = await getNetworkConfig();
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
@@ -56,7 +56,7 @@ const [recvWallet, ...sendWallets] = await Promise.all(
|
||||
log("Checking/minting test tokens...");
|
||||
|
||||
for (const wallet of sendWallets) {
|
||||
const testErc20 = MockERC20__factory.connect(
|
||||
const testErc20 = MockERC20Factory.connect(
|
||||
addresses.testToken,
|
||||
adminWallet,
|
||||
);
|
||||
@@ -90,7 +90,7 @@ let txsAdded = 0;
|
||||
let txsCompleted = 0;
|
||||
let sendWalletIndex = 0;
|
||||
|
||||
pollingLoop(() => {
|
||||
pollingLoop(async () => {
|
||||
// Send transactions
|
||||
|
||||
const lead = txsSent - txsCompleted;
|
||||
@@ -102,7 +102,7 @@ pollingLoop(() => {
|
||||
const nonce = nextNonceMap.get(sendWallet)!;
|
||||
nextNonceMap.set(sendWallet, nonce.add(1));
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { AggregatorClient, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
// import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -15,15 +15,13 @@ const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
// (evt) => {
|
||||
// console.log(evt);
|
||||
// },
|
||||
// addresses.verificationGateway,
|
||||
// addresses.utilities,
|
||||
// env.PRIVATE_KEY_AGG,
|
||||
// );
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
VerificationGateway__factory,
|
||||
VerificationGatewayFactory,
|
||||
Wallet,
|
||||
} from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
@@ -14,7 +14,7 @@ const wallet = new Wallet(env.PRIVATE_KEY_AGG, provider);
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const vg = VerificationGateway__factory.connect(
|
||||
const vg = VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
wallet,
|
||||
);
|
||||
@@ -32,10 +32,12 @@ const nonce = await internalBlsWallet.Nonce();
|
||||
if (!nonce.eq(0)) {
|
||||
console.log("Already exists with nonce", nonce.toNumber());
|
||||
} else {
|
||||
await (await vg.processBundle(internalBlsWallet.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await vg.processBundle(
|
||||
await internalBlsWallet.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
|
||||
console.log("Created successfully");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export async function run(...cmd: string[]): Promise<void> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "inherit", stderr: "inherit" });
|
||||
|
||||
const unloadListener = () => {
|
||||
@@ -20,6 +22,8 @@ export async function run(...cmd: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
export async function String(...cmd: string[]): Promise<string> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "piped" });
|
||||
|
||||
if (process.stdout === null) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Bundle,
|
||||
decodeError,
|
||||
ERC20,
|
||||
ERC20__factory,
|
||||
ERC20Factory,
|
||||
ethers,
|
||||
OperationResultError,
|
||||
Semaphore,
|
||||
@@ -70,6 +70,7 @@ export type AggregationStrategyResult = {
|
||||
aggregateBundle: Bundle | nil;
|
||||
includedRows: BundleRow[];
|
||||
bundleOverheadCost: BigNumber;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: BigNumber;
|
||||
expectedMaxCost: BigNumber;
|
||||
failedRows: BundleRow[];
|
||||
@@ -105,7 +106,8 @@ export default class AggregationStrategy {
|
||||
async run(eligibleRows: BundleRow[]): Promise<AggregationStrategyResult> {
|
||||
eligibleRows = await this.#filterRows(eligibleRows);
|
||||
|
||||
const bundleOverheadGas = await this.measureBundleOverheadGas();
|
||||
const { bundleOverheadGas, bundleOverheadLen } = await this
|
||||
.measureBundleOverhead();
|
||||
|
||||
let aggregateBundle = this.blsWalletSigner.aggregate([]);
|
||||
let aggregateGas = bundleOverheadGas;
|
||||
@@ -147,6 +149,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -164,6 +167,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -217,6 +221,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -256,6 +261,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -410,13 +416,21 @@ export default class AggregationStrategy {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAggregateGas = (aggregateGas
|
||||
.add(gasEstimate)
|
||||
.sub(bundleOverheadGas));
|
||||
const bundleEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
const newAggregateGas = aggregateGas.add(bundleEstimate);
|
||||
|
||||
if (newAggregateGas.gt(this.config.maxGasPerBundle)) {
|
||||
// Bundle would cause us to exceed maxGasPerBundle, so don't include it,
|
||||
// but also don't mark it as failed.
|
||||
this.emit({
|
||||
type: "aggregate-bundle-exceeds-max-gas",
|
||||
data: {
|
||||
hash: row.hash,
|
||||
gasEstimate: bundleEstimate.toNumber(),
|
||||
aggregateGasEstimate: newAggregateGas.toNumber(),
|
||||
maxGasPerBundle: this.config.maxGasPerBundle,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -451,14 +465,16 @@ export default class AggregationStrategy {
|
||||
.callStaticSequenceWithMeasure(
|
||||
feeToken
|
||||
? es.Call(feeToken, "balanceOf", [es.wallet.address])
|
||||
: es.Call(es.utilities, "ethBalanceOf", [es.wallet.address]),
|
||||
bundles.map((bundle) =>
|
||||
: es.Call(es.aggregatorUtilities, "ethBalanceOf", [
|
||||
es.wallet.address,
|
||||
]),
|
||||
await Promise.all(bundles.map(async (bundle) =>
|
||||
es.Call(
|
||||
es.verificationGateway,
|
||||
"processBundle",
|
||||
[bundle],
|
||||
es.bundleCompressor.blsExpanderDelegator,
|
||||
"run",
|
||||
[await es.bundleCompressor.compress(bundle)],
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
return Range(bundles.length).map((i) => {
|
||||
@@ -514,7 +530,7 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ERC20__factory.connect(
|
||||
return ERC20Factory.connect(
|
||||
this.config.fees.address,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -525,10 +541,12 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bundleOverheadGas ??= await this.measureBundleOverheadGas();
|
||||
bundleOverheadGas ??=
|
||||
(await this.measureBundleOverhead()).bundleOverheadGas;
|
||||
|
||||
const gasEstimate = await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
const gasEstimate = await this.ethereumService.estimateCompressedGas(
|
||||
bundle,
|
||||
);
|
||||
|
||||
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
|
||||
@@ -617,8 +635,7 @@ export default class AggregationStrategy {
|
||||
}
|
||||
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
await this.ethereumService.estimateCompressedGas(bundle);
|
||||
|
||||
return {
|
||||
success,
|
||||
@@ -631,7 +648,7 @@ export default class AggregationStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
async measureBundleOverheadGas() {
|
||||
async measureBundleOverhead() {
|
||||
// The simple way to do this would be to estimate the gas of an empty
|
||||
// bundle. However, an empty bundle is a bit of a special case, in
|
||||
// particular the on-chain BLS library outright refuses to validate it. So
|
||||
@@ -647,19 +664,50 @@ export default class AggregationStrategy {
|
||||
// wallet creation would be included in the bundle overhead.
|
||||
assert(nonce.gt(0));
|
||||
|
||||
const bundle1 = wallet.sign({ nonce, actions: [] });
|
||||
const bundle2 = wallet.sign({ nonce: nonce.add(1), actions: [] });
|
||||
const bundle1 = wallet.sign({ nonce, gas: 1_000_000, actions: [] });
|
||||
|
||||
const bundle2 = wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
|
||||
es.verificationGateway.estimateGas.processBundle(bundle1),
|
||||
es.verificationGateway.estimateGas.processBundle(
|
||||
es.estimateCompressedGas(bundle1),
|
||||
es.estimateCompressedGas(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
|
||||
|
||||
return oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
const bundleOverheadGas = oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
|
||||
const [compressedBundle1, compressedBundle12] = await Promise.all([
|
||||
es.bundleCompressor.compress(bundle1),
|
||||
es.bundleCompressor.compress(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const [oneOpLen, twoOpLen] = await Promise.all([
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle1,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle12,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
]);
|
||||
|
||||
const opMarginalLen = twoOpLen - oneOpLen;
|
||||
const bundleOverheadLen = oneOpLen - opMarginalLen;
|
||||
|
||||
return {
|
||||
bundleOverheadGas,
|
||||
bundleOverheadLen,
|
||||
};
|
||||
}
|
||||
|
||||
async #TokenDecimals(): Promise<number> {
|
||||
|
||||
@@ -15,6 +15,7 @@ type AppEvent =
|
||||
data: {
|
||||
includedRows: number;
|
||||
bundleOverheadCost: string;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: string;
|
||||
expectedMaxCost: string;
|
||||
};
|
||||
@@ -32,10 +33,24 @@ type AppEvent =
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "aggregate-bundle-exceeds-max-gas";
|
||||
data: {
|
||||
hash: string;
|
||||
gasEstimate: number;
|
||||
aggregateGasEstimate: number;
|
||||
maxGasPerBundle: number;
|
||||
};
|
||||
}
|
||||
| { type: "unprofitable-despite-breakeven-operations" }
|
||||
| {
|
||||
type: "submission-attempt";
|
||||
data: { publicKeyShorts: string[]; attemptNumber: number };
|
||||
data: {
|
||||
publicKeyShorts: string[];
|
||||
attemptNumber: number;
|
||||
txLen: number;
|
||||
compressedTxLen: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "submission-attempt-failed";
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Router } from "../../deps.ts";
|
||||
import failRequest from "./helpers/failRequest.ts";
|
||||
import BundleHandler from "./helpers/BundleHandler.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
|
||||
import BundleService from "./BundleService.ts";
|
||||
|
||||
export default function BundleRouter(bundleService: BundleService) {
|
||||
@@ -40,5 +39,19 @@ export default function BundleRouter(bundleService: BundleService) {
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"aggregateBundle/:subBundleHash",
|
||||
(ctx) => {
|
||||
const bundleRows = bundleService.lookupAggregateBundle(ctx.params.subBundleHash!);
|
||||
|
||||
if (bundleRows === nil || !bundleRows?.length) {
|
||||
ctx.response.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = bundleRows;
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
delay,
|
||||
ethers,
|
||||
Semaphore,
|
||||
VerificationGatewayFactory,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import { IClock } from "../helpers/Clock.ts";
|
||||
@@ -18,10 +19,11 @@ import * as env from "../env.ts";
|
||||
import runQueryGroup from "./runQueryGroup.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
|
||||
import BundleTable, { BundleRow } from "./BundleTable.ts";
|
||||
import plus from "./helpers/plus.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
|
||||
export type AddBundleResponse = { hash: string } | {
|
||||
failures: TransactionFailure[];
|
||||
@@ -156,14 +158,15 @@ export default class BundleService {
|
||||
|
||||
const failures: TransactionFailure[] = [];
|
||||
|
||||
for (const walletAddr of walletAddresses) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: `invalid signature for wallet address ${walletAddr}`,
|
||||
});
|
||||
}
|
||||
const signedCorrectly = this.blsWalletSigner.verify(
|
||||
bundle,
|
||||
walletAddresses,
|
||||
);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: `invalid bundle signature for signature ${bundle.signature}`,
|
||||
});
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
@@ -173,7 +176,7 @@ export default class BundleService {
|
||||
}
|
||||
|
||||
return await this.runQueryGroup(async () => {
|
||||
const hash = makeHash();
|
||||
const hash = await this.hashBundle(bundle);
|
||||
|
||||
this.bundleTable.add({
|
||||
status: "pending",
|
||||
@@ -201,15 +204,21 @@ export default class BundleService {
|
||||
return this.bundleTable.findBundle(hash);
|
||||
}
|
||||
|
||||
lookupAggregateBundle(subBundleHash: string) {
|
||||
const subBundle = this.bundleTable.findBundle(subBundleHash);
|
||||
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!)
|
||||
}
|
||||
|
||||
receiptFromBundle(bundle: BundleRow) {
|
||||
if (!bundle.receipt) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const { receipt, hash } = bundle;
|
||||
const { receipt, hash, aggregateHash } = bundle;
|
||||
|
||||
return {
|
||||
bundleHash: hash,
|
||||
aggregateBundleHash: aggregateHash,
|
||||
to: receipt.to,
|
||||
from: receipt.from,
|
||||
contractAddress: receipt.contractAddress,
|
||||
@@ -230,6 +239,44 @@ export default class BundleService {
|
||||
};
|
||||
}
|
||||
|
||||
async hashBundle(bundle: Bundle): Promise<string> {
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const verifyMethodName = "verify";
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === verifyMethodName,
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await this.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
}
|
||||
|
||||
async runSubmission() {
|
||||
this.submissionsInProgress++;
|
||||
|
||||
@@ -257,6 +304,7 @@ export default class BundleService {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost,
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -268,11 +316,19 @@ export default class BundleService {
|
||||
data: {
|
||||
includedRows: includedRows.length,
|
||||
bundleOverheadCost: ethers.utils.formatEther(bundleOverheadCost),
|
||||
bundleOverheadLen,
|
||||
expectedFee: ethers.utils.formatEther(expectedFee),
|
||||
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
|
||||
},
|
||||
});
|
||||
|
||||
if (aggregateBundle) {
|
||||
const aggregateBundleHash = await this.hashBundle(aggregateBundle);
|
||||
for (const row of includedRows) {
|
||||
row.aggregateHash = aggregateBundleHash;
|
||||
}
|
||||
}
|
||||
|
||||
for (const failedRow of failedRows) {
|
||||
this.emit({
|
||||
type: "failed-row",
|
||||
|
||||
@@ -30,6 +30,7 @@ type RawRow = {
|
||||
nextEligibilityDelay: string;
|
||||
submitError: string | null;
|
||||
receipt: string | null;
|
||||
aggregateHash: string | null;
|
||||
};
|
||||
|
||||
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
|
||||
@@ -44,17 +45,12 @@ type Row = {
|
||||
nextEligibilityDelay: BigNumber;
|
||||
submitError?: string;
|
||||
receipt?: ethers.ContractReceipt;
|
||||
aggregateHash?: string;
|
||||
};
|
||||
|
||||
type InsertRow = Omit<Row, "id">;
|
||||
type InsertRawRow = Omit<RawRow, "id">;
|
||||
|
||||
export function makeHash() {
|
||||
const buf = new Uint8Array(32);
|
||||
crypto.getRandomValues(buf);
|
||||
return ethers.utils.hexlify(buf);
|
||||
}
|
||||
|
||||
export type BundleRow = Row;
|
||||
|
||||
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
@@ -68,6 +64,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
nextEligibilityDelay: rawRow[5] as string,
|
||||
submitError: rawRow[6] as string | null,
|
||||
receipt: rawRow[7] as string | null,
|
||||
aggregateHash: rawRow[8] as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +96,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
|
||||
submitError: rawRow.submitError ?? nil,
|
||||
receipt,
|
||||
aggregateHash: rawRow.aggregateHash ?? nil,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,6 +107,7 @@ function toInsertRawRow(row: InsertRow): InsertRawRow {
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
eligibleAfter: toUint256Hex(row.eligibleAfter),
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
@@ -123,6 +122,7 @@ function toRawRow(row: Row): RawRow {
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
submitError: row.submitError ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,10 +140,11 @@ export default class BundleTable {
|
||||
eligibleAfter TEXT NOT NULL,
|
||||
nextEligibilityDelay TEXT NOT NULL,
|
||||
submitError TEXT,
|
||||
receipt TEXT
|
||||
receipt TEXT,
|
||||
aggregateHash TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
|
||||
this.onQuery(sql, params);
|
||||
@@ -164,7 +165,8 @@ export default class BundleTable {
|
||||
eligibleAfter,
|
||||
nextEligibilityDelay,
|
||||
submitError,
|
||||
receipt
|
||||
receipt,
|
||||
aggregateHash
|
||||
) VALUES (
|
||||
:id,
|
||||
:status,
|
||||
@@ -173,7 +175,8 @@ export default class BundleTable {
|
||||
:eligibleAfter,
|
||||
:nextEligibilityDelay,
|
||||
:submitError,
|
||||
:receipt
|
||||
:receipt,
|
||||
:aggregateHash
|
||||
)
|
||||
`,
|
||||
{
|
||||
@@ -184,6 +187,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,7 +206,8 @@ export default class BundleTable {
|
||||
eligibleAfter = :eligibleAfter,
|
||||
nextEligibilityDelay = :nextEligibilityDelay,
|
||||
submitError = :submitError,
|
||||
receipt = :receipt
|
||||
receipt = :receipt,
|
||||
aggregateHash = :aggregateHash
|
||||
WHERE
|
||||
id = :id
|
||||
`,
|
||||
@@ -215,6 +220,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -255,6 +261,21 @@ export default class BundleTable {
|
||||
return rows.map(fromRawRow)[0];
|
||||
}
|
||||
|
||||
findAggregateBundle(aggregateHash: string): Row[] | nil {
|
||||
const rows = this.dbQuery(
|
||||
`
|
||||
SELECT * from bundles
|
||||
WHERE
|
||||
aggregateHash = :aggregateHash AND
|
||||
status = 'confirmed'
|
||||
ORDER BY id ASC
|
||||
`,
|
||||
{ ":aggregateHash": aggregateHash },
|
||||
);
|
||||
|
||||
return rows.map(fromRawRow);
|
||||
}
|
||||
|
||||
count(): number {
|
||||
const result = this.dbQuery("SELECT COUNT(*) FROM bundles")[0][0];
|
||||
assert(typeof result === "number");
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import {
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory,
|
||||
BaseContract,
|
||||
BigNumber,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
BundleCompressor,
|
||||
BytesLike,
|
||||
ContractsConnector,
|
||||
delay,
|
||||
Erc20Compressor,
|
||||
ethers,
|
||||
FallbackCompressor,
|
||||
initBlsWalletSigner,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
Wallet,
|
||||
} from "../../deps.ts";
|
||||
|
||||
@@ -64,30 +67,23 @@ type DecodeReturnType<
|
||||
Method extends keyof Contract["callStatic"],
|
||||
> = EnforceArray<AsyncReturnType<Contract["callStatic"][Method]>>;
|
||||
|
||||
export default class EthereumService {
|
||||
verificationGateway: VerificationGateway;
|
||||
utilities: AggregatorUtilities;
|
||||
type ExpanderEntryPoint = AsyncReturnType<
|
||||
ContractsConnector["ExpanderEntryPoint"]
|
||||
>;
|
||||
|
||||
export default class EthereumService {
|
||||
constructor(
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public wallet: Wallet,
|
||||
public provider: ethers.providers.Provider,
|
||||
public blsWalletWrapper: BlsWalletWrapper,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
public verificationGateway: VerificationGateway,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
public expanderEntryPoint: ExpanderEntryPoint,
|
||||
public bundleCompressor: BundleCompressor,
|
||||
public nextNonce: BigNumber,
|
||||
) {
|
||||
this.verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
this.wallet,
|
||||
);
|
||||
|
||||
this.utilities = AggregatorUtilities__factory.connect(
|
||||
utilitiesAddress,
|
||||
this.wallet,
|
||||
);
|
||||
}
|
||||
) {}
|
||||
|
||||
NextNonce() {
|
||||
const result = this.nextNonce;
|
||||
@@ -97,17 +93,35 @@ export default class EthereumService {
|
||||
|
||||
static async create(
|
||||
emit: (evt: AppEvent) => void,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
aggPrivateKey: string,
|
||||
): Promise<EthereumService> {
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
provider.pollingInterval = env.RPC_POLLING_INTERVAL;
|
||||
const wallet = EthereumService.Wallet(provider, aggPrivateKey);
|
||||
|
||||
const contractsConnector = await ContractsConnector.create(wallet);
|
||||
|
||||
const [
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
blsExpanderDelegator,
|
||||
erc20Expander,
|
||||
blsRegistration,
|
||||
fallbackExpander,
|
||||
expanderEntryPoint,
|
||||
] = await Promise.all([
|
||||
contractsConnector.VerificationGateway(),
|
||||
contractsConnector.AggregatorUtilities(),
|
||||
contractsConnector.BLSExpanderDelegator(),
|
||||
contractsConnector.ERC20Expander(),
|
||||
contractsConnector.BLSRegistration(),
|
||||
contractsConnector.FallbackExpander(),
|
||||
contractsConnector.ExpanderEntryPoint(),
|
||||
]);
|
||||
|
||||
const blsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
aggPrivateKey,
|
||||
verificationGatewayAddress,
|
||||
verificationGateway.address,
|
||||
provider,
|
||||
);
|
||||
|
||||
@@ -122,13 +136,12 @@ export default class EthereumService {
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
await (await VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
wallet,
|
||||
).processBundle(blsWalletWrapper.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await verificationGateway.processBundle(
|
||||
await blsWalletWrapper.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
}
|
||||
|
||||
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
|
||||
@@ -136,16 +149,32 @@ export default class EthereumService {
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey: aggPrivateKey,
|
||||
verificationGatewayAddress: verificationGateway.address,
|
||||
});
|
||||
|
||||
const bundleCompressor = new BundleCompressor(blsExpanderDelegator);
|
||||
|
||||
const [erc20Compressor, blsRegistrationCompressor, fallbackCompressor] =
|
||||
await Promise.all([
|
||||
Erc20Compressor.wrap(erc20Expander),
|
||||
BlsRegistrationCompressor.wrap(blsRegistration),
|
||||
FallbackCompressor.wrap(fallbackExpander),
|
||||
]);
|
||||
|
||||
await bundleCompressor.addCompressor(erc20Compressor);
|
||||
await bundleCompressor.addCompressor(blsRegistrationCompressor);
|
||||
await bundleCompressor.addCompressor(fallbackCompressor);
|
||||
|
||||
return new EthereumService(
|
||||
emit,
|
||||
wallet,
|
||||
provider,
|
||||
blsWalletWrapper,
|
||||
blsWalletSigner,
|
||||
verificationGatewayAddress,
|
||||
utilitiesAddress,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
expanderEntryPoint,
|
||||
bundleCompressor,
|
||||
nextNonce,
|
||||
);
|
||||
}
|
||||
@@ -218,9 +247,10 @@ export default class EthereumService {
|
||||
async callStaticSequence<Calls extends CallHelper<unknown>[]>(
|
||||
...calls: Calls
|
||||
): Promise<MapCallHelperReturns<Calls>> {
|
||||
const rawResults = await this.utilities.callStatic.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
const rawResults = await this.aggregatorUtilities.callStatic
|
||||
.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
|
||||
const results: CallResult<unknown>[] = rawResults.map(
|
||||
([success, result], i) => {
|
||||
@@ -288,22 +318,33 @@ export default class EthereumService {
|
||||
assert(bundle.operations.length > 0, "Cannot process empty bundle");
|
||||
assert(maxAttempts > 0, "Must have at least one attempt");
|
||||
|
||||
const processBundleArgs: Parameters<VerificationGateway["processBundle"]> =
|
||||
[
|
||||
bundle,
|
||||
{
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
},
|
||||
];
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
const [rawTx, rawCompressedTx] = await Promise.all([
|
||||
this.verificationGateway.populateTransaction.processBundle(bundle).then(
|
||||
(tx) => this.wallet.signTransaction(tx),
|
||||
),
|
||||
this.wallet.signTransaction({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
}),
|
||||
]);
|
||||
|
||||
const txLen = ethers.utils.hexDataLength(rawTx);
|
||||
const compressedTxLen = ethers.utils.hexDataLength(rawCompressedTx);
|
||||
|
||||
const txRequest: ethers.providers.TransactionRequest = {
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
};
|
||||
|
||||
const attempt = async () => {
|
||||
let txResponse: ethers.providers.TransactionResponse;
|
||||
|
||||
try {
|
||||
txResponse = await this.verificationGateway.processBundle(
|
||||
...processBundleArgs,
|
||||
);
|
||||
txResponse = await this.wallet.sendTransaction(txRequest);
|
||||
} catch (error) {
|
||||
if (/\binvalid transaction nonce\b/.test(error.message)) {
|
||||
// This can occur when the nonce is in the future, which can
|
||||
@@ -330,7 +371,7 @@ export default class EthereumService {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
this.emit({
|
||||
type: "submission-attempt",
|
||||
data: { attemptNumber: i + 1, publicKeyShorts },
|
||||
data: { attemptNumber: i + 1, publicKeyShorts, txLen, compressedTxLen },
|
||||
});
|
||||
|
||||
const attemptResult = await attempt();
|
||||
@@ -364,6 +405,15 @@ export default class EthereumService {
|
||||
throw new Error("Expected return or throw from attempt loop");
|
||||
}
|
||||
|
||||
async estimateCompressedGas(bundle: Bundle): Promise<BigNumber> {
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
return await this.wallet.estimateGas({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
});
|
||||
}
|
||||
|
||||
async GasConfig() {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
const previousBaseFee = block.baseFeePerGas;
|
||||
|
||||
16
aggregator/src/app/HealthRouter.ts
Normal file
16
aggregator/src/app/HealthRouter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from "../../deps.ts";
|
||||
import HealthService from "./HealthService.ts";
|
||||
|
||||
export default function HealthRouter(healthService: HealthService) {
|
||||
const router = new Router({ prefix: "/" });
|
||||
|
||||
router.get(
|
||||
"health",
|
||||
async (ctx) => {
|
||||
const healthResults = await healthService.getHealth();
|
||||
console.log(`Status: ${healthResults.status}\n`);
|
||||
ctx.response.status = healthResults.status == 'healthy' ? 200 : 503;
|
||||
ctx.response.body = { status: healthResults.status };
|
||||
});
|
||||
return router;
|
||||
}
|
||||
11
aggregator/src/app/HealthService.ts
Normal file
11
aggregator/src/app/HealthService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type ResourceHealth = 'healthy' | 'unhealthy';
|
||||
|
||||
type HealthCheckResult = {
|
||||
status: ResourceHealth,
|
||||
};
|
||||
|
||||
export default class HealthService {
|
||||
getHealth(): Promise<HealthCheckResult> {
|
||||
return Promise.resolve({ status: 'healthy' });
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,14 @@ import errorHandler from "./errorHandler.ts";
|
||||
import notFoundHandler from "./notFoundHandler.ts";
|
||||
import Mutex from "../helpers/Mutex.ts";
|
||||
import Clock from "../helpers/Clock.ts";
|
||||
import getNetworkConfig from "../helpers/getNetworkConfig.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable from "./BundleTable.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import AggregationStrategyRouter from "./AggregationStrategyRouter.ts";
|
||||
import HealthService from "./HealthService.ts";
|
||||
import HealthRouter from "./HealthRouter.ts";
|
||||
|
||||
export default async function app(emit: (evt: AppEvent) => void) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const clock = Clock.create();
|
||||
|
||||
const bundleTableMutex = new Mutex();
|
||||
@@ -37,8 +36,6 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
emit,
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -64,10 +61,13 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
bundleTable,
|
||||
);
|
||||
|
||||
const healthService = new HealthService();
|
||||
|
||||
const routers = [
|
||||
BundleRouter(bundleService),
|
||||
AdminRouter(adminService),
|
||||
AggregationStrategyRouter(aggregationStrategy),
|
||||
HealthRouter(healthService),
|
||||
];
|
||||
|
||||
const app = new Application();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BundleDto } from "../../deps.ts";
|
||||
|
||||
type ParseResult<T> = (
|
||||
type ParseResult<T> =
|
||||
| { success: T }
|
||||
| { failures: string[] }
|
||||
);
|
||||
| { failures: string[] };
|
||||
|
||||
type Parser<T> = (value: unknown) => ParseResult<T>;
|
||||
|
||||
@@ -96,14 +95,12 @@ export function parseArray<T>(
|
||||
};
|
||||
}
|
||||
|
||||
type DataTuple<ParserTuple> = (
|
||||
ParserTuple extends Parser<unknown>[] ? (
|
||||
type DataTuple<ParserTuple> = ParserTuple extends Parser<unknown>[] ? (
|
||||
ParserTuple extends [Parser<infer T>, ...infer Tail]
|
||||
? [T, ...DataTuple<Tail>]
|
||||
: []
|
||||
)
|
||||
: never
|
||||
);
|
||||
: never;
|
||||
|
||||
export function parseTuple<ParserTuple extends Parser<unknown>[]>(
|
||||
...parserTuple: ParserTuple
|
||||
@@ -188,6 +185,7 @@ const parseActionDataDto: Parser<ActionDataDto> = parseObject({
|
||||
|
||||
const parseOperationDto: Parser<OperationDto> = parseObject({
|
||||
nonce: parseHex(),
|
||||
gas: parseHex(),
|
||||
actions: parseArray(parseActionDataDto),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import Fixture from "./helpers/Fixture.ts";
|
||||
Fixture.test("nonzero fee estimate from default test config", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -51,7 +51,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
let bundle = wallet.sign({
|
||||
let bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -84,7 +84,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
assertEquals(feeEstimation.feeDetected, BigNumber.from(1));
|
||||
|
||||
// Redefine bundle using the estimated fee
|
||||
bundle = wallet.sign({
|
||||
bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -135,7 +135,7 @@ Fixture.test("includes submitError on failed row when bundle callStaticSequence
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
|
||||
|
||||
import { BigNumber, Operation, VerificationGatewayFactory, assertBundleSucceeds, assertEquals, ethers } from "./deps.ts";
|
||||
import ExplicitAny from "../src/helpers/ExplicitAny.ts";
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("adds valid bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
const tx = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -20,7 +20,7 @@ Fixture.test("adds valid bundle", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
@@ -33,6 +33,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -53,7 +54,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
// sig test)
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -62,7 +63,47 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with valid signature but invalid public key", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet, otherWallet] = await fx.setupWallets(2);
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tx = wallet.sign(operation);
|
||||
const otherTx = otherWallet.sign(operation);
|
||||
|
||||
// Make the signature invalid
|
||||
// Note: Bug in bls prevents just corrupting the signature (see other invalid
|
||||
// sig test)
|
||||
tx.senderPublicKeys[0] = otherTx.senderPublicKeys[0];
|
||||
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
throw new Error("expected bundle to fail");
|
||||
}
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
assertEquals(res.failures.map((f) => f.description), [`invalid bundle signature for signature ${tx.signature}`]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
@@ -71,6 +112,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -83,7 +125,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -92,7 +134,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["duplicate-nonce"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test(
|
||||
@@ -103,6 +145,7 @@ Fixture.test(
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -125,7 +168,7 @@ Fixture.test(
|
||||
// https://github.com/thehubbleproject/hubble-bls/pull/20
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -138,7 +181,7 @@ Fixture.test(
|
||||
);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -148,6 +191,7 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -160,11 +204,232 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("Same bundle produces same hash", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const firstBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const secondBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 999999,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const firstBundleHash = await bundleService.hashBundle(firstBundle);
|
||||
const secondBundleHash = await bundleService.hashBundle(secondBundle);
|
||||
|
||||
assertEquals(firstBundleHash, secondBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with single operation", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with multiple operations", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 3],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 5],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes empty bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const bundle = fx.blsWalletSigner.aggregate([]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
// TODO (merge-ok): Add a mechanism for limiting the number of stored
|
||||
|
||||
@@ -37,7 +37,7 @@ function approveAndSendTokensToOrigin(
|
||||
fx: Fixture,
|
||||
nonce: BigNumber,
|
||||
amount: BigNumber,
|
||||
): Operation {
|
||||
): Omit<Operation, "gas"> {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
return {
|
||||
@@ -48,13 +48,13 @@ function approveAndSendTokensToOrigin(
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"approve",
|
||||
[es.utilities.address, amount],
|
||||
[es.aggregatorUtilities.address, amount],
|
||||
),
|
||||
},
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendTokenToTxOrigin",
|
||||
[fx.testErc20.address, amount],
|
||||
),
|
||||
@@ -68,7 +68,7 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -108,8 +108,9 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
tokenBalance: oneToken,
|
||||
});
|
||||
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, await wallet.Nonce(), oneToken),
|
||||
0.1,
|
||||
);
|
||||
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
@@ -120,7 +121,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
oneToken,
|
||||
);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
@@ -129,7 +130,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
const bundleRow = bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
|
||||
@@ -158,13 +159,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
})).wait();
|
||||
|
||||
const estimation = await bundleService.aggregationStrategy.estimateFee(
|
||||
wallet.sign({
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -183,13 +184,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
.sub(1), // Already sent 1 wei before
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
ethValue: fee,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -245,8 +246,9 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
wallet: BlsWalletWrapper,
|
||||
fee: BigNumber,
|
||||
) {
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, nonce, fee),
|
||||
0.1,
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
@@ -275,7 +277,7 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
// Restore this value now that all the bundles are added together
|
||||
bundleService.config.breakevenOperationCount = breakevenOperationCount;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10);
|
||||
assertEquals(bundleService.bundleTable.count(), 10);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
|
||||
@@ -10,7 +10,7 @@ const bundleServiceConfig = {
|
||||
};
|
||||
|
||||
const aggregationStrategyConfig: AggregationStrategyConfig = {
|
||||
maxGasPerBundle: 900000,
|
||||
maxGasPerBundle: 1_000_000,
|
||||
fees: nil,
|
||||
bundleCheckingConcurrency: 8,
|
||||
};
|
||||
@@ -23,7 +23,7 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -78,20 +78,22 @@ Fixture.test("submits a full submission without delay", async (fx) => {
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
for (const b of bundles) {
|
||||
@@ -121,20 +123,22 @@ Fixture.test(
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// Prevent submission from triggering on max aggregation size.
|
||||
@@ -185,6 +189,7 @@ Fixture.test(
|
||||
const bundles = Range(3).reverse().map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -273,6 +278,7 @@ Fixture.test("retains failing bundle when its eligibility delay is smaller than
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -313,6 +319,7 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -351,3 +358,57 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
|
||||
assertEquals(failedBundleRow?.status, "failed");
|
||||
});
|
||||
|
||||
Fixture.test("Retrieves all sub bundles included in a submitted bundle from single a sub bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
|
||||
const wallets = await fx.setupWallets(3);
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
const subBundleHashes = await Promise.all(bundles.map(async (bundle) => {
|
||||
const res = await bundleService.add(bundle);
|
||||
|
||||
if ("failures" in res) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
|
||||
return res.hash;
|
||||
}));
|
||||
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
const firstSubBundle = bundleService.lookupBundle(subBundleHashes[0]);
|
||||
const secondSubBundle = bundleService.lookupBundle(subBundleHashes[1]);
|
||||
const thirdSubBundle = bundleService.lookupBundle(subBundleHashes[2]);
|
||||
|
||||
const orderedSubBundles = [firstSubBundle, secondSubBundle, thirdSubBundle].sort((a, b) => a!.id - b!.id);
|
||||
|
||||
for (const subBundleHash of subBundleHashes) {
|
||||
const aggregateBundle = bundleService.lookupAggregateBundle(subBundleHash);
|
||||
assertEquals(aggregateBundle?.[0], orderedSubBundles[0]);
|
||||
assertEquals(aggregateBundle?.[1], orderedSubBundles[1]);
|
||||
assertEquals(aggregateBundle?.[2], orderedSubBundles[2]);
|
||||
}
|
||||
});
|
||||
@@ -13,6 +13,7 @@ const sampleRows: BundleRow[] = [
|
||||
operations: [
|
||||
{
|
||||
nonce: "0x01",
|
||||
gas: "0x01",
|
||||
actions: [
|
||||
{
|
||||
ethValue: "0x00",
|
||||
@@ -28,6 +29,7 @@ const sampleRows: BundleRow[] = [
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
submitError: nil,
|
||||
receipt: nil,
|
||||
aggregateHash: nil,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const startBalance = await fx.testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -32,7 +32,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
Fixture.test("EthereumService submits transfer action", async (fx) => {
|
||||
const wallets = await fx.setupWallets(2);
|
||||
|
||||
const bundle = wallets[0].sign({
|
||||
const bundle = await wallets[0].signWithGasEstimate({
|
||||
nonce: await wallets[0].Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -62,6 +62,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce: walletNonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -75,6 +76,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -102,21 +104,24 @@ Fixture.test("EthereumService submits large aggregate mint bundle", async (fx) =
|
||||
const size = 11;
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
await Promise.all(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -137,6 +142,7 @@ Fixture.test("EthereumService sends large aggregate transfer bundle", async (fx)
|
||||
Range(size).map((i) =>
|
||||
sendWallet.sign({
|
||||
nonce: sendWalletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -170,6 +176,7 @@ Fixture.test(
|
||||
Range(5).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -238,7 +245,7 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
value: transferAmount,
|
||||
})).wait();
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce: await sendWallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -253,9 +260,9 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
const results = await es.callStaticSequence(
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.verificationGateway, "processBundle", [bundle]),
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
);
|
||||
|
||||
const [balanceResultBefore, , balanceResultAfter] = results;
|
||||
|
||||
10
aggregator/test/HealthService.test.ts
Normal file
10
aggregator/test/HealthService.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { assertEquals } from "./deps.ts";
|
||||
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("HealthService returns healthy", async (fx) => {
|
||||
const healthCheckService = fx.createHealthCheckService()
|
||||
const healthStatus = await healthCheckService.getHealth();
|
||||
const expected = {"status":"healthy"};
|
||||
assertEquals(JSON.stringify(healthStatus), JSON.stringify(expected));
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
MockERC20,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
NetworkConfig,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
@@ -25,6 +25,7 @@ import BundleTable, { BundleRow } from "../../src/app/BundleTable.ts";
|
||||
import AggregationStrategy, {
|
||||
AggregationStrategyConfig,
|
||||
} from "../../src/app/AggregationStrategy.ts";
|
||||
import HealthService from "../../src/app/HealthService.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ExplicitAny = any;
|
||||
@@ -74,11 +75,10 @@ export default class Fixture {
|
||||
static async create(testName: string): Promise<Fixture> {
|
||||
const netCfg = await getNetworkConfig();
|
||||
const rng = testRng.seed(testName);
|
||||
const emit = (evt: AppEvent) => fx.emit(evt);
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
(evt) => fx.emit(evt),
|
||||
netCfg.addresses.verificationGateway,
|
||||
netCfg.addresses.utilities,
|
||||
emit,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ export default class Fixture {
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
aggregationStrategyDefaultTestConfig,
|
||||
emit,
|
||||
),
|
||||
netCfg,
|
||||
);
|
||||
@@ -131,7 +132,7 @@ export default class Fixture {
|
||||
public aggregationStrategy: AggregationStrategy,
|
||||
public networkConfig: NetworkConfig,
|
||||
) {
|
||||
this.testErc20 = MockERC20__factory.connect(
|
||||
this.testErc20 = MockERC20Factory.connect(
|
||||
this.networkConfig.addresses.testToken,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -171,6 +172,7 @@ export default class Fixture {
|
||||
this.blsWalletSigner,
|
||||
this.ethereumService,
|
||||
aggregationStrategyConfig,
|
||||
this.emit,
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
@@ -245,7 +247,7 @@ export default class Fixture {
|
||||
const topUp = BigNumber.from(tokenBalance).sub(balance);
|
||||
|
||||
if (topUp.gt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -261,7 +263,7 @@ export default class Fixture {
|
||||
}
|
||||
|
||||
if (topUp.lt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -293,6 +295,12 @@ export default class Fixture {
|
||||
return wallets;
|
||||
}
|
||||
|
||||
createHealthCheckService() {
|
||||
const healthCheckService = new HealthService();
|
||||
|
||||
return healthCheckService;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (const job of this.cleanupJobs) {
|
||||
await job();
|
||||
|
||||
@@ -29,6 +29,7 @@ Deno.test("parseBundleDto accepts dummy values", () => {
|
||||
"operations": [
|
||||
{
|
||||
"nonce": "0x01",
|
||||
"gas": "0x01",
|
||||
"actions": [
|
||||
{
|
||||
"ethValue": "0x00",
|
||||
|
||||
@@ -8,12 +8,6 @@ OPTIMISM_TESETNET_URL=https://kovan.optimism.io
|
||||
OPTIMISM_URL=https://mainnet.optimism.io
|
||||
OPTIMISM_GOERLI_URL=https://goerli.optimism.io
|
||||
|
||||
# Only used for deploying the deployer contract at the same address on each evm network
|
||||
DEPLOYER_MNEMONIC="sock poet alone around radar forum quiz session observe rebel another choice"
|
||||
DEPLOYER_SET_INDEX=1
|
||||
DEPLOYER_ACCOUNT=0x6435e511f8908D5C733898C81831a4A3aFE31D07
|
||||
DEPLOYER_CONTRACT_ADDRESS=0x036d996D6855B83cd80142f2933d8C2617dA5617
|
||||
|
||||
# Used for deploying contracts via the deployment contract, and testing
|
||||
MAIN_MNEMONIC="test test test test test test test test test test test junk"
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ For each network, the deployer contract can be deployed with the following scrip
|
||||
|
||||
To run integration tests:
|
||||
|
||||
1. cd into `./contracts` and run `yarn start-hardhat`
|
||||
1. cd into `./contracts` and run `yarn start`
|
||||
2. cd into `./aggregator` and run `./programs/aggregator.ts`
|
||||
3. from `./contracts`, run `yarn test-integration`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.8.2-1452ef5",
|
||||
"version": "0.9.0-2a20bfe",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
||||
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { AddressRegistry } from "../typechain-types/contracts/AddressRegistry";
|
||||
import { AddressRegistry__factory as AddressRegistryFactory } from "../typechain-types/factories/contracts/AddressRegistry__factory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
/**
|
||||
* A wrapper around the `AddressRegistry` contract to provide a more ergonomic
|
||||
* interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class AddressRegistryWrapper {
|
||||
constructor(public registry: AddressRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `AddressRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<AddressRegistryWrapper> {
|
||||
const factory = new AddressRegistryFactory(signer);
|
||||
|
||||
return new AddressRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new AddressRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new AddressRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup an address, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<string | undefined> {
|
||||
const address = await this.registry.addresses(id);
|
||||
|
||||
return address === ethers.constants.AddressZero ? undefined : address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an address to lookup an id - the reverse of what happens on chain, by
|
||||
* making use of the indexed `AddressRegistered` event.
|
||||
*/
|
||||
async reverseLookup(address: string): Promise<BigNumber | undefined> {
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.AddressRegistered(null, address),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address and returns the id that was assigned to it.
|
||||
*/
|
||||
async register(address: string): Promise<BigNumber> {
|
||||
await (await this.registry.register(address)).wait();
|
||||
|
||||
const id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address if it hasn't already been registered, and returns the
|
||||
* id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(address: string): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(address);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export type BundleReceiptError = {
|
||||
*/
|
||||
export type BlsBundleReceipt = {
|
||||
bundleHash: string;
|
||||
aggregateBundleHash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,7 @@ export default class Aggregator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the fee required for a bundle by the aggreagtor to submit it.
|
||||
* Estimates the fee required for a bundle by the aggregator to submit it.
|
||||
*
|
||||
* @param bundle Bundle to estimates the fee for
|
||||
* @returns Estimate of the fee needed to submit the bundle
|
||||
@@ -125,6 +126,21 @@ export default class Aggregator {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aggregate bundle that a sub bundle was a part of.
|
||||
* This will return undefined if the bundle does not exist or does not have an aggregate bundle.
|
||||
*
|
||||
* @param hash Hash of the bundle to find the aggregate bundle for.
|
||||
* @returns The aggregate bundle, or undefined if either the sub bundle or aggregate bundle were not found.
|
||||
*/
|
||||
async getAggregateBundleFromSubBundle(
|
||||
subBundleHash: string,
|
||||
): Promise<Bundle | undefined> {
|
||||
return this.jsonGet<Bundle>(
|
||||
`${this.origin}/aggregateBundle/${subBundleHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: This should be private instead of exposed. Leaving as is for compatibility.
|
||||
async jsonPost(path: string, body: unknown): Promise<unknown> {
|
||||
const resp = await this.fetchImpl(`${this.origin}${path}`, {
|
||||
|
||||
@@ -76,9 +76,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await this.getTransactionCount(
|
||||
resolvedTransaction.from.toString(),
|
||||
);
|
||||
// set to zero at all times as an error will be thrown. If the
|
||||
// nonce of the actual wallet is more than 0, there will be a
|
||||
// nonce mistmatch as signWithGasEstimate with check the operation
|
||||
// nonce against the throwawayBlsWalletWrapper nonce, which is always zero
|
||||
const nonce = 0;
|
||||
|
||||
const actionWithFeePaymentAction =
|
||||
this._addFeePaymentActionForFeeEstimation([action]);
|
||||
@@ -94,12 +96,12 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
this,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(
|
||||
throwawayBlsWalletWrapper.sign({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
}),
|
||||
);
|
||||
const bundle = await throwawayBlsWalletWrapper.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(bundle);
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
return addSafetyPremiumToFee(feeRequired);
|
||||
@@ -341,7 +343,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return [
|
||||
...actions,
|
||||
{
|
||||
ethValue: fee,
|
||||
ethValue: fee.toHexString(),
|
||||
contractAddress: this.aggregatorUtilitiesAddress,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
|
||||
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils";
|
||||
import {
|
||||
BLSPublicKeyRegistry,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
} from "../typechain-types";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { PublicKey } from "./signer";
|
||||
|
||||
/**
|
||||
* A wrapper around the `BLSPublicKeyRegistry` contract to provide a more
|
||||
* ergonomic interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class BlsPublicKeyRegistryWrapper {
|
||||
constructor(public registry: BLSPublicKeyRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `BLSPublicKeyRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new BlsPublicKeyRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup a public key, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<PublicKey | undefined> {
|
||||
const blsPublicKey = await Promise.all([
|
||||
this.registry.blsPublicKeys(id, 0),
|
||||
this.registry.blsPublicKeys(id, 1),
|
||||
this.registry.blsPublicKeys(id, 2),
|
||||
this.registry.blsPublicKeys(id, 3),
|
||||
]);
|
||||
|
||||
if (blsPublicKey.every((x) => x.eq(0))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a public key to lookup an id - the reverse of what happens on chain,
|
||||
* by making use of the indexed `BLSPublicKeyRegistered` event.
|
||||
*/
|
||||
async reverseLookup(blsPublicKey: PublicKey): Promise<BigNumber | undefined> {
|
||||
const blsPublicKeyHash = solidityKeccak256(["uint256[4]"], [blsPublicKey]);
|
||||
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.BLSPublicKeyRegistered(null, blsPublicKeyHash),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key and returns the id.
|
||||
*/
|
||||
async register(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
await (await this.registry.register(blsPublicKey)).wait();
|
||||
|
||||
const id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key if it hasn't already been registered, and returns
|
||||
* the id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(blsPublicKey);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import { encodePseudoFloat, encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class BlsRegistrationCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public blsRegistration: BLSRegistration,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
blsRegistration: BLSRegistration,
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
blsRegistration.blsPublicKeyRegistry(),
|
||||
blsRegistration.addressRegistry(),
|
||||
blsRegistration.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistration,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<BlsRegistrationCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const blsRegistrationFactory = new BLSRegistrationFactory(signer);
|
||||
|
||||
const blsRegistrationContract = await blsRegistrationFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const blsRegistrationContract = await factory.connectOrDeploy(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const blsPublicKeyRegistryAddress = factoryViewer.calculateAddress(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const addressRegistryAddress = factoryViewer.calculateAddress(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const aggregatorUtilitiesAddress = factoryViewer.calculateAddress(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const blsRegistration = await factoryViewer.connectIfDeployed(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!blsRegistration) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await BlsRegistrationCompressor.wrap(blsRegistration);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.blsRegistration.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
if (operation.actions.length > 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be a non-paying call to blsRegistration.register with the user's
|
||||
// blsPublicKey
|
||||
const firstAction = operation.actions.at(0);
|
||||
|
||||
if (
|
||||
firstAction === undefined ||
|
||||
!BigNumber.from(firstAction.ethValue).isZero() ||
|
||||
firstAction.contractAddress !== this.blsRegistration.address ||
|
||||
ethers.utils.hexlify(firstAction.encodedFunction) !==
|
||||
this.blsRegistration.interface.encodeFunctionData("register", [
|
||||
blsPublicKey,
|
||||
])
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be absent or a non-zero payment to tx.origin
|
||||
const secondAction = operation.actions.at(1);
|
||||
|
||||
if (secondAction !== undefined) {
|
||||
if (
|
||||
BigNumber.from(secondAction.ethValue).isZero() ||
|
||||
secondAction.contractAddress !== this.aggregatorUtilities.address ||
|
||||
ethers.utils.hexlify(secondAction.encodedFunction) !==
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
encodeVLQ(operation.nonce),
|
||||
encodePseudoFloat(operation.gas),
|
||||
encodePseudoFloat(secondAction?.ethValue ?? 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
|
||||
import {
|
||||
AccessListish,
|
||||
Deferrable,
|
||||
hexlify,
|
||||
isBytes,
|
||||
RLP,
|
||||
} from "ethers/lib/utils";
|
||||
import { AccessListish, Deferrable, hexlify, isBytes } from "ethers/lib/utils";
|
||||
|
||||
import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
import { ActionData, bundleToDto } from "./signer";
|
||||
import { ActionData, Signature, bundleToDto } from "./signer";
|
||||
|
||||
export const _constructorGuard = {};
|
||||
|
||||
@@ -114,6 +108,13 @@ export default class BlsSigner extends Signer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a random BLS private key
|
||||
*/
|
||||
static async getRandomBlsPrivateKey(): Promise<string> {
|
||||
return await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transactions to be executed. Converts the TransactionRequest
|
||||
* to a bundle and adds it to the aggregator
|
||||
@@ -131,10 +132,6 @@ export default class BlsSigner extends Signer {
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
@@ -155,7 +152,7 @@ export default class BlsSigner extends Signer {
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -188,7 +185,7 @@ export default class BlsSigner extends Signer {
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
nonce = BigNumber.from(validatedTransactionBatch.batchOptions!.nonce);
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -210,11 +207,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -226,7 +225,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -315,7 +314,7 @@ export default class BlsSigner extends Signer {
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -362,11 +361,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -378,7 +379,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -386,8 +387,15 @@ export default class BlsSigner extends Signer {
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/** Signs a message */
|
||||
// TODO: bls-wallet #201 Come back to this once we support EIP-1271
|
||||
/**
|
||||
* Signs a message. Because of the function signature enforced by ethers, we cannot return the signature
|
||||
* in it's default type. Instead, we return a concatenated string of the signature.
|
||||
*
|
||||
* Use BlsSigner.signedMessageToSignature to convert the concatenated signature string into a BLS Signature type.
|
||||
*
|
||||
* @param message the message to be signed
|
||||
* @returns a concatenated string of the signature
|
||||
*/
|
||||
override async signMessage(message: Bytes | string): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (isBytes(message)) {
|
||||
@@ -395,7 +403,18 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const signedMessage = this.wallet.signMessage(message);
|
||||
return RLP.encode(signedMessage);
|
||||
return (
|
||||
ethers.utils.hexlify(signedMessage[0]) +
|
||||
ethers.utils.hexlify(signedMessage[1]).substring(2)
|
||||
);
|
||||
}
|
||||
|
||||
/** helper method to convert blsSigner.signMessage concatenated signature string into BLS Signature type */
|
||||
static signedMessageToSignature(signedMessage: string): Signature {
|
||||
return [
|
||||
ethers.utils.hexlify(signedMessage.substring(0, 66)),
|
||||
ethers.utils.hexlify("0x" + signedMessage.substring(66, 130)),
|
||||
];
|
||||
}
|
||||
|
||||
override connect(provider: ethers.providers.Provider): BlsSigner {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { providers } from "ethers";
|
||||
import {
|
||||
BNPairingPrecompileCostEstimator,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
Create2Deployer,
|
||||
Create2Deployer__factory as Create2DeployerFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
BLSOpen,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSExpander,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
@@ -21,10 +15,7 @@ import { NetworkConfig } from "./NetworkConfig";
|
||||
* BLS Wallet Contracts
|
||||
*/
|
||||
export type BlsWalletContracts = Readonly<{
|
||||
create2Deployer: Create2Deployer;
|
||||
precompileCostEstimator: BNPairingPrecompileCostEstimator;
|
||||
verificationGateway: VerificationGateway;
|
||||
blsLibrary: BLSOpen;
|
||||
blsExpander: BLSExpander;
|
||||
aggregatorUtilities: AggregatorUtilities;
|
||||
testToken: MockERC20;
|
||||
@@ -41,32 +32,19 @@ export const connectToContracts = async (
|
||||
provider: providers.Provider,
|
||||
{ addresses }: NetworkConfig,
|
||||
): Promise<BlsWalletContracts> => {
|
||||
const [
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
] = await Promise.all([
|
||||
Create2DeployerFactory.connect(addresses.create2Deployer, provider),
|
||||
BNPairingPrecompileCostEstimatorFactory.connect(
|
||||
addresses.create2Deployer,
|
||||
provider,
|
||||
),
|
||||
VerificationGatewayFactory.connect(addresses.verificationGateway, provider),
|
||||
BLSOpenFactory.connect(addresses.blsLibrary, provider),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
const [verificationGateway, blsExpander, aggregatorUtilities, testToken] =
|
||||
await Promise.all([
|
||||
VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
provider,
|
||||
),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
|
||||
return {
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
|
||||
import {
|
||||
@@ -13,10 +11,10 @@ import {
|
||||
|
||||
import {
|
||||
BLSWallet,
|
||||
BLSWallet__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
BLSWallet__factory as BLSWalletFactory,
|
||||
TransparentUpgradeableProxy__factory as TransparentUpgradeableProxyFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
import getRandomBlsPrivateKey from "./signer/getRandomBlsPrivateKey";
|
||||
@@ -28,9 +26,11 @@ type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
*/
|
||||
export default class BlsWalletWrapper {
|
||||
public address: string;
|
||||
public blockGasLimit: BigNumber = BigNumber.from(0);
|
||||
private constructor(
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public walletContract: BLSWallet,
|
||||
public defaultGatewayAddress: string,
|
||||
) {
|
||||
this.address = walletContract.address;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
return BLSWallet__factory.connect(
|
||||
return BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -74,9 +74,10 @@ export default class BlsWalletWrapper {
|
||||
const blsWalletSigner = await this.#BlsWalletSigner(
|
||||
signerOrProvider,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
);
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -140,19 +141,24 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
provider: ethers.providers.Provider,
|
||||
): Promise<BlsWalletWrapper> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: (await verificationGateway.provider.getNetwork()).chainId,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
|
||||
verificationGateway.address,
|
||||
);
|
||||
blsWalletWrapper.blockGasLimit = (
|
||||
await provider.getBlock("latest")
|
||||
).gasLimit;
|
||||
|
||||
return blsWalletWrapper;
|
||||
}
|
||||
@@ -164,7 +170,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
this.walletContract = BLSWallet__factory.connect(
|
||||
this.walletContract = BLSWalletFactory.connect(
|
||||
this.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -194,7 +200,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
signerOrProvider: SignerOrProvider,
|
||||
): Promise<BigNumber> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -208,7 +214,7 @@ export default class BlsWalletWrapper {
|
||||
publicKeyHash,
|
||||
);
|
||||
|
||||
const walletContract = BLSWallet__factory.connect(
|
||||
const walletContract = BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -225,6 +231,46 @@ export default class BlsWalletWrapper {
|
||||
return await walletContract.nonce();
|
||||
}
|
||||
|
||||
/** Sign an operation with an estimate of the gas required. */
|
||||
async signWithGasEstimate(
|
||||
operation: Omit<Operation, "gas">,
|
||||
|
||||
/**
|
||||
* Optional: Multiply estimate by `(1+overhead)` to account for uncertainty.
|
||||
* Reduces the chance of running out of gas.
|
||||
*/
|
||||
overhead = 0,
|
||||
): Promise<Bundle> {
|
||||
let gas = await this.estimateGas(operation);
|
||||
gas = gas.add(gas.div(10000).mul(Math.ceil(overhead * 10000)));
|
||||
|
||||
return this.sign({ ...operation, gas });
|
||||
}
|
||||
|
||||
/** Estimate the gas needed for an operation. */
|
||||
async estimateGas(operation: Omit<Operation, "gas">): Promise<BigNumber> {
|
||||
const exists =
|
||||
(await this.walletContract.provider.getCode(this.address)) !== "0x";
|
||||
|
||||
const gatewayAddress = exists
|
||||
? await this.walletContract.trustedBLSGateway()
|
||||
: this.defaultGatewayAddress;
|
||||
|
||||
const gateway = VerificationGatewayFactory.connect(
|
||||
gatewayAddress,
|
||||
this.walletContract.provider,
|
||||
);
|
||||
|
||||
const gas = await gateway
|
||||
.connect(ethers.constants.AddressZero)
|
||||
.callStatic.measureOperationGas(this.PublicKey(), {
|
||||
...operation,
|
||||
gas: this.blockGasLimit,
|
||||
});
|
||||
|
||||
return gas;
|
||||
}
|
||||
|
||||
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
|
||||
sign(operation: Operation): Bundle {
|
||||
return this.blsWalletSigner.sign(operation, this.walletContract.address);
|
||||
@@ -264,7 +310,7 @@ export default class BlsWalletWrapper {
|
||||
[recoverWalletAddress, walletHash, saltHash],
|
||||
);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -284,13 +330,17 @@ export default class BlsWalletWrapper {
|
||||
newPrivateKey: string,
|
||||
recoverySalt: string,
|
||||
verificationGateway: VerificationGateway,
|
||||
signatureExpiryTimestamp: number,
|
||||
): Promise<Bundle> {
|
||||
const updatedWallet = await BlsWalletWrapper.connect(
|
||||
newPrivateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
const addressMessage = solidityPack(["address"], [recoveryAddress]);
|
||||
const addressMessage = solidityPack(
|
||||
["address", "uint256"],
|
||||
[recoveryAddress, signatureExpiryTimestamp],
|
||||
);
|
||||
const addressSignature = updatedWallet.signMessage(addressMessage);
|
||||
|
||||
const recoveryWalletHash = await verificationGateway.hashFromWallet(
|
||||
@@ -298,7 +348,7 @@ export default class BlsWalletWrapper {
|
||||
);
|
||||
const saltHash = ethers.utils.formatBytes32String(recoverySalt);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -311,6 +361,7 @@ export default class BlsWalletWrapper {
|
||||
recoveryWalletHash,
|
||||
saltHash,
|
||||
updatedWallet.PublicKey(),
|
||||
signatureExpiryTimestamp,
|
||||
],
|
||||
),
|
||||
},
|
||||
@@ -321,36 +372,18 @@ export default class BlsWalletWrapper {
|
||||
static async #BlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
return await initBlsWalletSigner({ chainId, privateKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the BlsWalletSigner instance to a new private key and chainId
|
||||
*
|
||||
* @returns The updated BlsWalletSigner object
|
||||
*/
|
||||
async setBlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
const newBlsWalletSigner = await initBlsWalletSigner({
|
||||
return await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
this.blsWalletSigner = newBlsWalletSigner;
|
||||
return newBlsWalletSigner;
|
||||
}
|
||||
|
||||
// Calculates the expected address the wallet will be created at
|
||||
@@ -364,7 +397,7 @@ export default class BlsWalletWrapper {
|
||||
]);
|
||||
|
||||
const initFunctionParams =
|
||||
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
|
||||
BLSWalletFactory.createInterface().encodeFunctionData("initialize", [
|
||||
verificationGateway.address,
|
||||
]);
|
||||
|
||||
@@ -374,7 +407,7 @@ export default class BlsWalletWrapper {
|
||||
ethers.utils.solidityKeccak256(
|
||||
["bytes", "bytes"],
|
||||
[
|
||||
TransparentUpgradeableProxy__factory.bytecode,
|
||||
TransparentUpgradeableProxyFactory.bytecode,
|
||||
ethers.utils.defaultAbiCoder.encode(
|
||||
["address", "address", "bytes"],
|
||||
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
|
||||
|
||||
98
contracts/clients/src/BundleCompressor.ts
Normal file
98
contracts/clients/src/BundleCompressor.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ethers } from "ethers";
|
||||
import { encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import { Bundle, Operation, PublicKey } from "./signer";
|
||||
import Range from "./helpers/Range";
|
||||
import { BLSExpanderDelegator } from "../typechain-types";
|
||||
|
||||
/**
|
||||
* Produces compressed bundles that can be passed to `BLSExpanderDelegator.run`
|
||||
* instead of `VerificationGateway.processBundle`.
|
||||
*
|
||||
* The compression of operations is delegated to other compressors that you
|
||||
* inject using `.addCompressor`. For each operation of the bundle, these
|
||||
* compressors are tried in the order they were added, and the first one that
|
||||
* succeeds is used. Note that `expanderIndex` is unrelated to this order - it
|
||||
* just needs to match the index that the corresponding expander contract is
|
||||
* registered at in BLSExpanderDelegator.
|
||||
*/
|
||||
export default class BundleCompressor {
|
||||
compressors: [number, IOperationCompressor][] = [];
|
||||
|
||||
constructor(public blsExpanderDelegator: BLSExpanderDelegator) {}
|
||||
|
||||
/** Add an operation compressor. */
|
||||
async addCompressor(compressor: IOperationCompressor) {
|
||||
const registrations = await this.blsExpanderDelegator.queryFilter(
|
||||
this.blsExpanderDelegator.filters.ExpanderRegistered(
|
||||
null,
|
||||
compressor.getExpanderAddress(),
|
||||
),
|
||||
);
|
||||
|
||||
const id = registrations.at(0)?.args?.id;
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Expander not registered");
|
||||
}
|
||||
|
||||
this.compressors.push([id.toNumber(), compressor]);
|
||||
}
|
||||
|
||||
/** Compresses a single operation. */
|
||||
async compressOperation(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string> {
|
||||
let expanderIndexAndData: [number, string] | undefined;
|
||||
|
||||
for (const [expanderIndex, compressor] of this.compressors) {
|
||||
let data: string | undefined;
|
||||
|
||||
try {
|
||||
data = await compressor.compress(blsPublicKey, operation);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expanderIndexAndData = [expanderIndex, data];
|
||||
break;
|
||||
}
|
||||
|
||||
if (expanderIndexAndData === undefined) {
|
||||
throw new Error("Failed to compress operation");
|
||||
}
|
||||
|
||||
const [expanderIndex, data] = expanderIndexAndData;
|
||||
|
||||
return hexJoin([encodeVLQ(expanderIndex), data]);
|
||||
}
|
||||
|
||||
/** Compresses a bundle. */
|
||||
async compress(bundle: Bundle): Promise<string> {
|
||||
const len = bundle.operations.length;
|
||||
|
||||
if (bundle.senderPublicKeys.length !== len) {
|
||||
throw new Error("ops vs keys length mismatch");
|
||||
}
|
||||
|
||||
const compressedOperations = await Promise.all(
|
||||
Range(len).map((i) =>
|
||||
this.compressOperation(
|
||||
bundle.senderPublicKeys[i],
|
||||
bundle.operations[i],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(bundle.operations.length),
|
||||
...compressedOperations,
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[2]"], [bundle.signature]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
165
contracts/clients/src/ContractsConnector.ts
Normal file
165
contracts/clients/src/ContractsConnector.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { ethers } from "ethers";
|
||||
import { SafeSingletonFactoryViewer } from "./SafeSingletonFactory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import assert from "./helpers/assert";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSExpanderDelegator__factory as BLSExpanderDelegatorFactory,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
ExpanderEntryPoint__factory as ExpanderEntryPointFactory,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
export default class ContractsConnector {
|
||||
constructor(
|
||||
public factoryViewer: SafeSingletonFactoryViewer,
|
||||
public salt: ethers.utils.BytesLike = ethers.utils.solidityPack(
|
||||
["uint256"],
|
||||
[0],
|
||||
),
|
||||
) {}
|
||||
|
||||
static async create(signerOrProvider: SignerOrProvider) {
|
||||
let provider: ethers.providers.Provider;
|
||||
|
||||
if ("getNetwork" in signerOrProvider) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
assert(
|
||||
signerOrProvider.provider !== undefined,
|
||||
"When using a signer, it's required to have a provider",
|
||||
);
|
||||
|
||||
provider = signerOrProvider.provider;
|
||||
}
|
||||
|
||||
const chainId = (await provider.getNetwork()).chainId;
|
||||
|
||||
const factoryViewer = new SafeSingletonFactoryViewer(
|
||||
signerOrProvider,
|
||||
chainId,
|
||||
);
|
||||
|
||||
return new ContractsConnector(factoryViewer);
|
||||
}
|
||||
|
||||
BNPairingPrecompileCostEstimator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BNPairingPrecompileCostEstimatorFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSOpen = once(() =>
|
||||
this.factoryViewer.connectOrThrow(BLSOpenFactory, [], this.salt),
|
||||
);
|
||||
|
||||
VerificationGateway = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
VerificationGatewayFactory,
|
||||
[(await this.BLSOpen()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AggregatorUtilities = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpanderDelegator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderDelegatorFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSPublicKeyRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AddressRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(AddressRegistryFactory, [], this.salt),
|
||||
);
|
||||
|
||||
FallbackExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
FallbackExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSRegistration = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSRegistrationFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ERC20Expander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ERC20ExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ExpanderEntryPoint = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ExpanderEntryPointFactory,
|
||||
[(await this.BLSExpanderDelegator()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function once<T extends {}>(fn: () => T): () => T {
|
||||
let result: T | undefined;
|
||||
|
||||
return () => {
|
||||
if (result === undefined) {
|
||||
result = fn();
|
||||
(fn as unknown as undefined) = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
394
contracts/clients/src/Erc20Compressor.ts
Normal file
394
contracts/clients/src/Erc20Compressor.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
ERC20Expander,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class Erc20Compressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public erc20Expander: ERC20Expander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(erc20Expander: ERC20Expander): Promise<Erc20Compressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
erc20Expander.blsPublicKeyRegistry(),
|
||||
erc20Expander.addressRegistry(),
|
||||
erc20Expander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20Expander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<Erc20Compressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const erc20ExpanderFactory = new ERC20ExpanderFactory(signer);
|
||||
|
||||
const erc20ExpanderContract = await erc20ExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20ExpanderContract = await factory.connectOrDeploy(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20Expander = await factoryViewer.connectIfDeployed(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!erc20Expander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await Erc20Compressor.wrap(erc20Expander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.erc20Expander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const success = await this.compressFunctionCall(
|
||||
action.encodedFunction,
|
||||
result,
|
||||
bitStream,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
|
||||
private async compressFunctionCall(
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
const encodedFunctionHex = ethers.utils.hexlify(encodedFunction);
|
||||
const parametersHex = ethers.utils.hexDataSlice(encodedFunctionHex, 4);
|
||||
|
||||
if (isMethod("transfer(address,uint256)", encodedFunction)) {
|
||||
return this.compressTransfer(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("transferFrom(address,address,uint256)", encodedFunction)) {
|
||||
return this.compressTransferFrom(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("approve(address,uint256)", encodedFunction)) {
|
||||
return this.compressApprove(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("mint(address,uint256)", encodedFunction)) {
|
||||
return this.compressMint(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async compressTransfer(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(0));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressTransferFrom(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 3 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(1));
|
||||
|
||||
const [from, to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, string, BigNumber];
|
||||
|
||||
await this.compressAddress(from, result, bitStream);
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressApprove(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [spender, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
if (value.eq(ethers.constants.MaxUint256)) {
|
||||
result.push(encodeVLQ(3));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
} else {
|
||||
result.push(encodeVLQ(2));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressMint(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(4));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressAddress(
|
||||
address: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(address);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(address);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMethod(
|
||||
signature: string,
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
): boolean {
|
||||
const methodId = ethers.utils
|
||||
.keccak256(ethers.utils.toUtf8Bytes(signature))
|
||||
.slice(0, 10);
|
||||
|
||||
return ethers.utils.hexlify(encodedFunction).startsWith(methodId);
|
||||
}
|
||||
7
contracts/clients/src/Experimental.ts
Normal file
7
contracts/clients/src/Experimental.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
247
contracts/clients/src/FallbackCompressor.ts
Normal file
247
contracts/clients/src/FallbackCompressor.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
FallbackExpander,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class FallbackCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public fallbackExpander: FallbackExpander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
fallbackExpander: FallbackExpander,
|
||||
): Promise<FallbackCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
fallbackExpander.blsPublicKeyRegistry(),
|
||||
fallbackExpander.addressRegistry(),
|
||||
fallbackExpander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<FallbackCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const fallbackExpanderFactory = new FallbackExpanderFactory(signer);
|
||||
|
||||
const fallbackExpanderContract = await fallbackExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpanderContract = await factory.connectOrDeploy(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpander = await factoryViewer.connectIfDeployed(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!fallbackExpander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await FallbackCompressor.wrap(fallbackExpander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.fallbackExpander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
result.push(encodePseudoFloat(action.ethValue));
|
||||
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const fnHex = ethers.utils.hexlify(action.encodedFunction);
|
||||
const fnLen = (fnHex.length - 2) / 2;
|
||||
|
||||
result.push(encodeVLQ(fnLen));
|
||||
result.push(fnHex);
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
}
|
||||
12
contracts/clients/src/IOperationCompressor.ts
Normal file
12
contracts/clients/src/IOperationCompressor.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Operation, PublicKey } from "./signer";
|
||||
|
||||
type IOperationCompressor = {
|
||||
getExpanderAddress(): string;
|
||||
|
||||
compress(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export default IOperationCompressor;
|
||||
@@ -10,10 +10,9 @@ export type NetworkConfig = {
|
||||
* Contract addresses
|
||||
*/
|
||||
addresses: {
|
||||
create2Deployer: string;
|
||||
safeSingletonFactory: string;
|
||||
precompileCostEstimator: string;
|
||||
verificationGateway: string;
|
||||
blsLibrary: string;
|
||||
blsExpander: string;
|
||||
utilities: string;
|
||||
testToken: string;
|
||||
@@ -24,11 +23,15 @@ export type NetworkConfig = {
|
||||
auxiliary: {
|
||||
chainid: number;
|
||||
/**
|
||||
* Domain used for BLS signing
|
||||
* Domain used for signing BLS Proof of Possession messages
|
||||
*/
|
||||
domain: string;
|
||||
walletDomain: string;
|
||||
/**
|
||||
* Starting block contracts began dpeloyment at
|
||||
* Domain used for signing BLS Bundle messages
|
||||
*/
|
||||
bundleDomain: string;
|
||||
/**
|
||||
* Starting block contracts began deployment at
|
||||
*/
|
||||
genesisBlock: number;
|
||||
/**
|
||||
@@ -54,19 +57,19 @@ export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
|
||||
return {
|
||||
parameters: assertUnknownRecord(cfg.parameters),
|
||||
addresses: {
|
||||
create2Deployer: assertString(cfg.addresses.create2Deployer),
|
||||
safeSingletonFactory: assertString(cfg.addresses.safeSingletonFactory),
|
||||
precompileCostEstimator: assertString(
|
||||
cfg.addresses.precompileCostEstimator,
|
||||
),
|
||||
verificationGateway: assertString(cfg.addresses.verificationGateway),
|
||||
blsLibrary: assertString(cfg.addresses.blsLibrary),
|
||||
blsExpander: assertString(cfg.addresses.blsExpander),
|
||||
utilities: assertString(cfg.addresses.utilities),
|
||||
testToken: assertString(cfg.addresses.testToken),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: assertNumber(cfg.auxiliary.chainid),
|
||||
domain: assertString(cfg.auxiliary.domain),
|
||||
walletDomain: assertString(cfg.auxiliary.walletDomain),
|
||||
bundleDomain: assertString(cfg.auxiliary.bundleDomain),
|
||||
genesisBlock: assertNumber(cfg.auxiliary.genesisBlock),
|
||||
deployedBy: assertString(cfg.auxiliary.deployedBy),
|
||||
version: assertString(cfg.auxiliary.version),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BigNumber, ContractReceipt, utils } from "ethers";
|
||||
import assert from "./helpers/assert";
|
||||
import { ActionData } from "./signer";
|
||||
import { Result } from "ethers/lib/utils";
|
||||
import { VerificationGateway__factory as VerificationGatewayFactory } from "../typechain-types";
|
||||
|
||||
export const errorSelectors = {
|
||||
Error: calculateAndCheckSelector("Error(string)", "0x08c379a0"),
|
||||
@@ -117,22 +119,45 @@ const getError = (
|
||||
export const getOperationResults = (
|
||||
txnReceipt: ContractReceipt,
|
||||
): OperationResult[] => {
|
||||
const walletOpProcessedEvents = txnReceipt.events?.filter(
|
||||
(e) => e.event === "WalletOperationProcessed",
|
||||
);
|
||||
if (!walletOpProcessedEvents?.length) {
|
||||
let walletOpProcessedEvents: Result[] = (txnReceipt.events ?? [])
|
||||
.filter((e) => e.event === "WalletOperationProcessed")
|
||||
.map(({ args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
|
||||
return args;
|
||||
});
|
||||
|
||||
if (walletOpProcessedEvents.length === 0 && txnReceipt.logs !== undefined) {
|
||||
const vgInterface = VerificationGatewayFactory.createInterface();
|
||||
|
||||
const WalletOperationProcessed = vgInterface.getEvent(
|
||||
"WalletOperationProcessed",
|
||||
);
|
||||
|
||||
walletOpProcessedEvents = txnReceipt.logs
|
||||
.filter(
|
||||
(log) =>
|
||||
log.topics[0] === vgInterface.getEventTopic(WalletOperationProcessed),
|
||||
)
|
||||
.map((log) =>
|
||||
vgInterface.decodeEventLog(
|
||||
WalletOperationProcessed,
|
||||
log.data,
|
||||
log.topics,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (walletOpProcessedEvents.length === 0) {
|
||||
throw new Error(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return walletOpProcessedEvents.reduce<OperationResult[]>(
|
||||
(opResults, { args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
const { wallet, nonce, actions: rawActions, success, results } = args;
|
||||
|
||||
(opResults, { wallet, nonce, actions: rawActions, success, results }) => {
|
||||
const actions = rawActions.map(
|
||||
({
|
||||
ethValue,
|
||||
@@ -150,7 +175,7 @@ export const getOperationResults = (
|
||||
);
|
||||
const error = getError(success, results);
|
||||
|
||||
return [
|
||||
const ret = [
|
||||
...opResults,
|
||||
{
|
||||
walletAddress: wallet,
|
||||
@@ -161,6 +186,8 @@ export const getOperationResults = (
|
||||
error,
|
||||
},
|
||||
];
|
||||
|
||||
return ret;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
import assert from "assert";
|
||||
import { ethers, Signer } from "ethers";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
|
||||
/**
|
||||
* Filters out the optional elements of an array type because an optional
|
||||
* element isn't considered to match First in [infer First, ...].
|
||||
*/
|
||||
type NonOptionalElementsOf<A extends unknown[]> = A extends [
|
||||
infer First,
|
||||
...infer Tail,
|
||||
]
|
||||
? [First, ...NonOptionalElementsOf<Tail>]
|
||||
: A extends [opt?: unknown]
|
||||
? []
|
||||
: never;
|
||||
|
||||
export type ContractFactoryConstructor = {
|
||||
new (): ethers.ContractFactory;
|
||||
};
|
||||
|
||||
export type DeployParams<CFC extends ContractFactoryConstructor> =
|
||||
NonOptionalElementsOf<Parameters<InstanceType<CFC>["deploy"]>>;
|
||||
|
||||
type Deployment = {
|
||||
gasPrice: number;
|
||||
gasLimit: number;
|
||||
signerAddress: string;
|
||||
transaction: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export default class SafeSingletonFactory {
|
||||
static sharedAddress = "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7";
|
||||
|
||||
static deployments: Record<number, Deployment | undefined> = {
|
||||
1337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf3820a96a",
|
||||
"0460c6ea9b8f791e5d9e67fbf2c70aba92bf88591c39ac3747ea1bedc2ef1750ca04b",
|
||||
"08a4b5cea15a56276513da7a0c0b34f16e89811d5dd911efba5f8625a921cc",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
31337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf382f4f5a",
|
||||
"00dc4d1d21b308094a30f5f93da35e4d72e99115378f135f2295bea47301a3165a063",
|
||||
"6b822daad40aa8c52dd5132f378c0c0e6d83b4898228c7e21c84e631a0b891",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
};
|
||||
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
viewer: SafeSingletonFactoryViewer;
|
||||
|
||||
private constructor(
|
||||
public signer: ethers.Signer,
|
||||
public chainId: number,
|
||||
public address: string,
|
||||
) {
|
||||
if (signer.provider === undefined) {
|
||||
throw new Error("Expected signer with provider");
|
||||
}
|
||||
|
||||
this.provider = signer.provider;
|
||||
|
||||
this.viewer = new SafeSingletonFactoryViewer(signer, chainId);
|
||||
}
|
||||
|
||||
static async init(signer: ethers.Signer): Promise<SafeSingletonFactory> {
|
||||
assert(signer.provider !== undefined, "Expected signer with provider");
|
||||
|
||||
const chainId = await signer.getChainId();
|
||||
|
||||
const address =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
const existingCode = await signer.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return new SafeSingletonFactory(signer, chainId, address);
|
||||
}
|
||||
|
||||
const deployment = SafeSingletonFactory.deployments[chainId];
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(
|
||||
[
|
||||
"Cannot get deployment for SafeSingletonFactory (check",
|
||||
"https://github.com/safe-global/safe-singleton-factory/tree/main/artifacts",
|
||||
`for chain id ${chainId})`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
// Fund the eoa account for the presigned transaction
|
||||
await (
|
||||
await signer.sendTransaction({
|
||||
to: deployment.signerAddress,
|
||||
value: ethers.BigNumber.from(deployment.gasPrice).mul(
|
||||
deployment.gasLimit,
|
||||
),
|
||||
})
|
||||
).wait();
|
||||
|
||||
await (
|
||||
await signer.provider.sendTransaction(deployment.transaction)
|
||||
).wait();
|
||||
|
||||
const deployedCode = await signer.provider.getCode(deployment.address);
|
||||
assert(deployedCode !== "0x", "Failed to deploy safe singleton factory");
|
||||
|
||||
return new SafeSingletonFactory(signer, chainId, deployment.address);
|
||||
}
|
||||
|
||||
static async from(signerOrFactory: ethers.Signer | SafeSingletonFactory) {
|
||||
if (signerOrFactory instanceof SafeSingletonFactory) {
|
||||
return signerOrFactory;
|
||||
}
|
||||
|
||||
return await SafeSingletonFactory.init(signerOrFactory);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
return this.viewer.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<boolean> {
|
||||
return this.viewer.isDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
let contract = await this.viewer.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (contract !== undefined) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrDeploy<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
|
||||
const deployTx = {
|
||||
to: this.address,
|
||||
data: ethers.utils.solidityPack(["uint256", "bytes"], [salt, initCode]),
|
||||
};
|
||||
|
||||
try {
|
||||
await (await this.signer.sendTransaction(deployTx)).wait();
|
||||
} catch (error) {
|
||||
if ((error as { code: string }).code !== "INSUFFICIENT_FUNDS") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const gasEstimate = await this.provider.estimateGas(deployTx);
|
||||
const gasPrice = await this.provider.getGasPrice();
|
||||
|
||||
const balance = await this.provider.getBalance(this.signer.getAddress());
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
"Account",
|
||||
await this.signer.getAddress(),
|
||||
"has insufficient funds:",
|
||||
ethers.utils.formatEther(balance),
|
||||
"ETH, need (approx):",
|
||||
ethers.utils.formatEther(gasEstimate.mul(gasPrice)),
|
||||
"ETH",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
const deployedCode = await this.provider.getCode(address);
|
||||
|
||||
assert(deployedCode !== "0x", "Failed to deploy to expected address");
|
||||
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeSingletonFactoryViewer {
|
||||
safeSingletonFactoryAddress: string;
|
||||
signer?: Signer;
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
constructor(
|
||||
public signerOrProvider: SignerOrProvider,
|
||||
public chainId: number,
|
||||
) {
|
||||
this.safeSingletonFactoryAddress =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
let provider: ethers.providers.Provider | undefined;
|
||||
|
||||
if (ethers.providers.Provider.isProvider(signerOrProvider)) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
provider = signerOrProvider.provider;
|
||||
this.signer = signerOrProvider;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
static async from(signerOrProvider: SignerOrProvider) {
|
||||
const provider = ethers.providers.Provider.isProvider(signerOrProvider)
|
||||
? signerOrProvider
|
||||
: signerOrProvider.provider;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
return new SafeSingletonFactoryViewer(signerOrProvider, network.chainId);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
return ethers.utils.getCreate2Address(
|
||||
this.safeSingletonFactoryAddress,
|
||||
salt,
|
||||
ethers.utils.keccak256(initCode),
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
return existingCode !== "0x";
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode === "0x") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
let contract = contractFactory.attach(address) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
|
||||
if (this.signer) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrThrow<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contract = await this.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(
|
||||
`Contract ${
|
||||
ContractFactoryConstructor.name
|
||||
} not deployed at ${this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
119
contracts/clients/src/encodeUtils.ts
Normal file
119
contracts/clients/src/encodeUtils.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BigNumber, BigNumberish, ethers } from "ethers";
|
||||
|
||||
export function hexJoin(hexStrings: string[]) {
|
||||
return "0x" + hexStrings.map(remove0x).join("");
|
||||
}
|
||||
|
||||
export function remove0x(hexString: string) {
|
||||
if (!hexString.startsWith("0x")) {
|
||||
throw new Error("Expected 0x prefix");
|
||||
}
|
||||
|
||||
return hexString.slice(2);
|
||||
}
|
||||
|
||||
export function encodeVLQ(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
while (true) {
|
||||
const segment = x.mod(128);
|
||||
segments.unshift(segment.toNumber());
|
||||
x = x.sub(segment);
|
||||
x = x.div(128);
|
||||
|
||||
if (x.eq(0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = "0x";
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const keepGoing = i !== segments.length - 1;
|
||||
|
||||
const byte = (keepGoing ? 128 : 0) + segments[i];
|
||||
result += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodePseudoFloat(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
if (x.eq(0)) {
|
||||
return "0x00";
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
|
||||
while (x.mod(10).eq(0) && exponent < 30) {
|
||||
x = x.div(10);
|
||||
exponent++;
|
||||
}
|
||||
|
||||
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
|
||||
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
|
||||
|
||||
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return hexJoin([`0x${firstByte}`, encodeVLQ(x.div(8))]);
|
||||
}
|
||||
|
||||
export function encodeRegIndex(regIndex: BigNumberish) {
|
||||
regIndex = BigNumber.from(regIndex);
|
||||
|
||||
const vlqValue = regIndex.div(0x010000);
|
||||
const fixedValue = regIndex.mod(0x010000).toNumber();
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(vlqValue),
|
||||
`0x${fixedValue.toString(16).padStart(4, "0")}`,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit streams are just the bits of a uint256 encoded as a VLQ.
|
||||
* (Technically the encoding is unbounded, but 256 booleans is a lot and it's
|
||||
* much easier to just decode the VLQ into a uint256 in the EVM.)
|
||||
*
|
||||
* Notably, the bits are little endian - the first bit is the *lowest* bit. This
|
||||
* is because the lowest bit is clearly the 1-valued bit, but the highest valued
|
||||
* bit could be anywhere - there's infinitely many zero-bits to choose from.
|
||||
*
|
||||
* If it wasn't for this need to be little endian, we'd definitely use big
|
||||
* endian (like our other encodings generally do), since that's preferred by the
|
||||
* EVM and the ecosystem:
|
||||
*
|
||||
* ```ts
|
||||
* const abi = new ethers.utils.AbiCoder():
|
||||
* console.log(abi.encode(["uint"], [0xff]));
|
||||
* // 0x00000000000000000000000000000000000000000000000000000000000000ff
|
||||
*
|
||||
* // If Ethereum used little endian (like x86), it would instead be:
|
||||
* // 0xff00000000000000000000000000000000000000000000000000000000000000
|
||||
* ```
|
||||
*/
|
||||
export function encodeBitStream(bitStream: boolean[]) {
|
||||
let stream = 0;
|
||||
let bitValue = 1;
|
||||
|
||||
const abi = new ethers.utils.AbiCoder();
|
||||
abi.encode(["uint"], [0xff]);
|
||||
|
||||
for (const bit of bitStream) {
|
||||
if (bit) {
|
||||
stream += bitValue;
|
||||
}
|
||||
|
||||
bitValue *= 2;
|
||||
}
|
||||
|
||||
const streamVLQ = encodeVLQ(stream);
|
||||
|
||||
return streamVLQ;
|
||||
}
|
||||
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
|
||||
export default SignerOrProvider;
|
||||
@@ -6,7 +6,7 @@ import { BigNumber } from "ethers";
|
||||
* the chance that bundles get accepted during aggregation.
|
||||
*
|
||||
* @param feeEstimate fee required for bundle
|
||||
* @param safetyDivisor optional safety divisor. Default is 5
|
||||
* @param safetyDivisor optional safety divisor. Default is 5 (adds a 20% safety margin)
|
||||
* @returns fee estimate with added safety premium
|
||||
*/
|
||||
export default function addSafetyPremiumToFee(
|
||||
|
||||
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { Bundle } from "../signer";
|
||||
import { VerificationGatewayFactory } from "../index";
|
||||
|
||||
/**
|
||||
* Generates a deterministic hash of a bundle. Because the signature of the bundle could change, along with the gas property on operations,
|
||||
* those values are set to 0 before hashing. This leads to a more consistent hash for variations of the same bundle.
|
||||
*
|
||||
* @remarks the hash output is senstive to the internal types of the bundle. For example, an identical bundle with a
|
||||
* BigNumber value for one of the properties, vs the same bundle with a hex string value for one of the properties, will
|
||||
* generate different hashes, even though the underlying value may be the same.
|
||||
*
|
||||
* @param bundle the signed bundle to generate the hash for
|
||||
* @param chainId the chain id of the network the bundle is being submitted to
|
||||
* @returns a deterministic hash of the bundle
|
||||
*/
|
||||
export default function hashBundle(bundle: Bundle, chainId: number): string {
|
||||
if (bundle.operations.length !== bundle.senderPublicKeys.length) {
|
||||
throw new Error(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
}
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const verifyMethodName = "verify";
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === verifyMethodName,
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as any],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
|
||||
const encoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(encoding);
|
||||
}
|
||||
@@ -1,81 +1,48 @@
|
||||
import Aggregator from "./Aggregator";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsSigner from "./BlsSigner";
|
||||
export { default as Aggregator } from "./Aggregator";
|
||||
export { default as BlsWalletWrapper } from "./BlsWalletWrapper";
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { VerificationGateway__factory } from "../typechain-types/factories/contracts/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain-types/contracts/VerificationGateway";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AggregatorUtilities__factory } from "../typechain-types/factories/contracts/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain-types/contracts/AggregatorUtilities";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { ERC20__factory } from "../typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { MockERC20__factory } from "../typechain-types/factories/contracts/mock/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain-types/contracts/mock/MockERC20";
|
||||
|
||||
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
import {
|
||||
export { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
export {
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
} from "./MultiNetworkConfig";
|
||||
export { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
import {
|
||||
export {
|
||||
OperationResult,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
OperationResultError,
|
||||
} from "./OperationResults";
|
||||
import { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
export { default as hashBundle } from "./helpers/hashBundle";
|
||||
|
||||
export {
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
ERC20__factory as ERC20Factory,
|
||||
MockERC20__factory as MockERC20Factory,
|
||||
type VerificationGateway,
|
||||
type AggregatorUtilities,
|
||||
type ERC20,
|
||||
type MockERC20,
|
||||
} from "../typechain-types";
|
||||
|
||||
export * from "./signer";
|
||||
|
||||
const Experimental_ = {
|
||||
BlsProvider,
|
||||
BlsSigner,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
namespace Experimental {
|
||||
export const BlsProvider = Experimental_.BlsProvider;
|
||||
export const BlsSigner = Experimental_.BlsSigner;
|
||||
}
|
||||
|
||||
export {
|
||||
Aggregator,
|
||||
BlsWalletWrapper,
|
||||
NetworkConfig,
|
||||
getConfig,
|
||||
validateConfig,
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
OperationResult,
|
||||
OperationResultError,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway,
|
||||
// eslint-disable-next-line camelcase
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilities,
|
||||
// eslint-disable-next-line camelcase
|
||||
ERC20__factory,
|
||||
ERC20,
|
||||
// eslint-disable-next-line camelcase
|
||||
MockERC20__factory,
|
||||
MockERC20,
|
||||
BlsWalletContracts,
|
||||
connectToContracts,
|
||||
Experimental,
|
||||
};
|
||||
default as SafeSingletonFactory,
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
export { default as AddressRegistryWrapper } from "./AddressRegistryWrapper";
|
||||
export { default as BlsPublicKeyRegistryWrapper } from "./BlsPublicKeyRegistryWrapper";
|
||||
export { default as FallbackCompressor } from "./FallbackCompressor";
|
||||
export { default as Erc20Compressor } from "./Erc20Compressor";
|
||||
export { default as BlsRegistrationCompressor } from "./BlsRegistrationCompressor";
|
||||
export { default as BundleCompressor } from "./BundleCompressor";
|
||||
export { default as ContractsConnector } from "./ContractsConnector";
|
||||
export * from "./encodeUtils";
|
||||
|
||||
@@ -12,6 +12,7 @@ export function bundleToDto(bundle: Bundle): BundleDto {
|
||||
]),
|
||||
operations: bundle.operations.map((op) => ({
|
||||
nonce: BigNumber.from(op.nonce).toHexString(),
|
||||
gas: BigNumber.from(op.gas).toHexString(),
|
||||
actions: op.actions.map((a) => ({
|
||||
ethValue: BigNumber.from(a.ethValue).toHexString(),
|
||||
contractAddress: a.contractAddress,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { keccak256, solidityPack } from "ethers/lib/utils";
|
||||
import { Operation } from "./types";
|
||||
|
||||
export default (chainId: number) =>
|
||||
export default () =>
|
||||
(operation: Operation, walletAddress: string): string => {
|
||||
let encodedActionData = "0x";
|
||||
|
||||
@@ -18,7 +18,7 @@ export default (chainId: number) =>
|
||||
}
|
||||
|
||||
return solidityPack(
|
||||
["uint256", "address", "uint256", "bytes32"],
|
||||
[chainId, walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
["address", "uint256", "bytes32"],
|
||||
[walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
);
|
||||
};
|
||||
|
||||
17
contracts/clients/src/signer/getDomain.ts
Normal file
17
contracts/clients/src/signer/getDomain.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { arrayify, solidityPack } from "ethers/lib/utils";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export default (
|
||||
name: string,
|
||||
version: string,
|
||||
chainId: number,
|
||||
verificationGatewayAddress: string,
|
||||
type: string,
|
||||
): Uint8Array => {
|
||||
const encoded = solidityPack(
|
||||
["string", "string", "uint256", "address", "string"],
|
||||
[name, version, chainId, verificationGatewayAddress, type],
|
||||
);
|
||||
|
||||
return arrayify(utils.keccak256(encoded));
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { signer } from "@thehubbleproject/bls";
|
||||
|
||||
import aggregate from "./aggregate";
|
||||
import defaultDomain from "./defaultDomain";
|
||||
import getDomain from "./getDomain";
|
||||
import getPublicKey from "./getPublicKey";
|
||||
import getPublicKeyHash from "./getPublicKeyHash";
|
||||
import getPublicKeyStr from "./getPublicKeyStr";
|
||||
@@ -16,13 +16,13 @@ export * from "./conversions";
|
||||
export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
|
||||
|
||||
export async function initBlsWalletSigner({
|
||||
domain = defaultDomain,
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
}: {
|
||||
domain?: Uint8Array;
|
||||
chainId: number;
|
||||
privateKey: string;
|
||||
verificationGatewayAddress: string;
|
||||
}) {
|
||||
// Note: Getting signers via this factory ensures that mcl-wasm's underlying
|
||||
// init() has been called when signing. However, other operations such as
|
||||
@@ -32,14 +32,32 @@ export async function initBlsWalletSigner({
|
||||
// properly initialized for all use cases, not just signing.
|
||||
const signerFactory = await signer.BlsSignerFactory.new();
|
||||
|
||||
const domainName = "BLS_WALLET";
|
||||
const domainVersion = "1";
|
||||
|
||||
const bundleDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainId,
|
||||
verificationGatewayAddress,
|
||||
"Bundle",
|
||||
);
|
||||
const walletDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainId,
|
||||
verificationGatewayAddress,
|
||||
"Wallet",
|
||||
);
|
||||
|
||||
return {
|
||||
aggregate,
|
||||
getPublicKey: getPublicKey(signerFactory, domain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, domain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, domain, privateKey),
|
||||
sign: sign(signerFactory, domain, chainId, privateKey),
|
||||
signMessage: signMessage(signerFactory, domain, privateKey),
|
||||
verify: verify(domain, chainId),
|
||||
getPublicKey: getPublicKey(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, bundleDomain, privateKey),
|
||||
sign: sign(signerFactory, bundleDomain, privateKey),
|
||||
signMessage: signMessage(signerFactory, walletDomain, privateKey),
|
||||
verify: verify(bundleDomain),
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ import { Bundle, Operation } from "./types";
|
||||
export default (
|
||||
signerFactory: signer.BlsSignerFactory,
|
||||
domain: Uint8Array,
|
||||
chainId: number,
|
||||
privateKey: string,
|
||||
) =>
|
||||
(operation: Operation, walletAddress: string): Bundle => {
|
||||
const signer = signerFactory.getSigner(domain, privateKey);
|
||||
const message = encodeMessageForSigning(chainId)(operation, walletAddress);
|
||||
const message = encodeMessageForSigning()(operation, walletAddress);
|
||||
const signature = signer.sign(message);
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ActionData = {
|
||||
|
||||
export type Operation = {
|
||||
nonce: BigNumberish;
|
||||
gas: BigNumberish;
|
||||
actions: ActionData[];
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export type ActionDataDto = {
|
||||
|
||||
export type OperationDto = {
|
||||
nonce: string;
|
||||
gas: string;
|
||||
actions: ActionDataDto[];
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import encodeMessageForSigning from "./encodeMessageForSigning";
|
||||
import type { Bundle } from "./types";
|
||||
import isValidEmptyBundle from "./isValidEmptyBundle";
|
||||
|
||||
export default (domain: Uint8Array, chainId: number) =>
|
||||
(bundle: Bundle, walletAddress: string): boolean => {
|
||||
export default (domain: Uint8Array) =>
|
||||
(bundle: Bundle, walletAddresses: Array<string>): boolean => {
|
||||
// hubbleBls verifier incorrectly rejects empty bundles
|
||||
if (isValidEmptyBundle(bundle)) {
|
||||
return true;
|
||||
@@ -25,8 +25,8 @@ export default (domain: Uint8Array, chainId: number) =>
|
||||
BigNumber.from(n2).toHexString(),
|
||||
BigNumber.from(n3).toHexString(),
|
||||
]),
|
||||
bundle.operations.map((op) =>
|
||||
encodeMessageForSigning(chainId)(op, walletAddress),
|
||||
bundle.operations.map((op, i) =>
|
||||
encodeMessageForSigning()(op, walletAddresses[i]),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { parseEther } from "ethers/lib/utils";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import BlsSigner, { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { BlsProvider, BlsSigner, bundleToDto } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { initBlsWalletSigner } from "../src/signer";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
let verificationGateway: string;
|
||||
@@ -12,25 +13,25 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
let regularProvider: ethers.providers.JsonRpcProvider;
|
||||
|
||||
describe("BlsProvider", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x539, // 1337
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -63,7 +64,7 @@ describe("BlsProvider", () => {
|
||||
it("should return a new signer", async () => {
|
||||
// Arrange
|
||||
const newVerificationGateway = "newMockVerificationGatewayAddress";
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
const newBlsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
newVerificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -72,7 +73,7 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
|
||||
// Act
|
||||
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const newPrivateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
|
||||
|
||||
@@ -198,9 +199,8 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should be a provider", async () => {
|
||||
// Arrange & Act
|
||||
const isProvider = Experimental.BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider =
|
||||
Experimental.BlsProvider.isProvider(blsSigner);
|
||||
const isProvider = BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider = BlsProvider.isProvider(blsSigner);
|
||||
|
||||
// Assert
|
||||
expect(isProvider).to.equal(true);
|
||||
@@ -217,4 +217,70 @@ describe("BlsProvider", () => {
|
||||
// Assert
|
||||
expect(ready).to.deep.equal(expectedReady);
|
||||
});
|
||||
|
||||
it("should throw an error when sending multiple signed operations to sendTransaction", async () => {
|
||||
// Arrange
|
||||
const mockWalletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const { sign, aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress: verificationGateway,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const expectedAmount = parseEther("1");
|
||||
const verySafeFee = parseEther("0.1");
|
||||
const firstRecipient = ethers.Wallet.createRandom().address;
|
||||
const secondRecipient = ethers.Wallet.createRandom().address;
|
||||
|
||||
const firstActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: firstRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
const secondActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: secondRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
|
||||
const nonce = BigNumber.from(0);
|
||||
|
||||
const firstOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...firstActionWithSafeFee],
|
||||
};
|
||||
const secondOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...secondActionWithSafeFee],
|
||||
};
|
||||
|
||||
const firstBundle = sign(firstOperation, mockWalletAddress);
|
||||
const secondBundle = sign(secondOperation, mockWalletAddress);
|
||||
|
||||
const aggregatedBundle = aggregate([firstBundle, secondBundle]);
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(
|
||||
JSON.stringify(bundleToDto(aggregatedBundle)),
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.rejectedWith(
|
||||
Error,
|
||||
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import { BlsProvider, BlsSigner } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
@@ -11,23 +11,23 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
describe("BlsSigner", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539,
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -74,13 +74,13 @@ describe("BlsSigner", () => {
|
||||
|
||||
// Assert
|
||||
expect(provider._isProvider).to.be.true;
|
||||
expect(provider).to.be.instanceOf(Experimental.BlsProvider);
|
||||
expect(provider).to.be.instanceOf(BlsProvider);
|
||||
});
|
||||
|
||||
it("should detect whether an object is a valid signer", async () => {
|
||||
// Arrange & Act
|
||||
const validSigner = Experimental.BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = Experimental.BlsSigner.isSigner({});
|
||||
const validSigner = BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = BlsSigner.isSigner({});
|
||||
|
||||
// Assert
|
||||
expect(validSigner).to.be.true;
|
||||
@@ -94,4 +94,27 @@ describe("BlsSigner", () => {
|
||||
// Assert
|
||||
expect(connect).to.throw(Error, "cannot alter JSON-RPC Signer connection");
|
||||
});
|
||||
|
||||
it("should throw error for wrong chain id when validating batch options", async () => {
|
||||
// Arrange
|
||||
const invalidChainId = 123;
|
||||
const batchOptions = {
|
||||
gas: BigNumber.from("40000"),
|
||||
maxPriorityFeePerGas: ethers.utils.parseUnits("0.5", "gwei"),
|
||||
maxFeePerGas: ethers.utils.parseUnits("23", "gwei"),
|
||||
nonce: 1,
|
||||
chainId: invalidChainId,
|
||||
accessList: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsSigner._validateBatchOptions(batchOptions);
|
||||
|
||||
// Assert
|
||||
expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
`Supplied chain ID ${invalidChainId} does not match the expected chain ID 1337`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,16 +112,6 @@ describe("OperationResults", () => {
|
||||
});
|
||||
|
||||
describe("getOperationResults", () => {
|
||||
it("fails if no events are in transaction", () => {
|
||||
const txnReceipt = {
|
||||
transactionHash: "0x111111",
|
||||
} as ContractReceipt;
|
||||
|
||||
expect(() => getOperationResults(txnReceipt)).to.throw(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails if no WalletOperationProcessed events are in transaction", () => {
|
||||
const event = { event: "Other" };
|
||||
const txnReceipt = {
|
||||
@@ -222,5 +212,159 @@ describe("OperationResults", () => {
|
||||
expect(r2.results).to.deep.equal(successfulEvent.args.results);
|
||||
expect(r2.error).to.eql(undefined);
|
||||
});
|
||||
|
||||
it("decodes WalletOperationProcessed logs", () => {
|
||||
const receipt = {
|
||||
// Sometimes you get .events but WalletOperationProcessed are not
|
||||
// decoded, this helps cover that case.
|
||||
// Eg: calling blsExpanderDelegator.run
|
||||
events: [] as unknown[],
|
||||
|
||||
logs: [
|
||||
// Unrelated log
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
topics: [
|
||||
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
|
||||
"0x000000000000000000000000e619cf09e1f0eb1f9172431ec49dbef4747f8fe7",
|
||||
],
|
||||
data: "0x",
|
||||
logIndex: 0,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x00000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a977",
|
||||
],
|
||||
data: "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed4200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 4,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for failing transfer (insufficient funds)
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed42",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000645c66760100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 9,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x0000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000429d069189e000000000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a97700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 14,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
],
|
||||
} as ContractReceipt;
|
||||
|
||||
const results = getOperationResults(receipt);
|
||||
|
||||
// Helps to smooth out oddities like `error: undefined` which we don't
|
||||
// care about
|
||||
const normalizedResults = JSON.parse(JSON.stringify(results));
|
||||
|
||||
expect(normalizedResults).to.deep.eq([
|
||||
{
|
||||
walletAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x016345785d8a0000",
|
||||
},
|
||||
contractAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
{
|
||||
walletAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x1bc16d674ec80000",
|
||||
},
|
||||
contractAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
results: [
|
||||
"0x5c667601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
|
||||
],
|
||||
error: {
|
||||
actionIndex: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
message: "Unexpected action error data: 0x",
|
||||
},
|
||||
},
|
||||
{
|
||||
walletAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x0429d069189e0000",
|
||||
},
|
||||
contractAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "chai";
|
||||
import { NetworkConfig } from "../src";
|
||||
import {
|
||||
UnvalidatedMultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
@@ -7,20 +8,20 @@ import {
|
||||
const getValue = (networkKey: string, propName: string) =>
|
||||
`${networkKey}-${propName}`;
|
||||
|
||||
const getSingleConfig = (networkKey: string) => ({
|
||||
const getSingleConfig = (networkKey: string): NetworkConfig => ({
|
||||
parameters: {},
|
||||
addresses: {
|
||||
create2Deployer: getValue(networkKey, "create2Deployer"),
|
||||
safeSingletonFactory: getValue(networkKey, "safeSingletonFactory"),
|
||||
precompileCostEstimator: getValue(networkKey, "precompileCostEstimator"),
|
||||
verificationGateway: getValue(networkKey, "verificationGateway"),
|
||||
blsLibrary: getValue(networkKey, "blsLibrary"),
|
||||
blsExpander: getValue(networkKey, "blsExpander"),
|
||||
utilities: getValue(networkKey, "utilities"),
|
||||
testToken: getValue(networkKey, "testToken"),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: 123,
|
||||
domain: getValue(networkKey, "domain"),
|
||||
walletDomain: getValue(networkKey, "walletDomain"),
|
||||
bundleDomain: getValue(networkKey, "bundleDomain"),
|
||||
genesisBlock: 456,
|
||||
deployedBy: getValue(networkKey, "deployedBy"),
|
||||
version: getValue(networkKey, "version"),
|
||||
@@ -71,13 +72,6 @@ describe("MultiNetworkConfig", () => {
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network1}.addresses.blsLibrary is set to a number`, async () => {
|
||||
validConfig[network1].addresses.blsLibrary = 1337;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network2}.auxiliary.chainid is set to a string`, async () => {
|
||||
validConfig[network1].auxiliary.chainid = "off-the-chain";
|
||||
|
||||
|
||||
65
contracts/clients/test/hashBundle.test.ts
Normal file
65
contracts/clients/test/hashBundle.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expect } from "chai";
|
||||
import hashBundle from "../src/helpers/hashBundle";
|
||||
import { Bundle } from "../src";
|
||||
import { BigNumber } from "ethers";
|
||||
|
||||
describe("hashBundle", () => {
|
||||
it("should return a valid hash when provided with a valid bundle and chainId", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act
|
||||
const result = hashBundle(bundle, chainId);
|
||||
|
||||
// Assert
|
||||
expect(result).to.be.a("string");
|
||||
expect(result.length).to.equal(66);
|
||||
});
|
||||
|
||||
it("should throw an error when the number of operations does not match the number of public keys", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle1: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [["0x4321", "0x4321", "0x4321", "0x4321"]],
|
||||
};
|
||||
const bundle2: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act & Assert
|
||||
expect(() => hashBundle(bundle1, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
|
||||
expect(() => hashBundle(bundle2, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BigNumber } from "ethers";
|
||||
import { keccak256, arrayify } from "ethers/lib/utils";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { initBlsWalletSigner, Bundle, Operation } from "../src/signer";
|
||||
|
||||
import Range from "./helpers/Range";
|
||||
|
||||
const domain = arrayify(keccak256("0xfeedbee5"));
|
||||
const weiPerToken = BigNumber.from(10).pow(18);
|
||||
|
||||
const samples = (() => {
|
||||
@@ -15,9 +13,12 @@ const samples = (() => {
|
||||
// Random addresses
|
||||
const walletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const otherWalletAddress = "0x42C8157D539825daFD6586B119db53761a2a91CD";
|
||||
const verificationGatewayAddress =
|
||||
"0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
|
||||
const bundleTemplate: Operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: BigNumber.from(0),
|
||||
@@ -42,6 +43,7 @@ const samples = (() => {
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -52,22 +54,22 @@ describe("index", () => {
|
||||
|
||||
const { sign, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const bundle = sign(bundleTemplate, walletAddress);
|
||||
|
||||
expect(bundle.signature).to.deep.equal([
|
||||
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
|
||||
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
|
||||
"0x117171c1a4af03133390b454989658d9c6ae7a7fe1c3958ad545e584e63ab5e3",
|
||||
"0x2f90b24bbc03de665816b3a632e0c7b5fb837c87541d9337480671613cf1359c",
|
||||
]);
|
||||
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle, [walletAddress])).to.equal(true);
|
||||
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -77,7 +79,7 @@ describe("index", () => {
|
||||
.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadSig, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadSig, [walletAddress])).to.equal(false);
|
||||
|
||||
const bundleBadMessage: Bundle = {
|
||||
senderPublicKeys: bundle.senderPublicKeys,
|
||||
@@ -97,7 +99,7 @@ describe("index", () => {
|
||||
signature: bundle.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadMessage, [walletAddress])).to.equal(false);
|
||||
});
|
||||
|
||||
it("aggregates transactions", async () => {
|
||||
@@ -111,12 +113,12 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -125,15 +127,19 @@ describe("index", () => {
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(aggBundle.signature).to.deep.equal([
|
||||
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
|
||||
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
|
||||
"0x18b917c1f52155d9748025bd94aa07c0017af31dd2ef2a00289931f660e88ec9",
|
||||
"0x0235a99bcd1f0793efb7f3307cd349f211a433f60cfab795f5f976298f17a768",
|
||||
]);
|
||||
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle2, otherWalletAddress)).to.equal(true);
|
||||
expect(verify(bundle1, [walletAddress])).to.equal(true);
|
||||
expect(verify(bundle2, [otherWalletAddress])).to.equal(true);
|
||||
|
||||
expect(verify(bundle1, otherWalletAddress)).to.equal(false);
|
||||
expect(verify(bundle2, walletAddress)).to.equal(false);
|
||||
expect(verify(bundle1, [otherWalletAddress])).to.equal(false);
|
||||
expect(verify(bundle2, [walletAddress])).to.equal(false);
|
||||
|
||||
expect(verify(aggBundle, [walletAddress, otherWalletAddress])).to.equal(
|
||||
true,
|
||||
);
|
||||
|
||||
const aggBundleBadMessage: Bundle = {
|
||||
...aggBundle,
|
||||
@@ -154,8 +160,12 @@ describe("index", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(verify(aggBundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(aggBundleBadMessage, otherWalletAddress)).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [walletAddress, otherWalletAddress]),
|
||||
).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [otherWalletAddress, walletAddress]),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it("can aggregate transactions which already have multiple subTransactions", async () => {
|
||||
@@ -163,7 +173,7 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -186,8 +196,39 @@ describe("index", () => {
|
||||
const aggBundle2 = aggregate(bundles.slice(2, 4));
|
||||
|
||||
const aggAggBundle = aggregate([aggBundle1, aggBundle2]);
|
||||
const walletAddresses = new Array(4).fill(walletAddress);
|
||||
|
||||
expect(verify(aggAggBundle, walletAddress)).to.equal(true);
|
||||
expect(verify(aggAggBundle, walletAddresses)).to.equal(true);
|
||||
});
|
||||
|
||||
it("should fail to verify bundle with wallet address mismatches", async () => {
|
||||
const {
|
||||
bundleTemplate,
|
||||
privateKey,
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
} = samples;
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
const bundle1 = sign(bundleTemplate, walletAddress);
|
||||
const bundle2 = signWithOtherPrivateKey(bundleTemplate, otherWalletAddress);
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(verify(aggBundle, [otherWalletAddress, walletAddress])).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("generates expected publicKeyStr", async () => {
|
||||
@@ -195,7 +236,7 @@ describe("index", () => {
|
||||
|
||||
const { getPublicKeyStr } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -215,7 +256,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -230,12 +271,12 @@ describe("index", () => {
|
||||
|
||||
const { aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const emptyBundle = aggregate([]);
|
||||
|
||||
expect(verify(emptyBundle, samples.walletAddress)).to.equal(true);
|
||||
expect(verify(emptyBundle, [samples.walletAddress])).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
25
contracts/contracts/AddressRegistry.sol
Normal file
25
contracts/contracts/AddressRegistry.sol
Normal file
@@ -0,0 +1,25 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract AddressRegistry {
|
||||
mapping(uint256 => address) public addresses;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event AddressRegistered(uint256 id, address indexed addr);
|
||||
|
||||
function register(address addr) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
addresses[id] = addr;
|
||||
|
||||
emit AddressRegistered(id, addr);
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (address) {
|
||||
address addr = addresses[id];
|
||||
require(addr != address(0), "AddressRegistry: Address not found");
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ contract BLSExpander {
|
||||
function blsCallMultiCheckRewardIncrease(
|
||||
IERC20 tokenRewardAddress,
|
||||
uint256 tokenRewardAmount,
|
||||
VerificationGateway.Bundle calldata bundle
|
||||
IWallet.Bundle calldata bundle
|
||||
// uint256[4][] calldata publicKeys,
|
||||
// uint256[2] memory signature,
|
||||
// VerificationGateway.TxSet[] calldata txs
|
||||
@@ -102,6 +102,7 @@ contract BLSExpander {
|
||||
function blsCallMultiSameCallerContractFunction(
|
||||
uint256[4] calldata publicKey,
|
||||
uint256 nonce,
|
||||
uint256 gas,
|
||||
uint256[2] calldata signature,
|
||||
address contractAddress,
|
||||
bytes4 methodId,
|
||||
@@ -109,7 +110,7 @@ contract BLSExpander {
|
||||
) external {
|
||||
uint256 length = encodedParamSets.length;
|
||||
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = signature;
|
||||
|
||||
bundle.senderPublicKeys = new uint256[4][](1);
|
||||
@@ -117,6 +118,7 @@ contract BLSExpander {
|
||||
|
||||
bundle.operations = new IWallet.Operation[](1);
|
||||
bundle.operations[0].nonce = nonce;
|
||||
bundle.operations[0].gas = gas;
|
||||
bundle.operations[0].actions = new IWallet.ActionData[](length);
|
||||
for (uint256 i=0; i<length; i++) {
|
||||
bundle.operations[0].actions[i].ethValue = 0;
|
||||
@@ -185,7 +187,7 @@ contract BLSExpander {
|
||||
}
|
||||
|
||||
// Use them to re-create bundle
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = addressBundle.signature;
|
||||
bundle.senderPublicKeys = senderPublicKeys;
|
||||
bundle.operations = addressBundle.operations;
|
||||
|
||||
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
@@ -0,0 +1,94 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./lib/VLQ.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
|
||||
// Aka gateway, but we only care about processBundle here
|
||||
interface IBundleProcessor {
|
||||
function processBundle(
|
||||
IWallet.Bundle memory bundle
|
||||
) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
);
|
||||
}
|
||||
|
||||
contract BLSExpanderDelegator {
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBundleProcessor gateway;
|
||||
|
||||
uint256 public nextExpanderId = 0;
|
||||
mapping(uint256 => IExpander) public expanders;
|
||||
event ExpanderRegistered(uint256 id, IExpander indexed expanderAddress);
|
||||
|
||||
constructor(IBundleProcessor gatewayParam) {
|
||||
gateway = gatewayParam;
|
||||
}
|
||||
|
||||
function run(bytes calldata stream) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
) {
|
||||
IWallet.Bundle memory bundle;
|
||||
|
||||
// The signature is just the last 64 bytes.
|
||||
uint256 opsByteLen = stream.length - 64;
|
||||
bundle.signature = abi.decode(stream[opsByteLen:], (uint256[2]));
|
||||
stream = stream[:opsByteLen]; // Only keep what we still need
|
||||
|
||||
uint256 vlqValue;
|
||||
|
||||
// Get the number of operations upfront so that we can allocate the
|
||||
// memory. This information is technically redundant but extracting it
|
||||
// by decoding everything before we have a place to put it would add a
|
||||
// lot of complexity. For <=127 operations it's only one extra byte.
|
||||
// Otherwise 2 bytes.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
|
||||
bundle.senderPublicKeys = new uint256[BLS_KEY_LEN][](vlqValue);
|
||||
bundle.operations = new IWallet.Operation[](vlqValue);
|
||||
|
||||
uint256 opsDecoded = 0;
|
||||
|
||||
while (stream.length > 0) {
|
||||
// First figure out which expander to use.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
IExpander expander = expanders[vlqValue];
|
||||
require(expander != IExpander(address(0)), "expander not found");
|
||||
|
||||
// Then use it to expand the operation.
|
||||
(
|
||||
uint256[BLS_KEY_LEN] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 expanderBytesRead
|
||||
) = expander.expand(stream);
|
||||
|
||||
// It would be more consistent to have .expand above return the new
|
||||
// stream, but there appears to be a bug in solidity that prevents
|
||||
// this.
|
||||
stream = stream[expanderBytesRead:];
|
||||
|
||||
bundle.senderPublicKeys[opsDecoded] = senderPublicKey;
|
||||
bundle.operations[opsDecoded] = operation;
|
||||
|
||||
opsDecoded++;
|
||||
}
|
||||
|
||||
// Finished expanding. Now just return the call.
|
||||
return gateway.processBundle(bundle);
|
||||
}
|
||||
|
||||
function registerExpander(
|
||||
IExpander expander
|
||||
) external {
|
||||
uint256 expanderId = nextExpanderId;
|
||||
nextExpanderId += 1;
|
||||
expanders[expanderId] = expander;
|
||||
|
||||
emit ExpanderRegistered(expanderId, expander);
|
||||
}
|
||||
}
|
||||
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
@@ -0,0 +1,35 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract BLSPublicKeyRegistry {
|
||||
mapping(uint256 => uint256[4]) public blsPublicKeys;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event BLSPublicKeyRegistered(uint256 id, bytes32 indexed blsPublicKeyHash);
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
blsPublicKeys[id] = blsPublicKey;
|
||||
|
||||
emit BLSPublicKeyRegistered(id, keccak256(abi.encode(blsPublicKey)));
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (uint256[4] memory) {
|
||||
uint256[4] memory blsPublicKey = blsPublicKeys[id];
|
||||
require(!isZeroBLSPublicKey(blsPublicKey), "BLSPublicKeyRegistry: BLS public key not found");
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
function isZeroBLSPublicKey(uint256[4] memory blsPublicKey) internal pure returns (bool) {
|
||||
for (uint i = 0; i < 4; i++) {
|
||||
if (blsPublicKey[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
81
contracts/contracts/BLSRegistration.sol
Normal file
81
contracts/contracts/BLSRegistration.sol
Normal file
@@ -0,0 +1,81 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AddressRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
|
||||
contract BLSRegistration is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
|
||||
uint256 nonce;
|
||||
(nonce, stream) = VLQ.decode(stream);
|
||||
|
||||
uint256 gas;
|
||||
(gas, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 txOriginPayment;
|
||||
(txOriginPayment, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 actionLen = 1;
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
actionLen += 1;
|
||||
}
|
||||
|
||||
operation = IWallet.Operation({
|
||||
nonce: nonce,
|
||||
gas: gas,
|
||||
actions: new IWallet.ActionData[](actionLen)
|
||||
});
|
||||
|
||||
operation.actions[0] = IWallet.ActionData({
|
||||
ethValue: 0,
|
||||
contractAddress: address(this),
|
||||
encodedFunction: abi.encodeWithSignature(
|
||||
"register(uint256[4])",
|
||||
senderPublicKey
|
||||
)
|
||||
});
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
operation.actions[1] = IWallet.ActionData({
|
||||
ethValue: txOriginPayment,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
blsPublicKeyRegistry.register(blsPublicKey);
|
||||
addressRegistry.register(msg.sender);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ pragma abicoder v2;
|
||||
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
interface IVerificationGateway {
|
||||
function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bool);
|
||||
}
|
||||
|
||||
/** Minimal upgradable smart contract wallet.
|
||||
Generic calls can only be requested by its trusted gateway.
|
||||
@@ -194,6 +197,22 @@ contract BLSWallet is Initializable, IWallet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev ERC-1271 signature validation
|
||||
*/
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) public view returns (bytes4 magicValue) {
|
||||
bool verified = IVerificationGateway(trustedBLSGateway).isValidSignature(hash, signature);
|
||||
|
||||
if (verified) {
|
||||
magicValue = 0x1626ba7e;
|
||||
} else {
|
||||
magicValue = 0xffffffff;
|
||||
}
|
||||
}
|
||||
|
||||
function stripMethodId(bytes memory encodedFunction) pure private returns(bytes memory) {
|
||||
bytes memory params = new bytes(encodedFunction.length - 4);
|
||||
for (uint256 i=0; i<params.length; i++) {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
|
||||
contract Create2Deployer {
|
||||
event Deployed(address sender, uint256 salt, address addr);
|
||||
|
||||
function addressFrom(
|
||||
address sender,
|
||||
uint256 salt,
|
||||
bytes calldata code
|
||||
) public pure returns (
|
||||
address predictedAddress
|
||||
) {
|
||||
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
sender,
|
||||
salt,
|
||||
keccak256(code)
|
||||
)))));
|
||||
}
|
||||
|
||||
function deploy(
|
||||
uint256 salt,
|
||||
bytes memory code
|
||||
) public {
|
||||
address addr;
|
||||
assembly {
|
||||
addr := create2(0, add(code, 0x20), mload(code), salt)
|
||||
if iszero(extcodesize(addr)) {
|
||||
revert(0, 0)
|
||||
}
|
||||
}
|
||||
emit Deployed(msg.sender, salt, addr);
|
||||
}
|
||||
|
||||
}
|
||||
316
contracts/contracts/ERC20Expander.sol
Normal file
316
contracts/contracts/ERC20Expander.sol
Normal file
@@ -0,0 +1,316 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./AddressRegistry.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/RegIndex.sol";
|
||||
import "./lib/VLQ.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
/**
|
||||
* An expander that supports ERC20 operations.
|
||||
*
|
||||
* This is a bit of a first pass, it could be more compact:
|
||||
* - Optimize specifically for transfer
|
||||
* - Require registries, removing the need for a bit stream
|
||||
* - Use a fixed gas value
|
||||
* - Use a dedicated ERC20 registry, allowing a single byte for popular
|
||||
* currencies (and 2 bytes for all but the most obscure currencies)
|
||||
* - Use a fixed tx.origin reward (or call contract that computes an appropriate
|
||||
* reward)
|
||||
*
|
||||
* This would get us down to about 11 bytes. (Current example is 20 bytes.)
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 0f - 0x0f = 0b1111 bit stream:
|
||||
* - 1: Use registry for BLS key
|
||||
* - 1: Include a tx.origin payment
|
||||
* - 1: Use registry for ERC20 address
|
||||
* - 1: Use registry for recipient address
|
||||
* 000000 - Registry index for sendWallet's public key
|
||||
* 00 - nonce: 0
|
||||
* 0bda28 - gas: 92,483
|
||||
* 02 - two actions
|
||||
* 000000 - Registry index for ERC20 address
|
||||
* 00 - transfer
|
||||
* 000002 - Registry index for recipient address
|
||||
* 9100 - 0.1 MCK (amount/value)
|
||||
* 6d00 - Pay 0.000005 ETH to tx.origin
|
||||
*/
|
||||
contract ERC20Expander is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
uint256 bitStream;
|
||||
|
||||
(bitStream, stream) = VLQ.decode(stream);
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(
|
||||
bitStream
|
||||
);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
senderPublicKey = blsPublicKeyRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
operation.nonce = decodedValue;
|
||||
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
operation.gas = decodedValue;
|
||||
|
||||
uint256 actionLen;
|
||||
(actionLen, stream) = VLQ.decode(stream);
|
||||
operation.actions = new IWallet.ActionData[](actionLen);
|
||||
|
||||
// hasTxOriginPayment
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
// We would use a separate variable for this, but the solidity
|
||||
// compiler makes it important to minimize local variables.
|
||||
actionLen -= 1;
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < actionLen; i++) {
|
||||
(
|
||||
operation.actions[i].contractAddress,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeAddress(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
|
||||
(
|
||||
operation.actions[i].encodedFunction,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeFunctionCall(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
if (actionLen < operation.actions.length) {
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
operation.actions[actionLen] = IWallet.ActionData({
|
||||
ethValue: decodedValue,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function decodeBit(uint256 bitStream) internal pure returns (bool, uint256) {
|
||||
return ((bitStream & 1) == 1, bitStream >> 1);
|
||||
}
|
||||
|
||||
// Following the naming convention this would be called
|
||||
// decodeEncodedFunction, but that's pretty confusing.
|
||||
function decodeFunctionCall(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 methodIndex;
|
||||
(methodIndex, stream) = VLQ.decode(stream);
|
||||
|
||||
if (methodIndex == 0) {
|
||||
return decodeTransfer(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 1) {
|
||||
return decodeTransferFrom(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 2) {
|
||||
return decodeApprove(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 3) {
|
||||
// Not a real method, but uint256Max is common for approve, and is
|
||||
// not represented efficiently by PseudoFloat.
|
||||
return decodeApproveMax(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 4) {
|
||||
return decodeMint(stream, bitStream);
|
||||
}
|
||||
|
||||
revert("Unrecognized ERC20 method index");
|
||||
}
|
||||
|
||||
function decodeTransfer(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("transfer(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeTransferFrom(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address from;
|
||||
(from, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"transferFrom(address,address,uint256)",
|
||||
from,
|
||||
to,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApprove(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApproveMax(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
type(uint256).max
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeMint(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("mint(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeAddress(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
address,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
return (addressRegistry.lookup(decodedValue), stream, bitStream);
|
||||
}
|
||||
|
||||
return (address(bytes20(stream[:20])), stream[20:], bitStream);
|
||||
}
|
||||
}
|
||||
24
contracts/contracts/ExpanderEntryPoint.sol
Normal file
24
contracts/contracts/ExpanderEntryPoint.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
interface IExpanderDelegator {
|
||||
function run(
|
||||
bytes calldata stream
|
||||
) external returns (bool[] memory successes, bytes[][] memory results);
|
||||
}
|
||||
|
||||
contract ExpanderEntryPoint {
|
||||
IExpanderDelegator expanderDelegator;
|
||||
|
||||
constructor(IExpanderDelegator expanderDelegatorParam) {
|
||||
expanderDelegator = expanderDelegatorParam;
|
||||
}
|
||||
|
||||
fallback(bytes calldata stream) external returns (bytes memory) {
|
||||
(bool[] memory successes, bytes[][] memory results) = expanderDelegator
|
||||
.run{ gas: type(uint256).max }(stream);
|
||||
|
||||
return abi.encode(successes, results);
|
||||
}
|
||||
}
|
||||
151
contracts/contracts/FallbackExpander.sol
Normal file
151
contracts/contracts/FallbackExpander.sol
Normal file
@@ -0,0 +1,151 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./AddressRegistry.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/RegIndex.sol";
|
||||
import "./lib/VLQ.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
/**
|
||||
* An expander that supports any operation.
|
||||
*
|
||||
* This is still a more compact encoding due to the use of VLQ and general byte
|
||||
* packing in several places where the solidity abi would just use a 32-byte
|
||||
* word.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 0x
|
||||
* 2409925687d52a67b435a011cf9ec82d390300cd12e5842d2a0c5e1c27898551 // BLS key
|
||||
* 0c4a8cbcc96cada40301e1d2a2d68425b5cf0e18f5cb12fa272f841017c36776 // BLS key
|
||||
* 27b9f42b237d75bcb0473e2eada290e62ec77048187484f8952fffe0239f7ba9 // BLS key
|
||||
* 24f1fc8a1f7256dc2914e524966309df2226fd329373aaaae1881bf5cd0c62f4 // BLS key
|
||||
*
|
||||
* 00 // nonce: 0
|
||||
* 3100 // gas: 100,000
|
||||
* 02 // two actions
|
||||
*
|
||||
* // Action 1
|
||||
* 7b0f // ethValue: 12300000000000000 (0.0123 ETH)
|
||||
* 70997970c51812dc3a010c7d01b50e0d17dc79c8 // contractAddress
|
||||
* 00 // encodedFunction: (empty)
|
||||
*
|
||||
* // Action 2
|
||||
* 6c01 // ethValue: 12000000000000 (0.000012 ETH)
|
||||
* 4bd2e4e99b50a2a9e6b9dabfa3c8dcd1f885f008 // contractAddress (AggUtils)
|
||||
* 04 // 4 bytes for encodedFunction
|
||||
* 1dfea6a0 // sendEthToTxOrigin
|
||||
*
|
||||
* The proposal doc for the new expander lists the same example ("Example of an
|
||||
* Expanded User Operation" https://hackmd.io/0q7H3Ad0Su-I4RWWK8wQPA) using the
|
||||
* solidity ABI, which uses 608 bytes. Here we've encoded the same thing (plus
|
||||
* gas) in 182 bytes, which is 70% smaller. (If you account for the zero-byte
|
||||
* discount, the saving is still over 30%.)
|
||||
*/
|
||||
contract FallbackExpander is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
uint256 bitStream;
|
||||
|
||||
(bitStream, stream) = VLQ.decode(stream);
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(
|
||||
bitStream
|
||||
);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
senderPublicKey = blsPublicKeyRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
operation.nonce = decodedValue;
|
||||
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
operation.gas = decodedValue;
|
||||
|
||||
uint256 actionLen;
|
||||
(actionLen, stream) = VLQ.decode(stream);
|
||||
operation.actions = new IWallet.ActionData[](actionLen);
|
||||
|
||||
// hasTxOriginPayment
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
// We would use a separate variable for this, but the solidity
|
||||
// compiler makes it important to minimize local variables.
|
||||
actionLen -= 1;
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < actionLen; i++) {
|
||||
uint256 ethValue;
|
||||
(ethValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
address contractAddress;
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
contractAddress = addressRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
contractAddress = address(bytes20(stream[:20]));
|
||||
stream = stream[20:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
bytes memory encodedFunction = stream[:decodedValue];
|
||||
stream = stream[decodedValue:];
|
||||
|
||||
operation.actions[i] = IWallet.ActionData({
|
||||
ethValue: ethValue,
|
||||
contractAddress: contractAddress,
|
||||
encodedFunction: encodedFunction
|
||||
});
|
||||
}
|
||||
|
||||
if (actionLen < operation.actions.length) {
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
operation.actions[actionLen] = IWallet.ActionData({
|
||||
ethValue: decodedValue,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function decodeBit(uint256 bitStream) internal pure returns (bool, uint256) {
|
||||
return ((bitStream & 1) == 1, bitStream >> 1);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
|
||||
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
||||
|
||||
import "./interfaces/IWallet.sol";
|
||||
import "./BLSWallet.sol";
|
||||
|
||||
/**
|
||||
A non-upgradable gateway used to create BLSWallets and call them with
|
||||
@@ -18,28 +19,24 @@ is the calling wallet's address.
|
||||
*/
|
||||
contract VerificationGateway
|
||||
{
|
||||
/** Domain chosen arbitrarily */
|
||||
bytes32 BLS_DOMAIN = keccak256(abi.encodePacked(uint32(0xfeedbee5)));
|
||||
bytes32 WALLET_DOMAIN;
|
||||
bytes32 BUNDLE_DOMAIN;
|
||||
string constant BLS_DOMAIN_NAME = "BLS_WALLET";
|
||||
string constant BLS_DOMAIN_VERSION = "1";
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBLS public immutable blsLib;
|
||||
ProxyAdmin public immutable walletProxyAdmin;
|
||||
address public immutable blsWalletLogic;
|
||||
ProxyAdmin public immutable walletProxyAdmin = new ProxyAdmin();
|
||||
BLSWallet public immutable blsWalletLogic = new BLSWallet();
|
||||
mapping(bytes32 => IWallet) public walletFromHash;
|
||||
mapping(IWallet => bytes32) public hashFromWallet;
|
||||
mapping(bytes32 => uint256[BLS_KEY_LEN]) public BLSPublicKeyFromHash;
|
||||
|
||||
//mapping from an existing wallet's bls key hash to pending variables when setting a new BLS key
|
||||
// mapping from an existing wallet's bls key hash to pending variables when setting a new BLS key
|
||||
mapping(bytes32 => uint256[BLS_KEY_LEN]) public pendingBLSPublicKeyFromHash;
|
||||
mapping(bytes32 => uint256[2]) public pendingMessageSenderSignatureFromHash;
|
||||
mapping(bytes32 => uint256) public pendingBLSPublicKeyTimeFromHash;
|
||||
|
||||
/** Aggregated signature with corresponding senders + operations */
|
||||
struct Bundle {
|
||||
uint256[2] signature;
|
||||
uint256[BLS_KEY_LEN][] senderPublicKeys;
|
||||
IWallet.Operation[] operations;
|
||||
}
|
||||
|
||||
event WalletCreated(
|
||||
address indexed wallet,
|
||||
uint256[BLS_KEY_LEN] publicKey
|
||||
@@ -65,24 +62,33 @@ contract VerificationGateway
|
||||
/**
|
||||
@param bls verified bls library contract address
|
||||
*/
|
||||
constructor(
|
||||
IBLS bls,
|
||||
address blsWalletImpl,
|
||||
address proxyAdmin
|
||||
) {
|
||||
constructor(IBLS bls) {
|
||||
blsLib = bls;
|
||||
blsWalletLogic = blsWalletImpl;
|
||||
walletProxyAdmin = ProxyAdmin(proxyAdmin);
|
||||
blsWalletLogic.initialize(address(0));
|
||||
WALLET_DOMAIN = keccak256(abi.encodePacked(
|
||||
BLS_DOMAIN_NAME,
|
||||
BLS_DOMAIN_VERSION,
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Wallet"
|
||||
));
|
||||
BUNDLE_DOMAIN = keccak256(abi.encodePacked(
|
||||
BLS_DOMAIN_NAME,
|
||||
BLS_DOMAIN_VERSION,
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Bundle"
|
||||
));
|
||||
}
|
||||
|
||||
/** Throw if bundle not valid or signature verification fails */
|
||||
function verify(
|
||||
Bundle memory bundle
|
||||
IWallet.Bundle memory bundle
|
||||
) public view {
|
||||
uint256 opLength = bundle.operations.length;
|
||||
require(
|
||||
opLength == bundle.senderPublicKeys.length,
|
||||
"VG: Sender/op length mismatch"
|
||||
"VG: length mismatch"
|
||||
);
|
||||
uint256[2][] memory messages = new uint256[2][](opLength);
|
||||
|
||||
@@ -91,7 +97,7 @@ contract VerificationGateway
|
||||
bytes32 keyHash = keccak256(abi.encodePacked(bundle.senderPublicKeys[i]));
|
||||
address walletAddress = address(walletFromHash[keyHash]);
|
||||
if (walletAddress == address(0)) {
|
||||
walletAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
walletAddress = address(uint160(uint256(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
address(this),
|
||||
keyHash,
|
||||
@@ -120,6 +126,29 @@ contract VerificationGateway
|
||||
require(verified, "VG: Sig not verified");
|
||||
}
|
||||
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) public view returns (bool verified) {
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
require(existingHash != 0, "VG: not called from wallet");
|
||||
|
||||
uint256[BLS_KEY_LEN] memory publicKey = BLSPublicKeyFromHash[existingHash];
|
||||
|
||||
bytes memory hashBytes = abi.encode(hash);
|
||||
|
||||
uint256[2] memory message = blsLib.hashToPoint(
|
||||
WALLET_DOMAIN,
|
||||
hashBytes
|
||||
);
|
||||
|
||||
require(signature.length == 64, "VG: Sig bytes length must be 64");
|
||||
uint256[2] memory decodedSignature = abi.decode(signature, (uint256[2]));
|
||||
|
||||
verified = blsLib.verifySingle(decodedSignature, publicKey, message);
|
||||
}
|
||||
|
||||
/**
|
||||
If an existing wallet contract wishes to be called by this verification
|
||||
gateway, it can directly register itself with a simple signed msg.
|
||||
@@ -128,17 +157,19 @@ contract VerificationGateway
|
||||
@dev overrides previous wallet address registered with the given public key
|
||||
@param messageSenderSignature signature of message containing only the calling address
|
||||
@param publicKey that signed the caller's address
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function setBLSKeyForWallet(
|
||||
uint256[2] memory messageSenderSignature,
|
||||
uint256[BLS_KEY_LEN] memory publicKey
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) public {
|
||||
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: publicKey must be non-zero");
|
||||
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: key is zero");
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
if (existingHash == bytes32(0)) { // wallet does not yet have a bls key registered with this gateway
|
||||
// set it instantly
|
||||
safeSetWallet(messageSenderSignature, publicKey, wallet);
|
||||
safeSetWallet(messageSenderSignature, publicKey, wallet, signatureExpiryTimestamp);
|
||||
}
|
||||
else { // wallet already has a key registered, set after delay
|
||||
pendingMessageSenderSignatureFromHash[existingHash] = messageSenderSignature;
|
||||
@@ -147,11 +178,13 @@ contract VerificationGateway
|
||||
emit PendingBLSKeySet(existingHash, publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
function setPendingBLSKeyForWallet() public {
|
||||
/**
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function setPendingBLSKeyForWallet(uint256 signatureExpiryTimestamp) public {
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
require(existingHash != bytes32(0), "VG: hash does not exist for caller");
|
||||
require(existingHash != bytes32(0), "VG: hash not found");
|
||||
if (
|
||||
(pendingBLSPublicKeyTimeFromHash[existingHash] != 0) &&
|
||||
(block.timestamp > pendingBLSPublicKeyTimeFromHash[existingHash])
|
||||
@@ -159,7 +192,8 @@ contract VerificationGateway
|
||||
safeSetWallet(
|
||||
pendingMessageSenderSignatureFromHash[existingHash],
|
||||
pendingBLSPublicKeyFromHash[existingHash],
|
||||
wallet
|
||||
wallet,
|
||||
signatureExpiryTimestamp
|
||||
);
|
||||
pendingMessageSenderSignatureFromHash[existingHash] = [0,0];
|
||||
pendingBLSPublicKeyTimeFromHash[existingHash] = 0;
|
||||
@@ -196,7 +230,7 @@ contract VerificationGateway
|
||||
for (uint256 i=0; i<32; i++) {
|
||||
require(
|
||||
(encodedFunction[selectorOffset+i] == encodedAddress[i]),
|
||||
"VG: first param to proxy admin is not calling wallet"
|
||||
"VG: first param is not wallet"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -226,19 +260,21 @@ contract VerificationGateway
|
||||
@param blsKeyHash calling wallet's bls public key hash
|
||||
@param salt used in the recovery hash
|
||||
@param newBLSKey to set as the wallet's bls public key
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function recoverWallet(
|
||||
uint256[2] memory walletAddressSignature,
|
||||
bytes32 blsKeyHash,
|
||||
bytes32 salt,
|
||||
uint256[BLS_KEY_LEN] memory newBLSKey
|
||||
uint256[BLS_KEY_LEN] memory newBLSKey,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) public {
|
||||
IWallet wallet = walletFromHash[blsKeyHash];
|
||||
bytes32 recoveryHash = keccak256(
|
||||
abi.encodePacked(msg.sender, blsKeyHash, salt)
|
||||
);
|
||||
if (recoveryHash == wallet.recoveryHash()) {
|
||||
safeSetWallet(walletAddressSignature, newBLSKey, wallet);
|
||||
safeSetWallet(walletAddressSignature, newBLSKey, wallet, signatureExpiryTimestamp);
|
||||
wallet.recover();
|
||||
}
|
||||
}
|
||||
@@ -255,7 +291,7 @@ contract VerificationGateway
|
||||
assembly { size := extcodesize(blsGateway) }
|
||||
require(
|
||||
(blsGateway != address(0)) && (size > 0),
|
||||
"BLSWallet: gateway address param not valid"
|
||||
"VG: invalid gateway"
|
||||
);
|
||||
|
||||
IWallet wallet = walletFromHash[hash];
|
||||
@@ -281,7 +317,7 @@ contract VerificationGateway
|
||||
Can be called with a single operation with no actions.
|
||||
*/
|
||||
function processBundle(
|
||||
Bundle memory bundle
|
||||
IWallet.Bundle memory bundle
|
||||
) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
@@ -296,12 +332,12 @@ contract VerificationGateway
|
||||
IWallet wallet = getOrCreateWallet(bundle.senderPublicKeys[i]);
|
||||
|
||||
// check nonce then perform action
|
||||
if (bundle.operations[i].nonce == wallet.nonce()) {
|
||||
if (bundle.operations[i].nonce == wallet.nonce{gas:20000}()) {
|
||||
// request wallet perform operation
|
||||
(
|
||||
bool success,
|
||||
bytes[] memory resultSet
|
||||
) = wallet.performOperation(bundle.operations[i]);
|
||||
) = wallet.performOperation{gas:bundle.operations[i].gas}(bundle.operations[i]);
|
||||
successes[i] = success;
|
||||
results[i] = resultSet;
|
||||
emit WalletOperationProcessed(
|
||||
@@ -331,7 +367,7 @@ contract VerificationGateway
|
||||
address(walletProxyAdmin),
|
||||
getInitializeData()
|
||||
)));
|
||||
updateWalletHashMappings(publicKeyHash, blsWallet);
|
||||
updateWalletHashMappings(publicKeyHash, blsWallet, publicKey);
|
||||
emit WalletCreated(
|
||||
address(blsWallet),
|
||||
publicKey
|
||||
@@ -340,45 +376,78 @@ contract VerificationGateway
|
||||
return IWallet(blsWallet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for measuring the gas used by an operation, suitable for use in
|
||||
* the operation's required gas parameter.
|
||||
*
|
||||
* This has two important differences over standard estimateGas methods:
|
||||
* 1. It does not include gas for calldata, which has already been paid
|
||||
* 2. It works for wallets which don't yet exist
|
||||
*/
|
||||
function measureOperationGas(
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
IWallet.Operation calldata op
|
||||
) external returns (uint256) {
|
||||
// Don't allow this to actually be executed on chain. Static calls only.
|
||||
require(msg.sender == address(0), "VG: read only");
|
||||
|
||||
IWallet wallet = getOrCreateWallet(publicKey);
|
||||
|
||||
uint256 gasBefore = gasleft();
|
||||
wallet.performOperation(op);
|
||||
uint256 gasUsed = gasBefore - gasleft();
|
||||
|
||||
return gasUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev safely sets/overwrites the wallet for the given public key, ensuring it is properly signed
|
||||
@param wallletAddressSignature signature of message containing only the wallet address
|
||||
@param walletAddressSignature signature of message containing only the wallet address
|
||||
@param publicKey that signed the wallet address
|
||||
@param wallet address to set
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function safeSetWallet(
|
||||
uint256[2] memory wallletAddressSignature,
|
||||
uint256[2] memory walletAddressSignature,
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
IWallet wallet
|
||||
IWallet wallet,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) private {
|
||||
require(
|
||||
block.timestamp < signatureExpiryTimestamp,
|
||||
"VG: message expired"
|
||||
);
|
||||
// verify the given wallet was signed for by the bls key
|
||||
uint256[2] memory addressMsg = blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
abi.encodePacked(wallet)
|
||||
WALLET_DOMAIN,
|
||||
abi.encodePacked(wallet, signatureExpiryTimestamp)
|
||||
);
|
||||
require(
|
||||
blsLib.verifySingle(wallletAddressSignature, publicKey, addressMsg),
|
||||
"VG: Signature not verified for wallet address."
|
||||
blsLib.verifySingle(walletAddressSignature, publicKey, addressMsg),
|
||||
"VG: Sig not verified"
|
||||
);
|
||||
bytes32 publicKeyHash = keccak256(abi.encodePacked(
|
||||
publicKey
|
||||
));
|
||||
emit BLSKeySetForWallet(publicKey, wallet);
|
||||
updateWalletHashMappings(publicKeyHash, wallet);
|
||||
updateWalletHashMappings(publicKeyHash, wallet, publicKey);
|
||||
}
|
||||
|
||||
/** @dev Only to be called on wallet creation, and in `safeSetWallet` */
|
||||
function updateWalletHashMappings(
|
||||
bytes32 publicKeyHash,
|
||||
IWallet wallet
|
||||
IWallet wallet,
|
||||
uint256[BLS_KEY_LEN] memory publicKey
|
||||
) private {
|
||||
// remove reference from old hash
|
||||
bytes32 oldHash = hashFromWallet[wallet];
|
||||
walletFromHash[oldHash] = IWallet(address(0));
|
||||
BLSPublicKeyFromHash[oldHash] = [0,0,0,0];
|
||||
|
||||
// update new hash / wallet mappings
|
||||
walletFromHash[publicKeyHash] = wallet;
|
||||
hashFromWallet[wallet] = publicKeyHash;
|
||||
BLSPublicKeyFromHash[publicKeyHash] = publicKey;
|
||||
}
|
||||
|
||||
function getInitializeData() private view returns (bytes memory) {
|
||||
@@ -411,9 +480,8 @@ contract VerificationGateway
|
||||
);
|
||||
}
|
||||
return blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
BUNDLE_DOMAIN,
|
||||
abi.encodePacked(
|
||||
block.chainid,
|
||||
walletAddress,
|
||||
op.nonce,
|
||||
keccak256(encodedActionData)
|
||||
|
||||
13
contracts/contracts/interfaces/IExpander.sol
Normal file
13
contracts/contracts/interfaces/IExpander.sol
Normal file
@@ -0,0 +1,13 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./IWallet.sol";
|
||||
|
||||
interface IExpander {
|
||||
function expand(bytes calldata stream) external returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,16 @@ pragma abicoder v2;
|
||||
/** Interface for a contract wallet that can perform Operations
|
||||
*/
|
||||
interface IWallet {
|
||||
/** Aggregated signature with corresponding senders + operations */
|
||||
struct Bundle {
|
||||
uint256[2] signature;
|
||||
uint256[4][] senderPublicKeys;
|
||||
IWallet.Operation[] operations;
|
||||
}
|
||||
|
||||
struct Operation {
|
||||
uint256 nonce;
|
||||
uint256 gas;
|
||||
IWallet.ActionData[] actions;
|
||||
}
|
||||
|
||||
|
||||
117
contracts/contracts/lib/PseudoFloat.sol
Normal file
117
contracts/contracts/lib/PseudoFloat.sol
Normal file
@@ -0,0 +1,117 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./VLQ.sol";
|
||||
|
||||
/**
|
||||
* Like a float, but technically for integers. Also base 10.
|
||||
*
|
||||
* The pseudo-float is an encoding that can represent any uint256 value but
|
||||
* efficiently represents values with a small number of significant figures
|
||||
* (just 2 bytes for 3 significant figures).
|
||||
*
|
||||
* Zero is a special case, it's just 0x00.
|
||||
*
|
||||
* Otherwise, start with the value in scientific notation:
|
||||
*
|
||||
* 1.23 * 10^16 (e.g. 0.0123 ETH)
|
||||
*
|
||||
* Make the mantissa (1.23) a whole number by adjusting the exponent:
|
||||
*
|
||||
* 123 * 10^14
|
||||
*
|
||||
* We add 1 to the exponent and encode it in 5 bits:
|
||||
*
|
||||
* 01111 (=15)
|
||||
*
|
||||
* Note: The maximum value we can encode here is 31 (11111). This means the
|
||||
* maximum exponent is 30. Adjust the left side of the previous equation if
|
||||
* needed.
|
||||
*
|
||||
* Encode the left side in binary:
|
||||
*
|
||||
* 1111011 (=123)
|
||||
*
|
||||
* Our first byte is the 5-bit exponent followed by the three lowest bits of the
|
||||
* mantissa:
|
||||
*
|
||||
* 01111011
|
||||
* ^^^^^-------- 15 => exponent is 14
|
||||
* ^^^----- lowest 3 bits of the mantissa
|
||||
*
|
||||
* Encode the remaining bits of the mantissa as a VLQ:
|
||||
*
|
||||
* 00001111
|
||||
* ^------------ special VLQ bit, zero indicates this is the last byte
|
||||
* ^^^^^^^----- bits to use, put them together with 011 above to get
|
||||
* 0001111011, which is 123.
|
||||
*
|
||||
* Putting it together is two bytes:
|
||||
*
|
||||
* 0x7b0f
|
||||
*
|
||||
* Example 2:
|
||||
*
|
||||
* 0.883887085 ETH uses 5 bytes: 0x55b4d7c27d
|
||||
* 883887085 * 10^9
|
||||
* For exponent 9 we encode 10 as 5 bits: 01010
|
||||
* 883887085 is 110100101011110000101111101(101)
|
||||
*
|
||||
* 01010101 10110100 11010111 11000010 01111101
|
||||
* ^^^^^------------------------------------------- 10 => exponent is 9
|
||||
* ^^^---------------------------------------- lowest 3 bits
|
||||
* ^^^^^^^--^^^^^^^--^^^^^^^--^^^^^^^ higher bits
|
||||
* ^--------^--------^-------------------- 1 => not the last byte
|
||||
* ^----------- 0 => the last byte
|
||||
*
|
||||
* Note that the *encode* process is described above for explanatory purposes.
|
||||
* On-chain we need to *decode* to recover the value from the encoded binary
|
||||
* instead.
|
||||
*/
|
||||
library PseudoFloat {
|
||||
function decode(
|
||||
bytes calldata stream
|
||||
) internal pure returns (uint256, bytes calldata) {
|
||||
uint8 firstByte = uint8(stream[0]);
|
||||
|
||||
if (firstByte == 0) {
|
||||
return (0, stream[1:]);
|
||||
}
|
||||
|
||||
uint8 exponent = ((firstByte & 0xf8) >> 3) - 1;
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = VLQ.decode(stream[1:]);
|
||||
|
||||
value <<= 3;
|
||||
value += firstByte & 0x07;
|
||||
|
||||
// TODO (merge-ok): Exponentiation by squaring might be better here.
|
||||
// Counterpoints:
|
||||
// - The gas used is pretty low anyway
|
||||
// - For these low exponents (typically ~15), the benefit is unclear
|
||||
for (uint256 i = 0; i < exponent; i++) {
|
||||
value *= 10;
|
||||
}
|
||||
|
||||
return (value, stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as decode, but public.
|
||||
*
|
||||
* This is here because when a library function that is not internal
|
||||
* requires linking when used in other contracts. This avoids including a
|
||||
* copy of that function in the contract but it's complexity that we don't
|
||||
* want right now.
|
||||
*
|
||||
* What we do want though, is a public version so that we can call it
|
||||
* statically for testing.
|
||||
*/
|
||||
function decodePublic(
|
||||
bytes calldata stream
|
||||
) public pure returns (uint256, bytes calldata) {
|
||||
return decode(stream);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user