308 Commits

Author SHA1 Message Date
jacque006
7671d78a1b Add contract audits 2023-08-15 08:33:17 -06:00
Jake C-T
9476d34132 Merge pull request #608 from getwax/update-licenses-mit
Update contract licenses to MIT
2023-06-28 16:45:24 -06:00
jacque006
f8dba7891b Pin bls-wallet-clients@0.9.0-405e23a 2023-06-28 16:34:41 -06:00
jacque006
405e23a7a0 Merge branch 'main' of github.com:web3well/bls-wallet into update-licenses-mit 2023-06-28 16:26:41 -06:00
Jake C-T
48637336b3 Merge pull request #606 from getwax/merge-in-contract-updates
Audit Fix 06 Integration branch
2023-06-28 15:31:43 -06:00
jacque006
d0be22aa49 Pin bls-wallet-clients@0.9.0-5d98448 2023-06-27 14:48:26 -06:00
jacque006
5d98448c66 Update contract licenses to MIT 2023-06-27 14:35:46 -06:00
jacque006
c72b0ea971 Pin bls-wallet-clients@0.9.0-3251dec 2023-06-27 13:47:56 -06:00
jacque006
3251deca54 Remove signatureExpiryTimestamp from setBLSKeyForWallet
Remove signatureExpiryTimestamp param from setBLSKeyForWallet as it is no longer used.
Rework branching logic in setBLSKeyForWallet to use single if.
2023-06-27 13:25:40 -06:00
Jake C-T
906538a295 Merge pull request #604 from getwax/bw-458-audit-fix-06
Bw 458 audit fix 06
2023-06-27 13:12:55 -06:00
jacque006
ada85008ad Merge branch 'contract-updates' of github.com:web3well/bls-wallet into bw-458-audit-fix-06 2023-06-27 12:02:49 -06:00
jacque006
d75868c182 Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates 2023-06-27 12:02:25 -06:00
Jake C-T
abde5526c2 Merge pull request #605 from getwax/bw-602-fix-fees-on-optimism
Fix fees on optimism
2023-06-27 11:58:20 -06:00
jacque006
823ec47c42 Move Optimism gas price oracle address to env file
Update docstrings & aggregator README.
2023-06-27 11:33:58 -06:00
Andrew Morris
2317b38e28 Add missing variable in .env.test 2023-06-05 09:20:15 +00:00
Andrew Morris
f31a35bd0e Fix missing config 2023-06-05 09:10:34 +00:00
Andrew Morris
9ccd2545e8 Merge remote-tracking branch 'origin/main' into bw-602-fix-fees-on-optimism 2023-06-05 09:04:39 +00:00
Andrew Morris
25ec77c45c Add details to insufficient fee 2023-06-05 08:53:07 +00:00
Andrew Morris
8bbb6d3a74 Return receipt only 2023-06-05 08:38:59 +00:00
Andrew Morris
797ec3528f Use receipt.transactionHash, logging improvements 2023-06-05 08:32:58 +00:00
Andrew Morris
4af97846bb Add the L1 fee for optimism when calculating profit 2023-06-02 18:02:41 +10:00
James Zaki
9a9b0284b3 Fix linter errors 2023-05-22 21:28:05 +01:00
James Zaki
60effa15a1 Resolve AI6 by removing wallet upgradability 2023-05-22 18:54:31 +01:00
James Zaki
e4c7dfb01c Revert "WIP AI6 - test fail"
This reverts commit 50b957ad4f.
2023-05-22 18:01:53 +01:00
James Zaki
50b957ad4f WIP AI6 - test fail 2023-05-22 17:47:05 +01:00
John Guilding
54227a57b0 Merge pull request #592 from web3well/192-generate-determinisitc-bundle-hashes
192 generate determinisitc bundle hashes
2023-05-09 17:48:22 +01:00
JohnGuilding
f9ce7be5b5 Update experimental release after rebasing onto main 2023-05-09 17:23:50 +01:00
JohnGuilding
2a20bfeb8d Fix aggregator lint warnings 2023-05-09 17:18:24 +01:00
JohnGuilding
275d593b5c publish bls-wallet-clients v0.9.0-4b76bb5 2023-05-09 17:18:24 +01:00
JohnGuilding
00d948376a Fix get request typo 2023-05-09 17:15:01 +01:00
JohnGuilding
0e51ecb5fe Fix invalid import statement 2023-05-09 17:15:01 +01:00
JohnGuilding
576e778855 Update hash logic and add integration test 2023-05-09 17:15:01 +01:00
JohnGuilding
4d170f73dd Remove unneeded return type change 2023-05-09 17:15:01 +01:00
JohnGuilding
cd010324a5 Add hash bundle fn & ability to get aggregate bundle to client module 2023-05-09 17:15:01 +01:00
JohnGuilding
29542e4c98 Use deterministic bundle hashes in aggregator 2023-05-09 17:15:01 +01:00
John Guilding
1ff60d5dd1 Merge pull request #603 from web3well/improve-quill-ui-responsiveness
Improve quill UI responsiveness
2023-05-09 17:08:43 +01:00
JohnGuilding
ebf415c573 Truncate long balances 2023-05-09 16:55:58 +01:00
JohnGuilding
b296a01a80 Add responsive navbar with toggle button 2023-05-09 16:43:35 +01:00
JohnGuilding
0e7e42154b Make home page UI more responsive 2023-05-09 16:42:57 +01:00
JohnGuilding
0940333e3c Make onboarding UI more responsive 2023-05-09 16:42:18 +01:00
John Guilding
831632ce8a Merge pull request #600 from web3well/392-blsWalletSigner-verify-cannot-verify-bundles-with-multiple-addresses
Add ability to blsWalletSigner to verify multiple addresses
2023-05-09 11:26:35 +01:00
Andrew Morris
534c5aa6ae getOptimismL1Fee 2023-05-09 12:54:24 +10:00
Andrew Morris
1701a1b7bd Move getRawTransaction into helpers 2023-05-09 11:43:27 +10:00
Andrew Morris
dae68465ba getRawTransaction 2023-05-09 10:58:53 +10:00
Andrew Morris
f3c0f57b0b Include scalar 2023-05-09 10:25:33 +10:00
Andrew Morris
24b011af9b Multiply l1BaseFee by scalar 2023-05-09 10:25:09 +10:00
Andrew Morris
4fd11c0356 Add scalar, decimals, getAllParams 2023-05-09 10:21:48 +10:00
JohnGuilding
a58dcdaee7 Update bls-wallet-clients to v0.9.0-1620721 2023-05-08 13:52:20 +01:00
JohnGuilding
162072155d publish experimental release & update agg to verify multiple addresses 2023-05-08 13:42:21 +01:00
JohnGuilding
e675062a53 Add test case for verify address mismatch 2023-05-08 13:33:04 +01:00
JohnGuilding
95a107f6b8 Add ability to blsWalletSigner to verify multiple addresses 2023-05-08 13:33:04 +01:00
Andrew Morris
5bd09ec4e5 Add required IS_OPTIMISM variable to example configs 2023-05-08 17:28:39 +10:00
Andrew Morris
b995c6d6a0 Add docs 2023-05-08 17:27:16 +10:00
Andrew Morris
7c3aee1f70 Unify effective gas calculations to fix optimism 2023-05-08 17:27:08 +10:00
John Guilding
a6f844b822 Merge pull request #601 from web3well/update-client-module-to-v0.9.0
Update components to use client module v0.9.0
2023-05-05 16:57:11 +01:00
JohnGuilding
d7f1917ed7 Update components to use client module v0.9.0 2023-05-05 16:48:28 +01:00
Jake C-T
8edaab3a71 Merge pull request #599 from web3well/clean-up-bls-provider-and-signer
Clean up bls provider and signer
2023-05-04 14:37:57 -06:00
JohnGuilding
c5d83f4650 Remove tests that aren't covering additional cases
- estimateGas test has an actual test in contract interaction tests
- list accounts is basically being tested in test below it (ethers logic just calls `.send("eth_accounts", [])`
- getBalance tested everywhere implicitly
2023-05-04 18:46:27 +01:00
JohnGuilding
ddb8f72864 Add merge-ok to remaining todos 2023-05-04 18:40:43 +01:00
JohnGuilding
32c9c6c7ed Remove unnecessary integration tests to regular client tests 2023-05-04 18:07:14 +01:00
JohnGuilding
48e472389d Consolidate tests & fix todos 2023-05-04 16:39:41 +01:00
Jake C-T
72c7b7c3c5 Merge pull request #597 from web3well/bw-596-test-decode-logs
Test "decodes WalletOperationProcessed logs"
2023-05-04 08:34:10 -06:00
Andrew Morris
1f52f159e4 Merge pull request #598 from web3well/main
Merge main into contract-updates
2023-05-04 12:34:15 +10:00
Andrew Morris
9c5c3ba950 Test "decodes WalletOperationProcessed logs" 2023-05-04 11:58:27 +10:00
Andrew Morris
d7369af0cf Fix fallback to .logs in case where .events exists but didn't recognize WalletOperationProcessed 2023-05-04 11:03:15 +10:00
JohnGuilding
fc5a668ed6 Update signMessage to concatenate sig instead of RLP encode it 2023-05-03 16:19:01 +01:00
JohnGuilding
40a3335a38 Restore chai spies and sinon fakes after each test 2023-05-03 15:17:44 +01:00
John Guilding
9b2ab4bb48 Merge pull request #594 from web3well/merge-in-contract-updates
Merge in contract updates
2023-05-03 15:41:21 +02:00
Andrew Morris
a609cb1f8e Don't assert the to address 2023-05-03 15:49:05 +10:00
Andrew Morris
553d9ba81d Update bls-wallet-clients in extension 2023-05-03 15:40:33 +10:00
Andrew Morris
ddbe420f53 Update optimism-goerli deployment 2023-05-03 14:37:32 +10:00
Andrew Morris
fbab084b52 Update arbitrum-goerli deployment 2023-05-03 14:37:01 +10:00
Andrew Morris
fff8dd397f Update bls-wallet-clients in aggregator 2023-05-03 12:21:37 +10:00
Andrew Morris
926637130a Fix lint error, attach issue for real fix 2023-05-03 11:59:39 +10:00
Andrew Morris
0047f14a0f Merge remote-tracking branch 'origin/contract-updates' into merge-in-contract-updates 2023-05-03 11:44:07 +10:00
Blake Duncan
0d52ddb20f Merge pull request #590 from web3well/bw-589-avoid-solidity-abi
Add `ExpanderEntryPoint` to avoid solidity abi
2023-05-02 16:54:17 +01:00
Blake Duncan
ffd7037573 Merge pull request #584 from web3well/bw-406-aggregator-compression
Use compression in aggregator
2023-05-02 15:41:23 +01:00
Blake Duncan
daa51bce52 Merge pull request #586 from web3well/contracts-connector
Contracts connector
2023-05-02 12:26:51 +01:00
Blake Duncan
69fa24daff Merge pull request #585 from web3well/fix-expander-lookup
Fix expander lookup
2023-05-02 12:02:38 +01:00
Andrew Morris
01a908e26f Merge pull request #577 from web3well/bw-576-erc20-expander
ERC20 expander
2023-05-02 16:48:09 +10:00
Hrishikesh deoghare
8c91d2faa3 Update local and remote docs (#591)
Update local and remote docs
2023-04-29 08:20:59 +01:00
John Guilding
b2ab3b766d Merge pull request #583 from deepaksing/deepaksing/issue563
fix: assing TransactionReceipt to getOperationResults always results in no WalletOperationProcessed events found in transaction error
2023-04-27 18:01:45 +02:00
John Guilding
c2afe790cf Merge pull request #588 from web3well/update-domains-with-relevant-erc712-fields
Add name and version to bls wallet domains
2023-04-27 16:58:10 +02:00
Andrew Morris
c736179e75 Use ExpanderEntryPoint in aggregator 2023-04-27 10:25:00 +10:00
Andrew Morris
dc488ecc00 Use ExpanderEntryPoint to avoid solidity abi 2023-04-26 17:25:16 +10:00
JohnGuilding
3032cf0917 Update test sigs & convert domain to hex string in deployment 2023-04-25 12:45:42 +01:00
JohnGuilding
e29c18c63f Add name and version to bls wallet domains 2023-04-24 21:50:25 +01:00
Andrew Morris
e42ccf2f1f Deploy to arbitrum goerli 2023-04-24 18:34:16 +10:00
Andrew Morris
ef8f2c7a13 Show address that lacks funds 2023-04-24 18:34:16 +10:00
Andrew Morris
0da9350fd6 registerWallet 2023-04-24 18:34:16 +10:00
Andrew Morris
5cacd017d1 Add tx len info to events 2023-04-24 18:34:16 +10:00
Andrew Morris
02c8565e98 Use compression 2023-04-24 18:34:16 +10:00
Andrew Morris
52d49e16d8 .utilities -> .aggregatorUtilities 2023-04-24 18:34:16 +10:00
Andrew Morris
b84aebfaa9 Remove unused variable 2023-04-24 18:34:16 +10:00
Andrew Morris
bdca7833d3 Set up bundleCompressor in EthereumService 2023-04-24 18:34:16 +10:00
Andrew Morris
56fa5b6e82 ContractsConnector 2023-04-24 17:26:19 +10:00
Andrew Morris
2cf961ff13 Serialize expander deployments 2023-04-24 17:25:32 +10:00
Andrew Morris
9e8269b841 Use events to lookup expanders 2023-04-24 17:16:00 +10:00
Andrew Morris
1139b4e4f1 Merge remote-tracking branch 'origin/contract-updates' into bw-576-erc20-expander 2023-04-24 17:09:19 +10:00
John Guilding
f5026347e1 Merge pull request #582 from web3well/main
main -> contract updates
2023-04-20 19:13:03 +02:00
John Guilding
638e1a1fb2 Merge pull request #581 from web3well/fix-cors
Configure cors for geth dev
2023-04-20 09:54:12 +02:00
Andrew Morris
fb5537a118 Configure cors for geth dev 2023-04-20 16:24:42 +10:00
Andrew Morris
4eeb9afb25 Merge pull request #575 from web3well/merge-in-contract-updates
Merge in contract updates
2023-04-20 16:21:29 +10:00
Andrew Morris
1e86c15892 Import ActionData from deps 2023-04-20 14:32:04 +10:00
Digvijay Pundir (DJ)
f8a8c490e8 Fix page move when border is increased (#578)
Co-authored-by: ksingh-vertex <ksingh@vertex.com>
2023-04-20 12:14:27 +10:00
JohnGuilding
28f3983fb2 Merge branch 'main' into merge-in-contract-updates 2023-04-19 18:53:57 +02:00
John Guilding
b653eaf5ab Merge pull request #580 from web3well/551-run-quill-against-geth-node-and-pay-aggregator-fees
551 run quill against geth node and pay aggregator fees
2023-04-19 18:40:34 +02:00
JohnGuilding
84a89cb5b6 hardhat uses same chainId as geth node (1337) 2023-04-19 16:10:53 +02:00
JohnGuilding
9922c3a79c Fix typos 2023-04-19 15:45:37 +02:00
JohnGuilding
f4fdf7148d Update docs 2023-04-19 15:30:05 +02:00
JohnGuilding
51f4a9588a Add fee payment action to quill 2023-04-19 15:29:49 +02:00
JohnGuilding
0b1f037ba7 Run quill against geth node (chainId 1337) 2023-04-19 13:27:46 +02:00
deepaksing
000126d226 fix: assing TransactionReceipt to getOperationResults always results in no WalletOperationProcessed events found in transaction error 2023-04-19 08:38:13 +05:30
JohnGuilding
d401e745cf Fix BlsSignerContractInteraction tests
- sinon fake for getRandomBlsPrivateKey was resulting in throwawayBlsWalletWrapper nonce increasing when it should stay at zero
- fake was using signers[0] private key
- fix was to use fresh signer that wouldn't have its nonced changed in any other test
2023-04-13 15:43:12 +02:00
JohnGuilding
21e0532fc0 Fix BlsSigner tests
- update invalid transaction expected error message (due to using signWithGasEstimate in BlsSigner)
- fix sign tx batch test by replicating what BlsSigner logic does

cleanup
- remove console logs
- remove unnecessary transaction.to check
2023-04-13 15:38:48 +02:00
JohnGuilding
6d1f96ab2e Fix BlsProviderContractInteraction tests
- set nonce to zero at all times in provider.estimateGas to prevent "BLSWallet: only callable with current nonce" error (see comment)
2023-04-13 15:04:16 +02:00
JohnGuilding
008dcf0b09 Fix BlsProvider tests
- use correct nonce in multiple operation sendTransaction error test
- add Operation gas property to missing test setup
- use new VG address
2023-04-13 14:58:36 +02:00
jacque006
1eed20fa6e Fix extension lint warnings, pass vg address to signer. 2023-04-13 15:38:35 +09:00
jacque006
126d82266b Add expiry timestamp to recovery bundle in extension.
Parallelize async calls in recoverWallet.
Remove unneeded camelcase lint disables.
Add comment for human reable times to expiry durations.
2023-04-13 15:06:40 +09:00
jacque006
ff1f253012 Pin bls-wallet-clients@0.8.3-c34db60 2023-04-13 13:50:15 +09:00
jacque006
c34db600cd Update client signature tests with new domain 2023-04-13 13:45:21 +09:00
jacque006
a37ae25990 Merge branch 'contract-updates' of github.com:web3well/bls-wallet into merge-in-contract-updates 2023-04-13 13:27:26 +09:00
John Guilding
b48dc20db4 Merge pull request #579 from web3well/461-audit-fix-9
461 audit fix 9
2023-04-12 16:57:35 +01:00
JohnGuilding
4d20166d6f Add comments to explain test logic 2023-04-12 17:50:19 +02:00
JohnGuilding
7a63b3aa4d rename signatureExpiryWindow to signatureExpiryOffsetSeconds 2023-04-12 17:50:08 +02:00
JohnGuilding
bc0d57007f add signature expiry negative logic tests 2023-04-12 17:48:53 +02:00
JohnGuilding
28226f71e5 Add freshness check to proof of possession signatures 2023-04-12 17:48:43 +02:00
Blake Duncan
5ad1314efc Audit issue 7 fixes (#571)
* Audit issue 7 fixes

* Remove unused params
2023-04-12 16:16:33 +01:00
jacque006
10c7d54d12 Fix test case exceeding max bundle gas limit.
Increase gas limit per bundle for BundleService submit test cases.
Add event when simulating adding a bundle would exceeed max gas per bundle limit with details.
Link emit func to AggregatationStrategy when created in test fixture.
2023-04-12 13:45:23 +09:00
jacque006
0cdc6430e4 Add resolution for @ethersproject/signing-key 2023-04-12 10:12:12 +09:00
jacque006
e1906cdbb7 Remove ethers.js resolution 2023-04-11 18:24:31 -06:00
jacque006
a3b4877c11 Fix extension build
Add new safe deployer address as 'n/a' to existing contract deploys.
Add assert-browserify and resolution to webpack.
Rename factory func to camelcase.
Use sign with gas estimate over older sign.
2023-04-11 18:12:18 -06:00
jacque006
b7ac4fd77c Add gas op field in agg proxy, fix factory rename in agg deps 2023-04-11 17:42:24 -06:00
jacque006
c88b05d0ec Pin bls-wallet-clients@0.8.3-f6ab5d9 2023-04-11 17:19:11 -06:00
jacque006
f6ab5d93ed Merge branch 'contract-updates' of github.com:web3well/bls-wallet into merge-in-contract-updates 2023-04-11 16:47:25 -06:00
jacque006
c07dc63896 Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates 2023-04-11 16:44:46 -06:00
John Guilding
70ca00f089 Merge pull request #570 from web3well/567-bls-provider-and-bls-signer-release
567 bls provider and bls signer formal release
2023-04-05 14:26:15 +01:00
Andrew Morris
6156b86b22 Update clients in aggregator 2023-04-04 22:29:29 -06:00
jacque006
639f6133bf Merge branch 'contract-updates' of github.com:web3well/bls-wallet into merge-in-contract-updates 2023-04-04 22:19:18 -06:00
JohnGuilding
1ec0330adb Point components to v0.8.3 2023-04-04 19:15:36 +01:00
JohnGuilding
cb5932776c v0.8.3 2023-04-04 19:09:35 +01:00
jacque006
115907c74f Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates 2023-04-03 10:41:42 -06:00
Paul-T.C-Yu
5529e078d9 Add /health to aggregator 2023-04-03 18:19:12 +10:00
Andrew Morris
4fd593ac1d Remove log 2023-04-03 18:08:29 +10:00
Andrew Morris
ff27bd0469 Fix bls-wallet-clients 2023-04-03 18:00:02 +10:00
Andrew Morris
cac4669cb9 Update docs 2023-04-03 17:33:40 +10:00
Andrew Morris
5fe9170c3e Add approveMax test 2023-04-03 17:14:07 +10:00
Andrew Morris
ad9350eb68 Test erc20 transfer 2023-04-03 16:33:12 +10:00
Andrew Morris
513df2229e Erc20Compressor 2023-04-03 15:52:04 +10:00
Andrew Morris
f08fa1e9ff ERC20Expander 2023-04-03 14:40:26 +10:00
Andrew Morris
aa8cd1d681 Fix bls-wallet-clients 2023-04-03 12:55:16 +10:00
Andrew Morris
e6326835bb Merge remote-tracking branch 'origin/contract-updates' into merge-in-contract-updates 2023-04-03 12:46:23 +10:00
John Guilding
cb216fa7d7 Merge pull request #572 from web3well/aggregator-tests-periodic-local-contract-deploy-issue
Add wait for contract deploy script to agg workflow
2023-03-30 12:26:39 +01:00
JohnGuilding
7978ed0690 Remove CI test comment 2023-03-30 12:19:34 +01:00
JohnGuilding
d86cf09716 Add wait for contract deploy script to agg workflow 2023-03-30 10:33:56 +01:00
jacque006
dc6ebc24d6 Fix contract tests
Fix issues in contract tests from merge conflict resolution with main.
Add expect utility for public keys to prevent false positive failures when hex values are zero padded (0x00...).
Replace most pubkey deep equality checks with new util.
2023-03-29 18:14:55 -06:00
jacque006
7b07df3aba Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates 2023-03-29 12:02:14 -06:00
JohnGuilding
2aa7e352f5 client module experimental release 0.8.2-de12b3c 2023-03-29 13:26:34 +01:00
JohnGuilding
de12b3c62f Remove provider & signer from experimental namespace 2023-03-29 13:03:06 +01:00
John Guilding
e1248c6b63 Merge pull request #568 from web3well/565-add-static-method-to-get-private-key-in-bls-signer
Add static get random private key method to bls signer
2023-03-29 11:59:57 +01:00
JohnGuilding
b988fbc92c Add static get random private key method to bls signer 2023-03-29 11:34:12 +01:00
John Guilding
96bfb32e5b Merge pull request #562 from web3well/549-add-documentation-for-bls-provider-and-bls-signer
549 add documentation for bls provider and bls signer
2023-03-28 14:36:57 +01:00
JohnGuilding
7435d9976e Fix typescript errors in BundleTable 2023-03-28 13:04:58 +01:00
JohnGuilding
2dae355817 Update root readme to include provider link 2023-03-28 11:07:20 +01:00
JohnGuilding
e734209df0 Remove BlsWalletWrapper in provider docs where possible 2023-03-28 11:06:50 +01:00
Jacob Caban-Tomski
4c9c0ed898 Merge pull request #558 from web3well/bw-554-fallback-tx-origin
`tx.origin` payments in fallback expander
2023-03-27 20:40:50 -06:00
Jacob Caban-Tomski
fcbfd7cc62 Merge pull request #556 from web3well/expanders-followup
Expanders followup
2023-03-27 20:38:37 -06:00
John Guilding
13f34dd02d Merge pull request #561 from web3well/201-implement-erc-1271-signature-validation
201 implement erc 1271 signature validation
2023-03-27 19:57:23 +01:00
JohnGuilding
b565c33193 Check VG is called with wallet via hashFromWallet mapping 2023-03-27 19:12:06 +01:00
JohnGuilding
c1404502f3 Return 0xffffffff magic value when sig is invalid 2023-03-27 16:57:24 +01:00
JohnGuilding
74f9d9020c Use onlyWallet modifier, abi.encode, and change function name 2023-03-27 16:53:50 +01:00
JohnGuilding
d2c6cff629 Add missing documentation following rebase 2023-03-27 12:48:35 +01:00
JohnGuilding
25469d50e4 Remove directly paying aggregator fees docs example 2023-03-27 11:09:26 +01:00
JohnGuilding
548301d32d Add bls provider guide and ts doc comments 2023-03-27 11:09:00 +01:00
JohnGuilding
4da348d9e2 Update signature length error message 2023-03-24 17:08:47 +00:00
JohnGuilding
51d7681626 Add erc 1271 implementation 2023-03-24 17:08:47 +00:00
JohnGuilding
7c3fbd4d40 Add BLSPublicKeyFromHash mapping to verification gateway 2023-03-24 17:08:47 +00:00
John Guilding
ac7cd956a8 Merge pull request #548 from web3well/413-improve-private-key-management-in-bls-provider-and-signer
413 improve private key management in bls provider and signer
2023-03-22 19:20:06 +00:00
JohnGuilding
5423f65503 Add comment explaining throwaway BlsWalletWrapper 2023-03-22 12:03:28 +00:00
Jacob Caban-Tomski
fdf80fc5fb Merge pull request #555 from web3well/bw-525-registration
Add expander for registration
2023-03-16 18:07:21 -06:00
JohnGuilding
3c0f36f444 re-add accidentally deleted tests 2023-03-16 17:19:36 +00:00
JohnGuilding
32c6b13e7d Cleanup test code & add contract interaction gas estimate tests 2023-03-16 16:59:28 +00:00
JohnGuilding
e3bbd393d8 Remove reference to signer from provider 2023-03-16 16:12:19 +00:00
Andrew Morris
b0ba263eb4 Test tx.origin payment with fallback expander 2023-03-16 16:53:25 +11:00
Andrew Morris
b04281cdac Enable optimized tx.origin payments in fallback expander 2023-03-16 16:53:14 +11:00
Andrew Morris
e196a101ff Docs 2023-03-15 18:46:44 +11:00
Andrew Morris
995ec24d1f Rename factory imports to avoid underscores, simplify index.ts 2023-03-15 18:20:04 +11:00
Andrew Morris
96c61e9932 Tidying up 2023-03-15 17:44:38 +11:00
Andrew Morris
8b76734316 Merge pull request #552 from web3well/bw-523-use-registries
Use registries
2023-03-15 17:21:51 +11:00
Andrew Morris
519e6f88c4 Merge remote-tracking branch 'origin/contract-updates' into bw-523-use-registries 2023-03-15 17:16:24 +11:00
Andrew Morris
7f803bd10a Merge pull request #553 from web3well/bw-523-registry-wrappers
Registry wrapper classes
2023-03-15 17:15:25 +11:00
Andrew Morris
1e71ef3c78 Merge pull request #547 from web3well/bw-524-reg-index
Registry index format
2023-03-15 17:13:29 +11:00
Andrew Morris
708a6d0c2d Remove unused import 2023-03-15 17:11:50 +11:00
Andrew Morris
42ea205f79 Add test with multiple transfers 2023-03-15 17:00:22 +11:00
Andrew Morris
52974e01f6 Add variation with tx.origin payment 2023-03-15 16:22:37 +11:00
Andrew Morris
13837e6729 Add registration expander test 2023-03-15 16:01:36 +11:00
Andrew Morris
7a911a3634 Add registry test, remove similar old tests 2023-03-15 15:16:40 +11:00
Andrew Morris
4ac580d1df Start expander tests 2023-03-15 14:59:15 +11:00
Jacob Caban-Tomski
30b16185fb Merge pull request #546 from web3well/bw-522-registries
AddressRegistry and BLSPublicKeyRegistry
2023-03-14 20:42:15 -06:00
Jacob Caban-Tomski
a9e511c3f0 Merge pull request #545 from web3well/bw-521-followup-tests
Extra PseudoFloat tests
2023-03-14 17:57:49 -06:00
Andrew Morris
1468c91dc0 Include blsRegistrationCompressor in fixture's compressor 2023-03-14 20:14:38 +11:00
Andrew Morris
5ce0943f00 Deploy, add compressor 2023-03-14 20:09:44 +11:00
Andrew Morris
8ccad86fdf BLSRegistration.sol 2023-03-14 19:26:42 +11:00
Andrew Morris
bc0c272d65 Merge branch 'bw-523-registry-wrappers' into bw-523-use-registries 2023-03-14 15:11:07 +11:00
Andrew Morris
3a0a1e5f41 Wrapper classes for registries 2023-03-14 15:09:12 +11:00
Andrew Morris
6de210b81b Remove unnecessary condition, parallelize address calls 2023-03-14 14:50:24 +11:00
Andrew Morris
7239d51a57 Use connectIfDeployed 2023-03-14 14:32:43 +11:00
Andrew Morris
5ce1723ed5 Streamline signerOrProvider 2023-03-14 14:30:26 +11:00
Andrew Morris
547bb7f34d Allow passing signer and prefer contracts with signer 2023-03-14 14:10:09 +11:00
Andrew Morris
059b1d4b1a Use registries 2023-03-13 17:58:10 +11:00
Andrew Morris
a01d7426ea Make compression async 2023-03-13 17:41:40 +11:00
Andrew Morris
80d551d14e BundleCompressor 2023-03-13 17:31:49 +11:00
Andrew Morris
e2d8399dbf BlsPublicKeyRegistryWrapper 2023-03-13 16:12:25 +11:00
Andrew Morris
7522fb655f AddressRegistryWrapper 2023-03-13 15:56:04 +11:00
Andrew Morris
fa489ab75f Move SafeSingletonFactory into clients 2023-03-13 15:20:22 +11:00
Andrew Morris
41ac18fb86 Decode regIndexes and use registries in fallback expander 2023-03-10 14:37:05 +11:00
Andrew Morris
c390ac0321 encodeBitStream 2023-03-10 14:12:51 +11:00
Andrew Morris
2adb7ace4c Include registryUsageBitStream as a VLQ and switch on the bits, but use stubs for the registry usage cases 2023-03-10 13:47:07 +11:00
Andrew Morris
f90e79d306 Merge branch 'bw-524-reg-index' into bw-523-use-registries 2023-03-10 12:11:47 +11:00
Andrew Morris
f3b5552fb2 Simplify format, add more tests 2023-03-08 18:44:07 +11:00
Andrew Morris
4130335f5a Fix 8,388,607 2023-03-08 17:43:49 +11:00
Andrew Morris
43cf586131 Test 1 and 8,388,607 (latter fails) 2023-03-08 17:41:03 +11:00
Andrew Morris
f4c2e21c9b Make RegIndex BigEndian 2023-03-08 17:35:03 +11:00
Andrew Morris
f8223693fa Start tests 2023-03-08 17:24:19 +11:00
Andrew Morris
e35b160e87 RegIndex.sol 2023-03-08 17:10:17 +11:00
Andrew Morris
cd570155da uint -> uint256 2023-03-07 17:03:31 +11:00
Andrew Morris
7a9f26d218 tests 2023-03-07 16:34:08 +11:00
Andrew Morris
13779ffe3b Add lookup methods 2023-03-07 15:23:43 +11:00
Andrew Morris
699f84cede AddressRegistry.sol, BLSPublicKeyRegistry.sol 2023-03-07 15:08:28 +11:00
Andrew Morris
e858c3c826 More pseudofloat tests 2023-03-07 13:53:57 +11:00
Andrew Morris
4052997b15 Merge pull request #519 from web3well/pseudo-floats
Pseudo floats
2023-03-07 12:55:02 +11:00
Andrew Morris
c260e60443 Merge pull request #536 from web3well/bw-394-followup-combine-contracts
Use BLSWallet and ProxyAdmin directly inside VerificationGateway
2023-03-03 18:15:52 +11:00
Andrew Morris
480ee1d3d0 bump 2023-03-03 18:09:05 +11:00
Andrew Morris
25c73d6697 Merge pull request #464 from web3well/bw-394-use-safe-singleton-factory
Use safe singleton factory
2023-03-02 20:10:13 +11:00
Andrew Morris
1052cc6c28 Remove unused contract (now inside VG) 2023-02-15 16:30:03 +11:00
Andrew Morris
af420d9a7b Remove unused blsLibrary address 2023-02-15 15:12:12 +11:00
Andrew Morris
fde1c61e93 Use BLSWallet and ProxyAdmin directly inside VerificationGateway 2023-02-15 14:38:26 +11:00
Andrew Morris
8bc62681f6 Merge remote-tracking branch 'origin/contract-updates' into bw-394-use-safe-singleton-factory 2023-02-15 10:55:55 +11:00
Andrew Morris
bac557a113 Merge pull request #528 from web3well/remove-lazy-wallets 2023-02-14 23:13:17 +11:00
Andrew Morris
1464476cec Allow deploy() when already deployed 2023-02-14 08:48:03 +11:00
Andrew Morris
306b90e83d Use official SafeSingletonFactory for 1337 2023-02-14 08:44:13 +11:00
Andrew Morris
d7348e581d Merge branch 'remove-lazy-wallets' into bw-394-use-safe-singleton-factory 2023-02-13 19:40:00 +11:00
Andrew Morris
5b529facd1 Merge branch 'bw-332-fix-proxy-admin-creation' into bw-394-use-safe-singleton-factory 2023-02-13 19:38:23 +11:00
Andrew Morris
4d7a83705b Fix lint issue 2023-02-13 19:26:44 +11:00
Andrew Morris
8e9cd18934 Fix tests 2023-02-13 19:25:00 +11:00
Andrew Morris
87eb27053a Replace lazyBlsWallets with just creating wallets 2023-02-13 19:05:05 +11:00
Andrew Morris
dd1a19b8d8 Inline BLS_DOMAIN calc (fit deployment into 30m) 2023-02-13 18:18:28 +11:00
Andrew Morris
a9bc94c641 Fix deterministic creation of VG using ProxyAdminGenerator 2023-02-13 17:31:19 +11:00
Andrew Morris
e416c13635 Merge remote-tracking branch 'origin/contract-updates' into bw-394-use-safe-singleton-factory 2023-02-13 16:57:01 +11:00
Andrew Morris
2b57673de4 Post-merge fixes 2023-02-13 16:31:53 +11:00
Andrew Morris
edae8c17f0 Merge remote-tracking branch 'origin/main' into contract-updates 2023-02-13 15:32:45 +11:00
Andrew Morris
e1e654eac5 Merge pull request #518 from web3well/bw-509-followup
Use .eventually in contracts
2023-02-13 15:03:13 +11:00
Andrew Morris
9202cee106 Fix lint issue 2023-02-13 14:50:50 +11:00
Andrew Morris
1d887e696e Use pseudo-floats for ethValues and gas 2023-02-10 17:35:33 +11:00
Andrew Morris
51e6666776 Add big test 2023-02-10 17:26:07 +11:00
Andrew Morris
74d31d561f encodePseudoFloat 2023-02-10 16:42:10 +11:00
Andrew Morris
a666a2b9c9 Add and fix tests 2023-02-10 16:25:31 +11:00
Andrew Morris
d2da8d8e7e PseudoFloat.sol 2023-02-10 16:07:01 +11:00
Andrew Morris
1018131c27 Use .eventually in contract tests 2023-02-10 14:45:08 +11:00
Andrew Morris
e1a306f9f2 Cast to code:string instead of any
Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2023-02-10 12:22:40 +11:00
Andrew Morris
847bbddd25 Merge pull request #511 from web3well/bw-509-new-expander
BLSExpanderDelegator and FallbackExpander
2023-02-10 11:27:14 +11:00
Andrew Morris
a4aaa42964 Fix vlq-test.ts 2023-02-08 17:53:26 +11:00
Andrew Morris
07da3694f2 Add and fix test 2023-02-08 17:37:30 +11:00
Andrew Morris
a968e86a20 Add gas to fallback expander 2023-02-08 13:05:15 +11:00
Andrew Morris
c5e3461d60 Add docs and example 2023-02-08 12:59:13 +11:00
Andrew Morris
81c3d1c965 FallbackExpander 2023-02-08 12:59:13 +11:00
Andrew Morris
30ddc6f8ab Use viaIR to fix stack depth issue 2023-02-08 12:59:13 +11:00
Andrew Morris
0ca6f4f8fa Move some things around 2023-02-08 12:59:13 +11:00
Andrew Morris
365084ed86 Make expanderIndex explicit 2023-02-08 12:59:13 +11:00
Andrew Morris
2daff8b542 Add commentary 2023-02-08 12:59:13 +11:00
Andrew Morris
027f899fe3 Implement BLSExpanderDelegator.run 2023-02-08 12:59:13 +11:00
Andrew Morris
9f76ceb9f8 More vlq testing 2023-02-08 12:59:13 +11:00
Andrew Morris
a348962e42 Test VLQ directly instead of looking at logs 2023-02-08 12:59:13 +11:00
Andrew Morris
3894fe68a2 Switch to big-endian 2023-02-08 12:59:13 +11:00
Andrew Morris
8972ccbc5c Move VLQ to library 2023-02-08 12:59:13 +11:00
Andrew Morris
4b42d2d960 Implement VLQ decoding 2023-02-08 12:59:13 +11:00
Andrew Morris
16a043279b Copy code from hackmd 2023-02-08 12:59:13 +11:00
Andrew Morris
59473044a0 Merge pull request #496 from web3well/bw-446-followup
Followups for #487 (gas limit PR)
2023-02-02 13:14:37 +11:00
Andrew Morris
a800935f51 Merge remote-tracking branch 'origin/contract-updates' into bw-394-use-safe-singleton-factory 2023-02-01 14:53:30 +11:00
Andrew Morris
a54b677720 Add docstring for overhead param 2023-02-01 12:35:26 +11:00
Andrew Morris
2d6ee184a6 Add return type annotation 2023-02-01 12:29:57 +11:00
Andrew Morris
0b2cca4816 uint -> uint256 2023-02-01 12:16:24 +11:00
Andrew Morris
e4cde3496d Add reason string 2023-02-01 12:15:24 +11:00
Andrew Morris
29d8884754 Merge pull request #487 from web3well/bw-446-add-gas-limit
Bw 446 add gas limit
2023-02-01 12:11:37 +11:00
Andrew Morris
28ebbe62f4 Fix lint issue 2023-02-01 12:04:57 +11:00
Andrew Morris
b4b757ce8b Fix gas measurement for wallets which don't yet exist
You are currently cherry-picking commit a44a348c.
2023-01-27 16:46:10 +00:00
James Zaki
470ac78044 Sign userOp with async gas estimate 2023-01-27 15:09:02 +00:00
James Zaki
cf16d8f833 Add gas param to Operation in client 2023-01-27 15:09:02 +00:00
James Zaki
1e666325ba Add gas param to tests. 2023-01-27 15:09:02 +00:00
James Zaki
78646e8d11 Add gas limit (WIP) 2023-01-27 15:09:02 +00:00
Andrew Morris
ae7886aba3 Add workaround for COST_ESTIMATOR_ADDRESS 2023-01-23 16:46:46 +11:00
Andrew Morris
86fc7c5895 Make ProxyAdmin address predetermined 2023-01-19 16:20:05 +11:00
Andrew Morris
0afefe9c1e Fix test 2023-01-19 16:19:20 +11:00
Andrew Morris
71a3520a0b Allow calling deploy multiple times 2023-01-19 16:01:13 +11:00
Andrew Morris
b97c8b5e6d Add temporary support for SafeSingletonFactory on 1337 2023-01-19 15:49:21 +11:00
Andrew Morris
73967cef4e Remove fundDeployer (no longer needed + doesn't exist) 2023-01-19 13:43:01 +11:00
Andrew Morris
3506fb6117 Fix config.test.ts 2023-01-19 13:43:01 +11:00
Andrew Morris
5918b12a75 Remove unused code 2023-01-19 13:43:01 +11:00
Andrew Morris
fef73de225 Remove lazyBlsWallets 2023-01-19 13:43:01 +11:00
Andrew Morris
321a2556b1 Fix tests running together with fixture singleton and avoiding lazyBlsWallets 2023-01-19 13:43:01 +11:00
Andrew Morris
03d61443e8 Simplify using processBundleWithExtraGas 2023-01-19 13:43:01 +11:00
Andrew Morris
77d9ff8823 Fix recovery-test.ts 2023-01-19 13:43:01 +11:00
Andrew Morris
f0cfe0d941 Fix upgrade-test.ts 2023-01-19 13:43:01 +11:00
Andrew Morris
711f898d96 Update deploy-test.ts 2023-01-19 13:43:01 +11:00
Andrew Morris
7def3c9b2e Fix walletAction-test.ts 2023-01-19 13:43:01 +11:00
Andrew Morris
139d8e04e4 Deploy using SafeSingletonFactory 2023-01-19 13:43:01 +11:00
Andrew Morris
59388de662 Implement calculateAddress, deploy 2023-01-19 13:43:01 +11:00
Andrew Morris
1928d59c58 SafeSingletonFactory initial setup 2023-01-19 13:43:01 +11:00
181 changed files with 9893 additions and 3279 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -11,6 +11,7 @@ You can watch a full end-to-end demo of the project [here](https://www.youtube.c
- See an [overview](./docs/system_overview.md) of BLS Wallet & how the components work together.
- Use BLS Wallet in [a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md).
- Use BLS Wallet in [your L2 dApp](./docs/use_bls_wallet_dapp.md) for cheaper, multi action transactions.
- Use BLS Wallet components and features with an [ethers.js provider and signer](./use_bls_provider.md)
### Setup your development environment

View File

@@ -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-405e23a",
"fp-ts": "^2.12.1",
"io-ts": "^2.2.16",
"io-ts-reporters": "^2.0.1",

View File

@@ -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,

View File

@@ -569,16 +569,6 @@
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
dependencies:
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/units@5.6.0":
version "5.6.0"
@@ -887,10 +877,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-405e23a:
version "0.9.0-405e23a"
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"

View File

@@ -40,3 +40,7 @@ PRIORITY_FEE_PER_GAS=0
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS=0x420000000000000000000000000000000000000F
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE=2

View File

@@ -38,3 +38,5 @@ PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false

View File

@@ -35,3 +35,5 @@ PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false

View File

@@ -89,32 +89,35 @@ commands.
#### Environment Variables
| Name | Example Value | Description |
| ---------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
| PORT | 3000 | The port to bind the aggregator to |
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregators onchain address |
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
| Name | Example Value | Description |
| ------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
| PORT | 3000 | The port to bind the aggregator to |
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregators onchain address |
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
| IS_OPTIMISM | false | Optimism's strategy for charging for L1 fees requires special logic in the aggregator. In addition to gasEstimate * gasPrice, we need to replicate Optimism's calculation and pass it on to the user |
| OPTIMISM_GAS_PRICE_ORACLE_ADDRESS | 0x420000000000000000000000000000000000000F | Address for the Optimism gas price oracle contract. Required when IS_OPTIMISM is true |
| OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE | 2 | Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for the optimism-specific calculation. This gets passed on to users. Required when IS_OPTIMISM is true |
## Running

View File

@@ -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,30 @@ 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-405e23a";
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-405e23a";
// 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-405e23a";
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 { pick } from "npm:@s-libs/micro-dash@15.2.0";

View File

@@ -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,

View File

@@ -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),

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import * as env from "../src/env.ts";
import { ethers } from "../deps.ts";
import OptimismGasPriceOracle from "../src/app/OptimismGasPriceOracle.ts";
const oracle = new OptimismGasPriceOracle(
new ethers.providers.JsonRpcProvider(env.RPC_URL),
);
const { l1BaseFee, overhead, scalar, decimals } = await oracle.getAllParams();
console.log({
l1BaseFee: `${(l1BaseFee.toNumber() / 1e9).toFixed(3)} gwei`,
overhead: `${overhead.toNumber()} L1 gas`,
scalar: scalar.toNumber() / (10 ** decimals.toNumber()),
});

View File

@@ -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,

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";
import getOptimismL1Fee from "../src/helpers/getOptimismL1Fee.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const txHash = Deno.args[0];
if (!txHash.startsWith("0x")) {
throw new Error("First arg should be tx hash");
}
const l1Fee = await getOptimismL1Fee(provider, txHash);
console.log(`${ethers.utils.formatEther(l1Fee)} ETH`);

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";
import getRawTransaction from "../src/helpers/getRawTransaction.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const txHash = Deno.args[0];
if (!txHash.startsWith("0x")) {
throw new Error("First arg should be tx hash");
}
console.log(await getRawTransaction(provider, txHash));

View 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;
}

View File

@@ -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],
});

View File

@@ -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,

View File

@@ -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");
@@ -60,24 +60,26 @@ const bundles: Bundle[] = [];
for (const [i, wallet] of wallets.entries()) {
const nonce = await wallet.Nonce();
console.log("Funding wallet", i);
console.log("Funding wallet", i, "(1 wei to make estimateFee work)");
await (await adminWallet.sendTransaction({
to: wallet.address,
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");
@@ -90,7 +92,7 @@ for (const [i, wallet] of wallets.entries()) {
// Ensure wallet can pay the fee
if (balance.lt(fee)) {
console.log("Funding wallet");
console.log("Funding wallet", i, "(based on estimateFee)");
await (await adminWallet.sendTransaction({
to: wallet.address,
@@ -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");

View 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",
],
),
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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");
}

View File

@@ -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) {

View File

@@ -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,13 @@ 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
.estimateEffectiveCompressedGas(
bundle,
);
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
@@ -612,13 +631,18 @@ export default class AggregationStrategy {
expectedFee: fee,
requiredFee: feeInfo.requiredFee,
expectedMaxCost: feeInfo.expectedMaxCost,
errorReason: { message: "Insufficient fee" },
errorReason: {
message: [
"Insufficient fee",
`(provided: ${ethers.utils.formatEther(fee)},`,
`required: ${ethers.utils.formatEther(feeInfo.requiredFee)})`,
].join(" "),
},
};
}
const gasEstimate = feeInfo?.gasEstimate ??
await this.ethereumService.verificationGateway
.estimateGas.processBundle(bundle);
await this.ethereumService.estimateEffectiveCompressedGas(bundle);
return {
success,
@@ -631,7 +655,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 +671,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.estimateEffectiveCompressedGas(bundle1),
es.estimateEffectiveCompressedGas(
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> {

View File

@@ -1,6 +1,7 @@
import { HTTPMethods } from "../../deps.ts";
type AppEvent =
| { type: "starting" }
| { type: "listening"; data: { port: number } }
| { type: "db-query"; data: { sql: string; params: unknown } }
| { type: "waiting-unconfirmed-space" }
@@ -15,6 +16,7 @@ type AppEvent =
data: {
includedRows: number;
bundleOverheadCost: string;
bundleOverheadLen: number;
expectedFee: string;
expectedMaxCost: string;
};
@@ -32,10 +34,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";

View File

@@ -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;
}

View File

@@ -6,6 +6,7 @@ import {
delay,
ethers,
Semaphore,
VerificationGatewayFactory,
} from "../../deps.ts";
import { IClock } from "../helpers/Clock.ts";
@@ -18,10 +19,12 @@ 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 getOptimismL1Fee from "../helpers/getOptimismL1Fee.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
export type AddBundleResponse = { hash: string } | {
failures: TransactionFailure[];
@@ -34,6 +37,7 @@ export default class BundleService {
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
isOptimism: env.IS_OPTIMISM,
};
unconfirmedBundles = new Set<Bundle>();
@@ -156,14 +160,16 @@ 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 +179,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 +207,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 +242,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 +307,7 @@ export default class BundleService {
aggregateBundle,
includedRows,
bundleOverheadCost,
bundleOverheadLen,
expectedFee,
expectedMaxCost,
failedRows,
@@ -268,11 +319,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",
@@ -361,7 +420,16 @@ export default class BundleService {
const profit = balanceAfter.sub(balanceBefore);
/** What we paid to process the bundle */
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
let cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
if (this.config.isOptimism) {
cost = cost.add(
await getOptimismL1Fee(
this.ethereumService.provider,
receipt.transactionHash,
),
);
}
/** Fees collected from users */
const actualFee = profit.add(cost);

View File

@@ -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,30 +45,26 @@ 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 {
if (Array.isArray(rawRow)) {
rawRow = {
id: rawRow[0],
status: rawRow[1],
hash: rawRow[2],
bundle: rawRow[3],
eligibleAfter: rawRow[4],
nextEligibilityDelay: rawRow[5],
submitError: rawRow[6],
receipt: rawRow[7],
id: rawRow[0] as number,
status: rawRow[1] as string,
hash: rawRow[2] as string,
bundle: rawRow[3] as string,
eligibleAfter: rawRow[4] as string,
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");

View File

@@ -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";
@@ -23,6 +26,8 @@ import toPublicKeyShort from "./helpers/toPublicKeyShort.ts";
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
import nil from "../helpers/nil.ts";
import hexToUint8Array from "../helpers/hexToUint8Array.ts";
import OptimismGasPriceOracle from "./OptimismGasPriceOracle.ts";
export type TxCheckResult = {
failures: TransactionFailure[];
@@ -64,30 +69,24 @@ 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 chainId: number,
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 +96,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 +139,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 +152,33 @@ 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,
chainId,
blsWalletWrapper,
blsWalletSigner,
verificationGatewayAddress,
utilitiesAddress,
verificationGateway,
aggregatorUtilities,
expanderEntryPoint,
bundleCompressor,
nextNonce,
);
}
@@ -218,9 +251,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 +322,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;
let response: ethers.providers.TransactionResponse;
try {
txResponse = await this.verificationGateway.processBundle(
...processBundleArgs,
);
response = 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
@@ -319,7 +364,10 @@ export default class EthereumService {
}
try {
return { type: "receipt" as const, value: await txResponse.wait() };
return {
type: "complete" as const,
value: await response.wait(),
};
} catch (error) {
return { type: "waitError" as const, value: error };
}
@@ -330,12 +378,12 @@ 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();
if (attemptResult.type === "receipt") {
if (attemptResult.type === "complete") {
return attemptResult.value;
}
@@ -364,8 +412,44 @@ export default class EthereumService {
throw new Error("Expected return or throw from attempt loop");
}
async GasConfig() {
const block = await this.provider.getBlock("latest");
/**
* Estimates the amount of effective gas needed to process the bundle using
* compression.
*
* Here 'effective' gas means the number you need to multiply by gasPrice in
* order to get the right fee. There are a few cases here:
*
* 1. L1 chains (used in testing, eg gethDev)
* - Effective gas is equal to regular gas
* 2. Arbitrum
* - The Arbitrum node already responds with effective gas when calling
* estimateGas
* 3. Optimism
* - We estimate Optimism's calculation for the amount of L1 gas it will
* charge for, and then convert that into an equivalend amount of L2 gas.
*/
async estimateEffectiveCompressedGas(bundle: Bundle): Promise<BigNumber> {
const compressedBundle = await this.bundleCompressor.compress(bundle);
let gasEstimate = await this.wallet.estimateGas({
to: this.expanderEntryPoint.address,
data: compressedBundle,
});
if (env.IS_OPTIMISM) {
const extraGasEstimate = await this.estimateOptimismL2GasNeededForL1Gas(
compressedBundle,
gasEstimate,
);
gasEstimate = gasEstimate.add(extraGasEstimate);
}
return gasEstimate;
}
async GasConfig(block?: ethers.providers.Block) {
block ??= await this.provider.getBlock("latest");
const previousBaseFee = block.baseFeePerGas;
assert(previousBaseFee !== null && previousBaseFee !== nil);
@@ -392,6 +476,66 @@ export default class EthereumService {
};
}
/**
* Estimates the L1 gas that Optimism will charge us for and expresses it as
* an amount of equivalent L2 gas.
*
* This is very similar to what Arbitrum does, but in Arbitrum it's built-in,
* and you actually sign for that additional L2 gas. On Optimism, you only
* sign for the actual L2 gas, and optimism just adds the L1 fee.
*
* For our purposes, this works as a way to normalize the behavior between
* the different chains.
*/
async estimateOptimismL2GasNeededForL1Gas(
compressedBundle: string,
gasLimit: BigNumber,
): Promise<BigNumber> {
const block = await this.provider.getBlock("latest");
const gasConfig = await this.GasConfig(block);
const txBytes = await this.wallet.signTransaction({
type: 2,
chainId: this.chainId,
nonce: this.nextNonce,
to: this.expanderEntryPoint.address,
data: compressedBundle,
...gasConfig,
gasLimit,
});
let l1Gas = 0;
for (const byte of hexToUint8Array(txBytes)) {
if (byte === 0) {
l1Gas += 4;
} else {
l1Gas += 16;
}
}
const gasOracle = new OptimismGasPriceOracle(this.provider);
const { l1BaseFee, overhead, scalar, decimals } = await gasOracle
.getAllParams();
const scalarNum = scalar.toNumber() / (10 ** decimals.toNumber());
l1Gas += overhead.toNumber();
assert(block.baseFeePerGas !== null && block.baseFeePerGas !== nil);
assert(env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil);
const adjustedL1BaseFee = l1BaseFee.toNumber() * scalarNum *
(1 + env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE / 100);
const feeRatio = adjustedL1BaseFee / block.baseFeePerGas.toNumber();
return BigNumber.from(
Math.ceil(feeRatio * l1Gas),
);
}
private static Wallet(
provider: ethers.providers.Provider,
privateKey: string,

View 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;
}

View 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' });
}
}

View File

@@ -0,0 +1,52 @@
import { BigNumber, ethers } from "../../deps.ts";
import assert from "../helpers/assert.ts";
import { OPTIMISM_GAS_PRICE_ORACLE_ADDRESS } from "../env.ts";
export default class OptimismGasPriceOracle {
constructor(
public provider: ethers.providers.Provider,
) {}
private async callFn(method: string, blockTag?: ethers.providers.BlockTag) {
const outputBytes = await this.provider.call({
to: OPTIMISM_GAS_PRICE_ORACLE_ADDRESS,
data: ethers.utils.id(method),
}, blockTag);
const result = ethers.utils.defaultAbiCoder.decode(
["uint256"],
outputBytes,
)[0];
assert(result instanceof BigNumber);
return result;
}
async l1BaseFee(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("l1BaseFee()", blockTag);
}
async overhead(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("overhead()", blockTag);
}
async scalar(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("scalar()", blockTag);
}
async decimals(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("decimals()", blockTag);
}
async getAllParams(blockTag?: ethers.providers.BlockTag) {
const [l1BaseFee, overhead, scalar, decimals] = await Promise.all([
this.l1BaseFee(blockTag),
this.overhead(blockTag),
this.scalar(blockTag),
this.decimals(blockTag),
]);
return { l1BaseFee, overhead, scalar, decimals };
}
}

View File

@@ -10,14 +10,15 @@ 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();
emit({ type: "starting" });
const clock = Clock.create();
@@ -37,8 +38,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 +63,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();

View File

@@ -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),
});

View File

@@ -1,4 +1,6 @@
import assert from "./helpers/assert.ts";
import {
optionalEnv,
optionalNumberEnv,
requireBigNumberEnv,
requireBoolEnv,
@@ -95,3 +97,38 @@ export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
export const BUNDLE_CHECKING_CONCURRENCY = requireIntEnv(
"BUNDLE_CHECKING_CONCURRENCY",
);
/**
* Optimism's strategy for charging for L1 fees requires special logic in the
* aggregator. In addition to gasEstimate * gasPrice, we need to replicate
* Optimism's calculation and pass it on to the user.
*/
export const IS_OPTIMISM = requireBoolEnv("IS_OPTIMISM");
/**
* Address for the Optimism gas price oracle contract. Required when
* IS_OPTIMISM is true.
*/
export const OPTIMISM_GAS_PRICE_ORACLE_ADDRESS = optionalEnv(
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS",
);
/**
* Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for
* the optimism-specific calculation. This gets passed on to users.
* Required when IS_OPTIMISM is true.
*/
export const OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE = optionalNumberEnv(
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE",
);
if (IS_OPTIMISM) {
assert(
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil,
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE is required when IS_OPTIMISM is true",
);
assert(
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS !== nil,
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS is required when IS_OPTIMISM is true",
);
}

View File

@@ -0,0 +1,50 @@
import { BigNumber, ethers } from "../../deps.ts";
import OptimismGasPriceOracle from "../app/OptimismGasPriceOracle.ts";
import assert from "./assert.ts";
import getRawTransaction from "./getRawTransaction.ts";
import hexToUint8Array from "./hexToUint8Array.ts";
import nil from "./nil.ts";
export default async function getOptimismL1Fee(
provider: ethers.providers.Provider,
txResponseOrHash: string | ethers.providers.TransactionResponse,
) {
const tx = typeof txResponseOrHash === "string"
? await provider.getTransaction(txResponseOrHash)
: txResponseOrHash;
const rawTx = await getRawTransaction(provider, tx);
let l1Gas = 0;
for (const byte of hexToUint8Array(rawTx)) {
if (byte === 0) {
l1Gas += 4;
} else {
l1Gas += 16;
}
}
const gasOracle = new OptimismGasPriceOracle(provider);
assert(tx.blockNumber !== nil);
const {
l1BaseFee,
overhead,
scalar,
decimals,
} = await gasOracle.getAllParams(tx.blockNumber);
l1Gas = l1Gas += overhead.toNumber();
const l1Fee = BigNumber
.from(l1Gas)
.mul(l1BaseFee)
.mul(scalar)
.div(
BigNumber.from(10).pow(decimals),
);
return l1Fee;
}

View File

@@ -0,0 +1,49 @@
import { ethers, pick } from "../../deps.ts";
import assert from "./assert.ts";
import nil from "./nil.ts";
export default async function getRawTransaction(
provider: ethers.providers.Provider,
txResponseOrHash: string | ethers.providers.TransactionResponse,
) {
const tx = typeof txResponseOrHash === "string"
? await provider.getTransaction(txResponseOrHash)
: txResponseOrHash;
const txHash = typeof txResponseOrHash === "string"
? txResponseOrHash
: tx.hash;
assert(typeof txHash === "string");
const { v, r, s } = tx;
assert(r !== nil);
const txBytes = ethers.utils.serializeTransaction(
pick(
tx,
"to",
"nonce",
"gasLimit",
...(tx.type === 2 ? [] : ["gasPrice"] as const),
"data",
"value",
"chainId",
"type",
...(tx.type !== 2 ? [] : [
"accessList",
"maxPriorityFeePerGas",
"maxFeePerGas",
] as const),
),
{ v, r, s },
);
const reconstructedHash = ethers.utils.keccak256(txBytes);
if (reconstructedHash !== txHash) {
throw new Error("Reconstructed hash did not match original hash");
}
return txBytes;
}

View File

@@ -0,0 +1,16 @@
import assert from "./assert.ts";
export default function hexToUint8Array(hex: string) {
assert(hex.startsWith("0x"));
assert(hex.length % 2 === 0);
const len = (hex.length - 2) / 2;
const result = new Uint8Array(len);
for (let i = 0; i < len; i++) {
const hexPos = 2 * i + 2;
result[i] = parseInt(hex.slice(hexPos, hexPos + 2), 16);
}
return result;
}

View File

@@ -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: [
{

View File

@@ -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

View File

@@ -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);

View File

@@ -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]);
}
});

View File

@@ -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,
},
];

View File

@@ -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;

View 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));
});

View File

@@ -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;
@@ -36,6 +37,7 @@ export const bundleServiceDefaultTestConfig:
maxAggregationDelayMillis: 5000,
maxUnconfirmedAggregations: 3,
maxEligibilityDelay: 300,
isOptimism: false,
};
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {
@@ -74,11 +76,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 +96,7 @@ export default class Fixture {
ethereumService.blsWalletSigner,
ethereumService,
aggregationStrategyDefaultTestConfig,
emit,
),
netCfg,
);
@@ -131,7 +133,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 +173,7 @@ export default class Fixture {
this.blsWalletSigner,
this.ethereumService,
aggregationStrategyConfig,
this.emit,
);
const bundleService = new BundleService(
@@ -245,7 +248,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 +264,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 +296,12 @@ export default class Fixture {
return wallets;
}
createHealthCheckService() {
const healthCheckService = new HealthService();
return healthCheckService;
}
async cleanup() {
for (const job of this.cleanupJobs) {
await job();

View File

@@ -29,6 +29,7 @@ Deno.test("parseBundleDto accepts dummy values", () => {
"operations": [
{
"nonce": "0x01",
"gas": "0x01",
"actions": [
{
"ethValue": "0x00",

View File

@@ -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"

View File

@@ -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`.

View File

@@ -0,0 +1,11 @@
# Audits
## 2021
[Hubble contracts, including core BLS contracts](https://github.com/thehubbleproject/hubble-contracts/blob/master/audits/2021-03-17%20Igor%20Gulamov.md)
## 2022
[BLS Wallet](./Sigma_Prime_-_Ethereum_Foundation_-_BLS_Wallet_Smart_Contract_Security_Assessment_Report_-_v1.0.pdf)
All critical & high issues were addressed, but re-audit was not conducted.

View File

@@ -142,7 +142,7 @@ const bundle = wallet.sign({
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
### Paying aggregator fees with native currency (ETH)
@@ -198,21 +198,6 @@ const bundle = wallet.sign({
});
```
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
```ts
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: safeFee, // fee amount
contractAddress: aggregatorAddress,
},
],
});
```
### Paying aggregator fees with custom currency (ERC20)
The aggregator must be set up to accept ERC20 tokens in order for this to work.
@@ -288,25 +273,6 @@ const bundle = wallet.sign({
});
```
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
```ts
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("transfer", [
aggregatorAddress,
safeFee, // fee amount
]),
},
],
});
```
## VerificationGateway
Exposes `VerificationGateway` and `VerificationGateway__factory` generated by

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.8.2-1452ef5",
"version": "0.9.0",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",

View 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;
}
}

View File

@@ -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}`, {

View File

@@ -2,10 +2,13 @@
import { ethers, BigNumber } from "ethers";
import { Deferrable } from "ethers/lib/utils";
import { ActionData, Bundle } from "./signer/types";
import { ActionData, Bundle, PublicKey } from "./signer/types";
import Aggregator, { BundleReceipt } from "./Aggregator";
import BlsSigner, {
TransactionBatchResponse,
// Used for sendTransactionBatch TSdoc comment
// eslint-disable-next-line no-unused-vars
TransactionBatch,
UncheckedBlsSigner,
_constructorGuard,
} from "./BlsSigner";
@@ -14,15 +17,28 @@ import BlsWalletWrapper from "./BlsWalletWrapper";
import {
AggregatorUtilities__factory,
BLSWallet__factory,
VerificationGateway__factory,
} from "../typechain-types";
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
/** Public key linked to actions parsed from a bundle */
export type PublicKeyLinkedToActions = {
publicKey: PublicKey;
actions: Array<ActionData>;
};
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
readonly aggregator: Aggregator;
readonly verificationGatewayAddress: string;
readonly aggregatorUtilitiesAddress: string;
signer!: BlsSigner;
/**
* @param aggregatorUrl The url for an aggregator instance
* @param verificationGatewayAddress Verification gateway contract address
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
* @param url Rpc url
* @param network The network the provider should connect to
*/
constructor(
aggregatorUrl: string,
verificationGatewayAddress: string,
@@ -36,56 +52,70 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
}
/**
* @param transaction Transaction request object
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
*/
override async estimateGas(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<BigNumber> {
if (!transaction.to) {
const resolvedTransaction = await ethers.utils.resolveProperties(
transaction,
);
if (!resolvedTransaction.to) {
throw new TypeError("Transaction.to should be defined");
}
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
// We do not want to pass the private key to this method.
if (!this.signer) {
throw new Error("Call provider.getSigner first");
if (!resolvedTransaction.from) {
throw new TypeError("Transaction.from should be defined");
}
const action: ActionData = {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
ethValue: resolvedTransaction.value?.toString() ?? "0",
contractAddress: resolvedTransaction.to.toString(),
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
};
const nonce = await BlsWalletWrapper.Nonce(
this.signer.wallet.PublicKey(),
this.verificationGatewayAddress,
this,
);
// 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]);
const feeEstimate = await this.aggregator.estimateFee(
this.signer.wallet.sign({
nonce,
actions: [...actionWithFeePaymentAction],
}),
// TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle
// There is no way to estimate the cost of a bundle without signing a bundle. The
// alternative would be to use a signer instance in this method which is undesirable,
// as this would result in tight coupling between a provider and a signer.
const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect(
throwawayPrivateKey,
this.verificationGatewayAddress,
this,
);
const bundle = await throwawayBlsWalletWrapper.signWithGasEstimate({
nonce,
actions: [...actionWithFeePaymentAction],
});
const feeEstimate = await this.aggregator.estimateFee(bundle);
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
return addSafetyPremiumToFee(feeRequired);
}
/**
* Sends transaction to be executed. Adds the signed bundle to the aggregator
*
* @param signedTransaction A signed bundle
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
signedTransaction: string | Promise<string>,
): Promise<ethers.providers.TransactionResponse> {
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
// We do not want to pass the private key to this method.
if (!this.signer) {
throw new Error("Call provider.getSigner first");
}
const resolvedTransaction = await signedTransaction;
const bundle: Bundle = JSON.parse(resolvedTransaction);
@@ -107,23 +137,20 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
};
return this.signer.constructTransactionResponse(
return await this._constructTransactionResponse(
actionData,
bundle.senderPublicKeys[0],
result.hash,
this.signer.wallet.address,
);
}
/**
* @param signedTransactionBatch A signed {@link TransactionBatch}
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
signedTransactionBatch: string,
): Promise<TransactionBatchResponse> {
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
// We do not want to pass the private key to this method.
if (!this.signer) {
throw new Error("Call provider.getSigner first");
}
const bundle: Bundle = JSON.parse(signedTransactionBatch);
const result = await this.aggregator.add(bundle);
@@ -132,35 +159,40 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
throw new Error(JSON.stringify(result.failures));
}
const actionData: Array<ActionData> = bundle.operations
.map((operation) => operation.actions)
.flat();
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
bundle.senderPublicKeys.map((publicKey, i) => {
const operation = bundle.operations[i];
const actions = operation.actions;
return this.signer.constructTransactionBatchResponse(
actionData,
return {
publicKey,
actions,
};
});
return await this._constructTransactionBatchResponse(
publicKeysLinkedToActions,
result.hash,
this.signer.wallet.address,
);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new BlsSigner instance
*/
override getSigner(
privateKey: string,
addressOrIndex?: string | number,
): BlsSigner {
if (this.signer) {
return this.signer;
}
const signer = new BlsSigner(
_constructorGuard,
this,
privateKey,
addressOrIndex,
);
this.signer = signer;
return signer;
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new UncheckedBlsSigner instance
*/
override getUncheckedSigner(
privateKey: string,
addressOrIndex?: string,
@@ -168,6 +200,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
*/
override async getTransactionReceipt(
transactionHash: string | Promise<string>,
): Promise<ethers.providers.TransactionReceipt> {
@@ -175,6 +216,17 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
* @param retries The number of retries to poll the receipt for
* @returns
*/
override async waitForTransaction(
transactionHash: string,
confirmations?: number,
@@ -187,6 +239,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
);
}
/**
* @param address The address that the method gets the transaction count from
* @param blockTag The specific block tag to get the transaction count from
* @returns The number of transactions an account has sent
*/
override async getTransactionCount(
address: string | Promise<string>,
blockTag?:
@@ -286,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(
@@ -295,4 +352,104 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
},
];
}
async _constructTransactionResponse(
action: ActionData,
publicKey: PublicKey,
hash: string,
nonce?: BigNumber,
): Promise<ethers.providers.TransactionResponse> {
const chainId = await this.send("eth_chainId", []);
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
publicKey,
this.verificationGatewayAddress,
this,
);
}
const verificationGateway = VerificationGateway__factory.connect(
this.verificationGatewayAddress,
this,
);
const from = await BlsWalletWrapper.AddressFromPublicKey(
publicKey,
verificationGateway,
);
return {
hash,
to: action.contractAddress,
from,
nonce: nonce.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId: parseInt(chainId, 16),
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
};
}
async _constructTransactionBatchResponse(
publicKeysLinkedToActions: Array<PublicKeyLinkedToActions>,
hash: string,
nonce?: BigNumber,
): Promise<TransactionBatchResponse> {
const chainId = await this.send("eth_chainId", []);
const verificationGateway = VerificationGateway__factory.connect(
this.verificationGatewayAddress,
this,
);
const transactions: Array<ethers.providers.TransactionResponse> = [];
for (const publicKeyLinkedToActions of publicKeysLinkedToActions) {
const from = await BlsWalletWrapper.AddressFromPublicKey(
publicKeyLinkedToActions.publicKey,
verificationGateway,
);
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
publicKeyLinkedToActions.publicKey,
this.verificationGatewayAddress,
this,
);
}
for (const action of publicKeyLinkedToActions.actions) {
if (action.contractAddress === this.aggregatorUtilitiesAddress) {
break;
}
transactions.push({
hash,
to: action.contractAddress,
from,
nonce: nonce!.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId: parseInt(chainId, 16),
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
});
}
}
return {
transactions,
awaitBatchReceipt: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
};
}
}

View 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;
}
}

View 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),
]);
}
}

View File

@@ -1,26 +1,22 @@
/* 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 from "./BlsProvider";
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 = {};
/**
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) transaction gas limit
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) miner tip aka priority fee
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) the maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
* @property nonce - integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
* @property chainId - chain ID that this transaction is valid on
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
*
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
* @property chainId - Chain ID that this transaction is valid on
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
*/
export type BatchOptions = {
@@ -33,14 +29,18 @@ export type BatchOptions = {
};
/**
* @property transactions - an array of transaction objects
* @property batchOptions - optional batch options taken into account by smart contract wallets
* @property transactions - An array of Ethers transaction objects
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
*/
export type TransactionBatch = {
transactions: Array<ethers.providers.TransactionRequest>;
batchOptions?: BatchOptions;
};
/**
* @property transactions - An array of Ethers transaction response objects
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
*/
export interface TransactionBatchResponse {
transactions: Array<ethers.providers.TransactionResponse>;
awaitBatchReceipt: (
@@ -58,10 +58,16 @@ export default class BlsSigner extends Signer {
readonly initPromise: Promise<void>;
/**
* @param constructorGuard Prevents BlsSigner constructor being called directly
* @param provider BlsProvider accociated with this signer
* @param privateKey Private key for the account this signer represents
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
*/
constructor(
constructorGuard: Record<string, unknown>,
provider: BlsProvider,
privateKey: string,
privateKey: string | Promise<string>,
readonly addressOrIndex?: string | number,
) {
super();
@@ -92,22 +98,41 @@ export default class BlsSigner extends Signer {
}
}
private async initializeWallet(privateKey: string) {
/** Instantiates a BLS Wallet and then connects the signer to it */
private async initializeWallet(privateKey: string | Promise<string>) {
const resolvedPrivateKey = await privateKey;
this.wallet = await BlsWalletWrapper.connect(
privateKey,
resolvedPrivateKey,
this.verificationGatewayAddress,
this.provider,
);
}
/**
* @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
*
* @remarks The transaction hash returned in the transaction response does
* NOT correspond to a transaction hash that can be viewed on a block
* explorer. It instead represents the bundle hash, which can be used to
* get a transaction receipt that has a hash that can be used on a block explorer
*
* @param transaction Transaction request object
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): 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(
this.wallet.PublicKey(),
@@ -116,18 +141,18 @@ export default class BlsSigner extends Signer {
);
const action: ActionData = {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
ethValue: validatedTransaction.value?.toString() ?? "0",
contractAddress: validatedTransaction.to!.toString(),
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
};
const feeEstimate = await this.provider.estimateGas(transaction);
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
const bundle = await this.wallet.signWithGasEstimate({
nonce,
actions: [...actionsWithSafeFee],
});
@@ -137,26 +162,30 @@ export default class BlsSigner extends Signer {
throw new Error(JSON.stringify(result.failures));
}
return this.constructTransactionResponse(
return await this.provider._constructTransactionResponse(
action,
bundle.senderPublicKeys[0],
result.hash,
this.wallet.address,
nonce,
);
}
/**
* @param transactionBatch A transaction batch object
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatchResponse> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
const validatedBatchOptions = await this._validateBatchOptions(
transactionBatch.batchOptions,
);
nonce = validatedBatchOptions.nonce as BigNumber;
nonce = BigNumber.from(validatedTransactionBatch.batchOptions!.nonce);
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
@@ -166,11 +195,7 @@ export default class BlsSigner extends Signer {
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction, i) => {
if (!transaction.to) {
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
}
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
@@ -182,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(
@@ -198,7 +225,7 @@ export default class BlsSigner extends Signer {
safeFee,
);
const bundle = this.wallet.sign({
const bundle = await this.wallet.signWithGasEstimate({
nonce,
actions: [...actionsWithSafeFee],
});
@@ -208,14 +235,27 @@ export default class BlsSigner extends Signer {
throw new Error(JSON.stringify(result.failures));
}
return this.constructTransactionBatchResponse(
actions,
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
bundle.senderPublicKeys.map((publicKey, i) => {
const operation = bundle.operations[i];
const actions = operation.actions;
return {
publicKey,
actions,
};
});
return await this.provider._constructTransactionBatchResponse(
publicKeysLinkedToActions,
result.hash,
this.wallet.address,
nonce,
);
}
/**
* @returns The address associated with the BlsSigner
*/
async getAddress(): Promise<string> {
await this.initPromise;
if (this._address) {
@@ -226,83 +266,6 @@ export default class BlsSigner extends Signer {
return this._address;
}
// Construct a response that follows the ethers TransactionResponse type
async constructTransactionResponse(
action: ActionData,
hash: string,
from: string,
nonce?: BigNumber,
): Promise<ethers.providers.TransactionResponse> {
await this.initPromise;
const chainId = await this.getChainId();
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
return {
hash,
to: action.contractAddress,
from,
nonce: nonce.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId,
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.provider.waitForTransaction(hash, confirmations);
},
};
}
async constructTransactionBatchResponse(
actions: Array<ActionData>,
hash: string,
from: string,
nonce?: BigNumber,
): Promise<TransactionBatchResponse> {
await this.initPromise;
const chainId = await this.getChainId();
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const transactions: Array<ethers.providers.TransactionResponse> =
actions.map((action) => {
return {
hash,
to: action.contractAddress,
from,
nonce: nonce!.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId,
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.provider.waitForTransaction(hash, confirmations);
},
};
});
return {
transactions,
awaitBatchReceipt: (confirmations?: number) => {
return this.provider.waitForTransaction(hash, confirmations);
},
};
}
/**
* This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider.
* The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients.
@@ -319,19 +282,23 @@ export default class BlsSigner extends Signer {
]);
}
/**
* @remarks Signs a transaction that can be executed by the BlsProvider
*
* @param transaction Transaction request object
* @returns A signed bundle as a string
*/
override async signTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
await this.initPromise;
if (!transaction.to) {
throw new TypeError("Transaction.to should be defined");
}
const validatedTransaction = await this._validateTransaction(transaction);
const action: ActionData = {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
ethValue: validatedTransaction.value?.toString() ?? "0",
contractAddress: validatedTransaction.to!.toString(),
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
};
const nonce = await BlsWalletWrapper.Nonce(
@@ -340,14 +307,14 @@ export default class BlsSigner extends Signer {
this.provider,
);
const feeEstimate = await this.provider.estimateGas(transaction);
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
const bundle = await this.wallet.signWithGasEstimate({
nonce,
actions: [...actionsWithSafeFee],
});
@@ -355,18 +322,24 @@ export default class BlsSigner extends Signer {
return JSON.stringify(bundleToDto(bundle));
}
/**
* Signs a transaction batch that can be executed by the BlsProvider
*
* @param transactionBatch A transaction batch object
* @returns A signed bundle containing all transactions from the transaction batch as a string
*/
async signTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<string> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
const validatedBatchOptions = await this._validateBatchOptions(
transactionBatch.batchOptions,
);
nonce = validatedBatchOptions.nonce as BigNumber;
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
@@ -376,11 +349,7 @@ export default class BlsSigner extends Signer {
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction, i) => {
if (!transaction.to) {
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
}
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
@@ -392,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(
@@ -408,7 +379,7 @@ export default class BlsSigner extends Signer {
safeFee,
);
const bundle = this.wallet.sign({
const bundle = await this.wallet.signWithGasEstimate({
nonce,
actions: [...actionsWithSafeFee],
});
@@ -416,8 +387,15 @@ export default class BlsSigner extends Signer {
return JSON.stringify(bundleToDto(bundle));
}
/** Sign a message */
// TODO: 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)) {
@@ -425,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 {
@@ -440,6 +429,9 @@ export default class BlsSigner extends Signer {
throw new Error("_signTypedData() is not implemented");
}
/**
* @returns A new Signer object which does not perform additional checks when sending a transaction
*/
connectUnchecked(): BlsSigner {
return new UncheckedBlsSigner(
_constructorGuard,
@@ -453,6 +445,10 @@ export default class BlsSigner extends Signer {
);
}
/**
* @param transaction Transaction request object
* @returns Transaction hash for the transaction, corresponds to a bundle hash
*/
async sendUncheckedTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
@@ -464,6 +460,54 @@ export default class BlsSigner extends Signer {
throw new Error("_legacySignMessage() is not implemented");
}
async _validateTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionRequest> {
const resolvedTransaction = await ethers.utils.resolveProperties(
transaction,
);
if (!resolvedTransaction.to) {
throw new TypeError("Transaction.to should be defined");
}
if (!resolvedTransaction.from) {
resolvedTransaction.from = await this.getAddress();
}
return resolvedTransaction;
}
async _validateTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatch> {
const signerAddress = await this.getAddress();
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
transactionBatch.transactions.map((transaction, i) => {
if (!transaction.to) {
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
}
if (!transaction.from) {
transaction.from = signerAddress;
}
return {
...transaction,
};
});
const validatedBatchOptions = transactionBatch.batchOptions
? await this._validateBatchOptions(transactionBatch.batchOptions)
: transactionBatch.batchOptions;
return {
transactions: validatedTransactions,
batchOptions: validatedBatchOptions,
};
}
async _validateBatchOptions(
batchOptions: BatchOptions,
): Promise<BatchOptions> {
@@ -481,6 +525,12 @@ export default class BlsSigner extends Signer {
}
export class UncheckedBlsSigner extends BlsSigner {
/**
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
*
* @param transaction Transaction request object
* @returns The transaction response object with only the transaction hash property populated with a valid value
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionResponse> {
@@ -489,7 +539,7 @@ export class UncheckedBlsSigner extends BlsSigner {
const transactionResponse = await super.sendTransaction(transaction);
return {
hash: transactionResponse.hash,
nonce: 1,
nonce: NaN,
gasLimit: BigNumber.from(0),
gasPrice: BigNumber.from(0),
data: "",

View File

@@ -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,

View File

@@ -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],

View 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]),
]);
}
}

View 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;
};
}

View 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);
}

View 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";

View 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);
}
}

View 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;

View File

@@ -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),

View File

@@ -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;
},
[],
);

View 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;
}
}

View 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;
}

View File

@@ -0,0 +1,5 @@
import { ethers } from "ethers";
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
export default SignerOrProvider;

View File

@@ -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(

View 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);
}

View File

@@ -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";

View File

@@ -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,

View File

@@ -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)],
);
};

View 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));
};

View File

@@ -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,
};
}

View File

@@ -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 {

View File

@@ -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[];
};

View File

@@ -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]),
),
);
};

View File

@@ -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: 0x7a69,
chainId: 0x539, // 1337
};
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
privateKey = await BlsSigner.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
blsProvider = new BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
@@ -60,51 +61,29 @@ describe("BlsProvider", () => {
expect(uncheckedBlsSigner).to.be.instanceOf(UncheckedBlsSigner);
});
it("should return a new signer if one has not been instantiated", async () => {
it("should return a new signer", async () => {
// Arrange
const newBlsProvider = new Experimental.BlsProvider(
const newVerificationGateway = "newMockVerificationGatewayAddress";
const newBlsProvider = new BlsProvider(
aggregatorUrl,
verificationGateway,
newVerificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
// Act
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const newPrivateKey = await BlsSigner.getRandomBlsPrivateKey();
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
// Assert
expect(newBlsSigner).to.not.equal(blsSigner);
expect(newBlsSigner).to.equal(newBlsProvider.getSigner(newPrivateKey));
});
it("should throw an error when this.signer has not been assigned", async () => {
// Arrange
const newBlsProvider = new Experimental.BlsProvider(
aggregatorUrl,
expect(newBlsSigner.provider.verificationGatewayAddress).to.not.equal(
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
const recipient = ethers.Wallet.createRandom().address;
const value = parseEther("1");
const transactionRequest = {
to: recipient,
value,
};
// Act
const gasEstimate = async () =>
await newBlsProvider.estimateGas(transactionRequest);
// Assert
await expect(gasEstimate()).to.be.rejectedWith(
Error,
"Call provider.getSigner first",
expect(newBlsSigner.provider.verificationGatewayAddress).to.equal(
newVerificationGateway,
);
});
@@ -112,6 +91,7 @@ describe("BlsProvider", () => {
// Arrange
const transaction = {
value: parseEther("1"),
// Explicitly omit 'to'
};
// Act
@@ -124,28 +104,21 @@ describe("BlsProvider", () => {
);
});
it("should throw an error sending a transaction when this.signer is not defined", async () => {
it("should throw an error estimating gas when 'transaction.from' has not been defined", async () => {
// Arrange
const newBlsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
const signedTransaction = blsSigner.signTransaction({
to: ethers.Wallet.createRandom().address,
const transaction = {
value: parseEther("1"),
});
to: ethers.Wallet.createRandom().address,
// Explicitly omit 'from'
};
// Act
const result = async () =>
await newBlsProvider.sendTransaction(signedTransaction);
const result = async () => await blsProvider.estimateGas(transaction);
// Assert
await expect(result()).to.be.rejectedWith(
Error,
"Call provider.getSigner first",
TypeError,
"Transaction.from should be defined",
);
});
@@ -159,4 +132,155 @@ describe("BlsProvider", () => {
// Assert
expect(connection).to.deep.equal(expectedConnection);
});
it("should throw an error when sending invalid signed transactions", async () => {
// Arrange
const invalidTransaction = "Invalid signed transaction";
// Act
const result = async () =>
await blsProvider.sendTransaction(invalidTransaction);
const batchResult = async () =>
await blsProvider.sendTransaction(invalidTransaction);
// Assert
await expect(result()).to.be.rejectedWith(
Error,
"Unexpected token I in JSON at position 0",
);
await expect(batchResult()).to.be.rejectedWith(
Error,
"Unexpected token I in JSON at position 0",
);
});
it("should get the polling interval", async () => {
// Arrange
const expectedpollingInterval = 4000; // default
const updatedInterval = 1000;
// Act
const pollingInterval = blsProvider.pollingInterval;
blsProvider.pollingInterval = updatedInterval;
const updatedPollingInterval = blsProvider.pollingInterval;
// Assert
expect(pollingInterval).to.equal(expectedpollingInterval);
expect(updatedPollingInterval).to.equal(updatedInterval);
});
it("should get the event listener count and remove all listeners", async () => {
blsProvider.on("block", () => {});
blsProvider.on("error", () => {});
expect(blsProvider.listenerCount("block")).to.equal(1);
expect(blsProvider.listenerCount("error")).to.equal(1);
expect(blsProvider.listenerCount()).to.equal(2);
blsProvider.removeAllListeners();
expect(blsProvider.listenerCount("block")).to.equal(0);
expect(blsProvider.listenerCount("error")).to.equal(0);
expect(blsProvider.listenerCount()).to.equal(0);
});
it("should return true and an array of listeners if polling", async () => {
// Arrange
const expectedListener = () => {};
// Act
blsProvider.on("block", expectedListener);
const listeners = blsProvider.listeners("block");
const isPolling = blsProvider.polling;
blsProvider.removeAllListeners();
// Assert
expect(listeners).to.deep.equal([expectedListener]);
expect(isPolling).to.be.true;
});
it("should be a provider", async () => {
// Arrange & Act
const isProvider = BlsProvider.isProvider(blsProvider);
const isProviderWithInvalidProvider = BlsProvider.isProvider(blsSigner);
// Assert
expect(isProvider).to.equal(true);
expect(isProviderWithInvalidProvider).to.equal(false);
});
it("should a return a promise which will stall until the network has heen established", async () => {
// Arrange
const expectedReady = { name: "localhost", chainId: 1337 };
// Act
const ready = await blsProvider.ready;
// 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",
);
});
});

View File

@@ -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`,
);
});
});

View File

@@ -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"],
},
]);
});
});
});

View File

@@ -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";

View 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",
);
});
});

View File

@@ -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);
});
});

View File

@@ -0,0 +1,25 @@
//SPDX-License-Identifier: MIT
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;
}
}

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: Unlicense
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
pragma abicoder v2;

View File

@@ -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;

View File

@@ -0,0 +1,94 @@
//SPDX-License-Identifier: MIT
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);
}
}

View File

@@ -0,0 +1,35 @@
//SPDX-License-Identifier: MIT
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;
}
}

View File

@@ -0,0 +1,81 @@
//SPDX-License-Identifier: MIT
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);
}
}

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: Unlicense
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4 <0.9.0;
pragma abicoder v2;
@@ -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++) {

View File

@@ -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);
}
}

View File

@@ -0,0 +1,316 @@
//SPDX-License-Identifier: MIT
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);
}
}

Some files were not shown because too many files have changed in this diff Show More