├── .env ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .solhint.json ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── bindings ├── balance_tracker.go ├── fee_disburser.go ├── go.mod └── go.sum ├── foundry.toml ├── logo.webp ├── remappings.txt ├── script ├── deploy │ ├── Utils.sol │ ├── l1 │ │ ├── SetGasLimit.sol │ │ └── tests │ │ │ ├── DeployTestTokenContracts.s.sol │ │ │ └── TestDeposits.s.sol │ └── l2 │ │ └── tests │ │ ├── DeployTestTokenContracts.s.sol │ │ └── TestWithdraw.s.sol └── universal │ ├── DoubleNestedMultisigBuilder.sol │ ├── IGnosisSafe.sol │ ├── MultisigBuilder.sol │ ├── MultisigDeploy.sol │ ├── MultisigScript.sol │ ├── NestedMultisigBuilder.sol │ ├── Signatures.sol │ └── Simulation.sol ├── src ├── Challenger1of2.sol ├── Test.sol ├── TestOwner.sol ├── Vetoer1of2.sol ├── fee-vault-fixes │ └── FeeVault.sol ├── revenue-share │ ├── BalanceTracker.sol │ └── FeeDisburser.sol └── smart-escrow │ └── SmartEscrow.sol └── test ├── Challenger1of2.t.sol ├── CommonTest.t.sol ├── MockERC20.t.sol ├── fee-vault-fixes └── e2e │ └── FeeVault.t.sol ├── revenue-share ├── BalanceTracker.t.sol ├── FeeDisburser.t.sol └── mocks │ ├── FeeVaultRevert.sol │ ├── OptimismWalletRevert.sol │ └── ReenterProcessFees.sol ├── smart-escrow ├── BaseSmartEscrow.t.sol ├── Constructor.t.sol ├── Release.t.sol ├── Resume.t.sol ├── Terminate.t.sol ├── UpdateBenefactor.t.sol ├── UpdateBeneficiary.t.sol └── WithdrawUnvestedTokens.t.sol └── universal ├── Counter.sol ├── DoubleNestedMultisigBuilder.t.sol ├── MultisigBuilder.t.sol └── NestedMultisigBuilder.t.sol /.env: -------------------------------------------------------------------------------- 1 | OP_COMMIT=3056348b21a07e584225310bbc27f1adce5ca2a9 2 | BASE_MAINNET_URL=https://mainnet.base.org 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | FOUNDRY_PROFILE: ci 8 | 9 | jobs: 10 | check: 11 | strategy: 12 | fail-fast: true 13 | 14 | name: Foundry project 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - name: Install Foundry 22 | uses: foundry-rs/foundry-toolchain@v1 23 | with: 24 | version: nightly 25 | 26 | - name: Show Forge version 27 | run: | 28 | forge --version 29 | 30 | - name: Run Forge fmt 31 | run: | 32 | forge fmt --check 33 | id: fmt 34 | 35 | - name: Install dependencies 36 | run: | 37 | make deps 38 | 39 | - name: Run Forge build 40 | run: | 41 | forge build --sizes 42 | id: build 43 | 44 | - name: Run Forge tests 45 | run: | 46 | forge test -vvv 47 | id: test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | cache/*** 3 | out/** 4 | 5 | .gitmodules 6 | 7 | /.idea/ 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Base 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | ## 4 | # Solidity Setup / Testing 5 | ## 6 | .PHONY: install-foundry 7 | install-foundry: 8 | curl -L https://foundry.paradigm.xyz | bash 9 | ~/.foundry/bin/foundryup 10 | 11 | .PHONY: deps 12 | deps: clean-lib checkout-op-commit 13 | forge install --no-git github.com/foundry-rs/forge-std \ 14 | github.com/OpenZeppelin/openzeppelin-contracts@v4.9.3 \ 15 | github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@v4.7.3 \ 16 | github.com/rari-capital/solmate@8f9b23f8838670afda0fd8983f2c41e8037ae6bc \ 17 | github.com/Vectorized/solady@862a0afd3e66917f50e987e91886b9b90c4018a1 \ 18 | github.com/safe-global/safe-smart-account@v1.4.1-3 19 | 20 | .PHONY: test 21 | test: 22 | forge test --ffi -vvv 23 | 24 | .PHONY: clean-lib 25 | clean-lib: 26 | rm -rf lib 27 | 28 | .PHONY: checkout-op-commit 29 | checkout-op-commit: 30 | [ -n "$(OP_COMMIT)" ] || (echo "OP_COMMIT must be set in .env" && exit 1) 31 | rm -rf lib/optimism 32 | mkdir -p lib/optimism 33 | cd lib/optimism; \ 34 | git init; \ 35 | git remote add origin https://github.com/ethereum-optimism/optimism.git; \ 36 | git fetch --depth=1 origin $(OP_COMMIT); \ 37 | git reset --hard FETCH_HEAD 38 | 39 | .PHONY: bindings 40 | bindings: 41 | go install github.com/ethereum/go-ethereum/cmd/abigen@v1.15.8 42 | forge build 43 | mkdir -p bindings 44 | abigen --abi out/BalanceTracker.sol/BalanceTracker.abi.json --pkg bindings --type BalanceTracker --out bindings/balance_tracker.go 45 | abigen --abi out/FeeDisburser.sol/FeeDisburser.abi.json --pkg bindings --type FeeDisburser --out bindings/fee_disburser.go 46 | cd bindings && go mod tidy -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Base](logo.webp) 2 | 3 | # contracts 4 | 5 | This repo contains contracts and scripts for Base. 6 | Note that Base primarily utilizes Optimism's bedrock contracts located in Optimism's repo [here](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock). 7 | For contract deployment artifacts, see [base-org/contract-deployments](https://github.com/base-org/contract-deployments). 8 | 9 | 10 | 11 | [![GitHub contributors](https://img.shields.io/github/contributors/base-org/contracts)](https://github.com/base-org/contracts/graphs/contributors) 12 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/base-org/contracts)](https://github.com/base-org/contracts/graphs/contributors) 13 | [![GitHub Stars](https://img.shields.io/github/stars/base-org/contracts.svg)](https://github.com/base-org/contracts/stargazers) 14 | ![GitHub repo size](https://img.shields.io/github/repo-size/base-org/contracts) 15 | [![GitHub](https://img.shields.io/github/license/base-org/contracts?color=blue)](https://github.com/base-org/contracts/blob/main/LICENSE) 16 | 17 | 18 | 19 | [![Website base.org](https://img.shields.io/website-up-down-green-red/https/base.org.svg)](https://base.org) 20 | [![Blog](https://img.shields.io/badge/blog-up-green)](https://base.mirror.xyz/) 21 | [![Docs](https://img.shields.io/badge/docs-up-green)](https://docs.base.org/) 22 | [![Discord](https://img.shields.io/discord/1067165013397213286?label=discord)](https://base.org/discord) 23 | [![Twitter Base](https://img.shields.io/twitter/follow/Base?style=social)](https://twitter.com/Base) 24 | 25 | 26 | 27 | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr-raw/base-org/contracts)](https://github.com/base-org/contracts/pulls) 28 | [![GitHub Issues](https://img.shields.io/github/issues-raw/base-org/contracts.svg)](https://github.com/base-org/contracts/issues) 29 | 30 | ### setup and testing 31 | 32 | - If you don't have foundry installed, run `make install-foundry`. 33 | - `make deps` 34 | - Test contracts: `make test` 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Bug bounty program 4 | 5 | In line with our strategy of being the safest way for users to access crypto: 6 | 7 | + Coinbase is extending our [best-in-industry][1] million-dollar [HackerOne bug bounty program][2] 8 | to cover the Base network, the Base bridge contracts, and Base infrastructure. 9 | 10 | + Coinbase's bug bounty program runs alongside Optimism's existing [Immunefi Bedrock bounty program][4] 11 | to support the open source [Bedrock][5] OP Stack framework. 12 | 13 | ## Reporting vulnerabilities 14 | 15 | All potential vulnerability reports can be submitted via the [HackerOne][6] 16 | platform. 17 | 18 | The HackerOne platform allows us to have a centralized and single reporting 19 | source for us to deliver optimized SLA's and results. All reports submitted to 20 | the platform are triaged around the clock by our team of Coinbase engineers 21 | with domain knowledge, assuring the best quality of review. 22 | 23 | For more information on reporting vulnerabilities and our HackerOne bug bounty 24 | program, view our [security program policies][7]. 25 | 26 | [1]: https://www.coinbase.com/blog/celebrating-10-years-of-our-bug-bounty-program 27 | [2]: https://hackerone.com/coinbase?type=team 28 | [3]: https://stack.optimism.io/ 29 | [4]: https://immunefi.com/bounty/optimism/ 30 | [5]: https://stack.optimism.io/docs/releases/bedrock/ 31 | [6]: https://hackerone.com/coinbase 32 | [7]: https://hackerone.com/coinbase?view_policy=true 33 | -------------------------------------------------------------------------------- /bindings/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/base/contracts/bindings 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require github.com/ethereum/go-ethereum v1.15.8 8 | 9 | require ( 10 | github.com/Microsoft/go-winio v0.6.2 // indirect 11 | github.com/StackExchange/wmi v1.2.1 // indirect 12 | github.com/bits-and-blooms/bitset v1.17.0 // indirect 13 | github.com/consensys/bavard v0.1.22 // indirect 14 | github.com/consensys/gnark-crypto v0.14.0 // indirect 15 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 16 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect 17 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect 18 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 19 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 20 | github.com/ethereum/go-verkle v0.2.2 // indirect 21 | github.com/fsnotify/fsnotify v1.6.0 // indirect 22 | github.com/go-ole/go-ole v1.3.0 // indirect 23 | github.com/google/uuid v1.3.0 // indirect 24 | github.com/gorilla/websocket v1.4.2 // indirect 25 | github.com/holiman/uint256 v1.3.2 // indirect 26 | github.com/mmcloughlin/addchain v0.4.0 // indirect 27 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect 28 | github.com/supranational/blst v0.3.14 // indirect 29 | github.com/tklauser/go-sysconf v0.3.12 // indirect 30 | github.com/tklauser/numcpus v0.6.1 // indirect 31 | golang.org/x/crypto v0.35.0 // indirect 32 | golang.org/x/sync v0.11.0 // indirect 33 | golang.org/x/sys v0.30.0 // indirect 34 | rsc.io/tmplfunc v0.0.3 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /bindings/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= 2 | github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 5 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 6 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 7 | github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= 8 | github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= 12 | github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 13 | github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= 14 | github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= 15 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 16 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= 18 | github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= 19 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= 20 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= 21 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= 22 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= 23 | github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= 24 | github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= 25 | github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= 26 | github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 27 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= 28 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= 29 | github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= 30 | github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= 31 | github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= 32 | github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 34 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 35 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 36 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 37 | github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= 38 | github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= 39 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 40 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= 42 | github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 43 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 44 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 45 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 46 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 47 | github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= 48 | github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= 49 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= 50 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 51 | github.com/ethereum/go-ethereum v1.15.8 h1:H6NilvRXFVoHiXZ3zkuTqKW5XcxjLZniV5UjxJt1GJU= 52 | github.com/ethereum/go-ethereum v1.15.8/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0= 53 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= 54 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 55 | github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk= 56 | github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs= 57 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 58 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 59 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= 60 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= 61 | github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= 62 | github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= 63 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 64 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 65 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 66 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 67 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= 71 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 72 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 73 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 74 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 75 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 76 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 77 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 78 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 79 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 80 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 82 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 83 | github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= 84 | github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= 85 | github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= 86 | github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= 87 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= 88 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= 89 | github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= 90 | github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= 91 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 92 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 93 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 94 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 95 | github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= 96 | github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= 97 | github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= 98 | github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 99 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= 100 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= 101 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 102 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 103 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= 104 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 105 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 106 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 107 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 108 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 109 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 110 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 111 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 112 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 113 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= 114 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= 115 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 116 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 117 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 118 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 119 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 120 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 121 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 122 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 123 | github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= 124 | github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 125 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 126 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 127 | github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= 128 | github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= 129 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 130 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 131 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 132 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 133 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 134 | github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 135 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 136 | github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= 137 | github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= 138 | github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= 139 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= 140 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 141 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 142 | github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= 143 | github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= 144 | github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= 145 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= 146 | github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= 147 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= 148 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 149 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 150 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 151 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 152 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= 153 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 154 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= 155 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 156 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 157 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 158 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 159 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 160 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 161 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 162 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 163 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 164 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 165 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 166 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 167 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 168 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 169 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 170 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 171 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 172 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= 173 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 174 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 175 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 176 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 177 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 178 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 179 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 180 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 181 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 182 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 183 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 184 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 185 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 186 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= 187 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 188 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 189 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 190 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 191 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 192 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 198 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 199 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 200 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 201 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 202 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 203 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 204 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 205 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 206 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 207 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 208 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 209 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 210 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 211 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 212 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 213 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | libs = ['lib'] 3 | fs_permissions = [ {access = "read-write", path = "./"} ] 4 | optimizer = true 5 | optimizer_runs = 999999 6 | solc_version = "0.8.15" 7 | extra_output_files = ["abi"] 8 | 9 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/base/contracts/ea4a921ba601b1c385a363777d6fc52b7392327b/logo.webp -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/ 2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts 3 | @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts 4 | @rari-capital/solmate/=lib/solmate/ 5 | @solady/=lib/solady/src/ 6 | -------------------------------------------------------------------------------- /script/deploy/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {stdJson} from "forge-std/StdJson.sol"; 6 | 7 | contract Utils is Script { 8 | using stdJson for string; 9 | 10 | struct DeployBedrockConfig { 11 | address baseFeeVaultRecipient; 12 | address batchSenderAddress; 13 | address controller; 14 | address deployerAddress; 15 | address finalSystemOwner; 16 | uint256 finalizationPeriodSeconds; 17 | uint256 gasPriceOracleOverhead; 18 | uint256 gasPriceOracleScalar; 19 | uint256 l1ChainId; 20 | address l1FeeVaultRecipient; 21 | uint256 l2BlockTime; 22 | uint256 l2ChainId; 23 | uint64 l2GenesisBlockGasLimit; 24 | address l2OutputOracleChallenger; 25 | address l2OutputOracleProposer; 26 | uint256 l2OutputOracleStartingBlockNumber; 27 | uint256 l2OutputOracleStartingTimestamp; 28 | uint256 l2OutputOracleSubmissionInterval; 29 | address p2pSequencerAddress; 30 | address portalGuardian; 31 | address proxyAdminOwner; 32 | address sequencerFeeVaultRecipient; 33 | } 34 | 35 | struct AddressesConfig { 36 | address AddressManager; 37 | address L1CrossDomainMessengerProxy; 38 | address L1ERC721BridgeProxy; 39 | address L1StandardBridgeProxy; 40 | address L2OutputOracleProxy; 41 | address OptimismMintableERC20FactoryProxy; 42 | address OptimismPortalProxy; 43 | address ProxyAdmin; 44 | address SystemConfigProxy; 45 | address SystemDictatorProxy; 46 | } 47 | 48 | struct AddressesL2ImplementationsConfig { 49 | address BaseFeeVault; 50 | address GasPriceOracle; 51 | address L1Block; 52 | address L1FeeVault; 53 | address L2CrossDomainMessenger; 54 | address L2ERC721Bridge; 55 | address L2StandardBridge; 56 | address L2ToL1MessagePasser; 57 | address OptimismMintableERC20Factory; 58 | address OptimismMintableERC721Factory; 59 | address SequencerFeeVault; 60 | } 61 | 62 | function getDeployBedrockConfig() external view returns (DeployBedrockConfig memory) { 63 | string memory root = vm.projectRoot(); 64 | string memory path = string.concat(root, "/inputs/foundry-config.json"); 65 | string memory json = vm.readFile(path); 66 | bytes memory deployBedrockConfigRaw = json.parseRaw(".deployConfig"); 67 | return abi.decode(deployBedrockConfigRaw, (DeployBedrockConfig)); 68 | } 69 | 70 | function readAddressesFile() external view returns (AddressesConfig memory) { 71 | string memory root = vm.projectRoot(); 72 | string memory addressPath = string.concat(root, "/inputs/addresses.json"); 73 | string memory addressJson = vm.readFile(addressPath); 74 | bytes memory addressRaw = vm.parseJson(addressJson); 75 | return abi.decode(addressRaw, (AddressesConfig)); 76 | } 77 | 78 | function readImplAddressesL2File() external view returns (AddressesL2ImplementationsConfig memory) { 79 | string memory root = vm.projectRoot(); 80 | string memory addressPath = string.concat(root, "/inputs/addresses-l2.json"); 81 | string memory addressJson = vm.readFile(addressPath); 82 | bytes memory addressRaw = vm.parseJson(addressJson); 83 | return abi.decode(addressRaw, (AddressesL2ImplementationsConfig)); 84 | } 85 | 86 | function writeAddressesFile(AddressesConfig memory cfg) external { 87 | string memory json = ""; 88 | 89 | // Proxy contract addresses 90 | vm.serializeAddress(json, "ProxyAdmin", cfg.ProxyAdmin); 91 | vm.serializeAddress(json, "AddressManager", cfg.AddressManager); 92 | vm.serializeAddress(json, "L1StandardBridgeProxy", cfg.L1StandardBridgeProxy); 93 | vm.serializeAddress(json, "L2OutputOracleProxy", cfg.L2OutputOracleProxy); 94 | vm.serializeAddress(json, "L1CrossDomainMessengerProxy", cfg.L1CrossDomainMessengerProxy); 95 | vm.serializeAddress(json, "OptimismPortalProxy", cfg.OptimismPortalProxy); 96 | vm.serializeAddress(json, "OptimismMintableERC20FactoryProxy", cfg.OptimismMintableERC20FactoryProxy); 97 | vm.serializeAddress(json, "L1ERC721BridgeProxy", cfg.L1ERC721BridgeProxy); 98 | vm.serializeAddress(json, "SystemConfigProxy", cfg.SystemConfigProxy); 99 | 100 | string memory finalJson = vm.serializeAddress(json, "SystemDictatorProxy", cfg.SystemDictatorProxy); 101 | 102 | finalJson.write(string.concat("unsorted.json")); 103 | } 104 | 105 | function writeImplAddressesL2File(AddressesL2ImplementationsConfig memory cfg) external { 106 | string memory json = ""; 107 | 108 | vm.serializeAddress(json, "BaseFeeVault", cfg.BaseFeeVault); 109 | vm.serializeAddress(json, "GasPriceOracle", cfg.GasPriceOracle); 110 | vm.serializeAddress(json, "L1Block", cfg.L1Block); 111 | vm.serializeAddress(json, "L1FeeVault", cfg.L1FeeVault); 112 | vm.serializeAddress(json, "L2CrossDomainMessenger", cfg.L2CrossDomainMessenger); 113 | vm.serializeAddress(json, "L2ERC721Bridge", cfg.L2ERC721Bridge); 114 | vm.serializeAddress(json, "L2StandardBridge", cfg.L2StandardBridge); 115 | vm.serializeAddress(json, "L2ToL1MessagePasser", cfg.L2ToL1MessagePasser); 116 | vm.serializeAddress(json, "SequencerFeeVault", cfg.SequencerFeeVault); 117 | vm.serializeAddress(json, "OptimismMintableERC20Factory", cfg.OptimismMintableERC20Factory); 118 | string memory finalJson = 119 | vm.serializeAddress(json, "OptimismMintableERC721Factory", cfg.OptimismMintableERC721Factory); 120 | 121 | finalJson.write(string.concat("unsortedl2Impls.json")); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /script/deploy/l1/SetGasLimit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {SystemConfig} from "@eth-optimism-bedrock/src/L1/SystemConfig.sol"; 5 | import {MultisigScript, IMulticall3, IGnosisSafe, Simulation} from "../../universal/MultisigScript.sol"; 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | contract SetGasLimit is MultisigScript { 9 | address internal SYSTEM_CONFIG_OWNER = vm.envAddress("SYSTEM_CONFIG_OWNER"); 10 | address internal L1_SYSTEM_CONFIG = vm.envAddress("L1_SYSTEM_CONFIG_ADDRESS"); 11 | 12 | /** 13 | * ----------------------------------------------------------- 14 | * Implemented Functions 15 | * ----------------------------------------------------------- 16 | */ 17 | function _fromGasLimit() internal view returns (uint64) { 18 | return uint64(vm.envUint("FROM_GAS_LIMIT")); 19 | } 20 | 21 | function _toGasLimit() internal view returns (uint64) { 22 | return uint64(vm.envUint("TO_GAS_LIMIT")); 23 | } 24 | 25 | function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { 26 | assert(SystemConfig(L1_SYSTEM_CONFIG).gasLimit() == _toGasLimit()); 27 | } 28 | 29 | function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { 30 | IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); 31 | 32 | calls[0] = IMulticall3.Call3Value({ 33 | target: L1_SYSTEM_CONFIG, 34 | allowFailure: false, 35 | callData: abi.encodeCall(SystemConfig.setGasLimit, (_toGasLimit())), 36 | value: 0 37 | }); 38 | 39 | return calls; 40 | } 41 | 42 | function _ownerSafe() internal view override returns (address) { 43 | return SYSTEM_CONFIG_OWNER; 44 | } 45 | 46 | // We need to expect that the gas limit will have been updated previously in our simulation 47 | // Use this override to specifically set the gas limit to the expected update value. 48 | function _simulationOverrides() internal view override returns (Simulation.StateOverride[] memory) { 49 | Simulation.StateOverride[] memory _stateOverrides = new Simulation.StateOverride[](1); 50 | Simulation.StorageOverride[] memory _storageOverrides = new Simulation.StorageOverride[](1); 51 | _storageOverrides[0] = Simulation.StorageOverride({ 52 | key: 0x0000000000000000000000000000000000000000000000000000000000000068, // slot of gas limit 53 | value: bytes32(uint256(_fromGasLimit())) 54 | }); 55 | // solhint-disable-next-line max-line-length 56 | _stateOverrides[0] = Simulation.StateOverride({contractAddress: L1_SYSTEM_CONFIG, overrides: _storageOverrides}); 57 | return _stateOverrides; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /script/deploy/l1/tests/DeployTestTokenContracts.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | import {Script} from "forge-std/Script.sol"; 7 | 8 | import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 9 | import {ERC721PresetMinterPauserAutoId} from 10 | "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 11 | 12 | // Deploys test token contracts on L1 to test Base Mainnet's bridging functionality 13 | contract DeployTestTokenContracts is Script { 14 | function run(address _tester) public { 15 | vm.startBroadcast(_tester); 16 | ERC20PresetMinterPauser erc20 = new ERC20PresetMinterPauser("L1 TEST ERC20", "L1T20"); 17 | console.log("TEST ERC20 deployed to: %s", address(erc20)); 18 | 19 | ERC721PresetMinterPauserAutoId erc721 = 20 | new ERC721PresetMinterPauserAutoId("L1 TEST ERC721", "L1T721", "not applicable"); 21 | console.log("TEST ERC721 deployed to: %s", address(erc721)); 22 | 23 | erc20.mint(_tester, 1_000_000 ether); 24 | erc721.mint(_tester); 25 | console.log("Minting to tester complete"); 26 | 27 | vm.stopBroadcast(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /script/deploy/l1/tests/TestDeposits.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | import {Script} from "forge-std/Script.sol"; 7 | 8 | import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 9 | import {ERC721PresetMinterPauserAutoId} from 10 | "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 11 | 12 | import {L1StandardBridge} from "@eth-optimism-bedrock/src/L1/L1StandardBridge.sol"; 13 | import {L1ERC721Bridge} from "@eth-optimism-bedrock/src/L1/L1ERC721Bridge.sol"; 14 | 15 | // Deposits funds to Base Mainnet to test its functionality 16 | contract DeployTestContracts is Script { 17 | function run( 18 | address _tester, 19 | address payable _l1StandardBridge, 20 | address _l1erc721Bridge, 21 | address payable _l1erc20, 22 | address _l1erc721, 23 | address _l2erc20, 24 | address _l2erc721 25 | ) public { 26 | vm.startBroadcast(_tester); 27 | ERC20PresetMinterPauser(_l1erc20).approve(_l1StandardBridge, 1_000_000 ether); 28 | ERC721PresetMinterPauserAutoId(_l1erc721).approve(_l1erc721Bridge, 0); 29 | 30 | console.log("Approvals to bridge contracts complete"); 31 | 32 | L1StandardBridge(_l1StandardBridge).depositERC20(_l1erc20, _l2erc20, 1_000_000 ether, 200_000, bytes("")); 33 | 34 | console.log("L1StandardBridge erc20 deposit complete"); 35 | 36 | L1ERC721Bridge(_l1erc721Bridge).bridgeERC721(_l1erc721, _l2erc721, 0, 200_000, bytes("")); 37 | 38 | console.log("L1ERC721Bridge erc721 deposit complete"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /script/deploy/l2/tests/DeployTestTokenContracts.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | import {Script} from "forge-std/Script.sol"; 7 | 8 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 9 | import {OptimismMintableERC20Factory} from "@eth-optimism-bedrock/src/universal/OptimismMintableERC20Factory.sol"; 10 | import {OptimismMintableERC721Factory} from "@eth-optimism-bedrock/src/universal/OptimismMintableERC721Factory.sol"; 11 | 12 | // Deploys test token contracts on L2 to test Base Mainnet functionality 13 | contract DeployTestTokenContracts is Script { 14 | function run(address _tester, address _l1erc20, address _l1erc721) public { 15 | vm.startBroadcast(_tester); 16 | address erc20 = OptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY) 17 | .createOptimismMintableERC20(_l1erc20, "L2 TEST ERC20", "L2T20"); 18 | console.log("Bridged erc20 deployed to: %s", address(erc20)); 19 | 20 | address erc721 = OptimismMintableERC721Factory(payable(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY)) 21 | .createOptimismMintableERC721(_l1erc721, "L2 TEST ERC721", "L2T721"); 22 | console.log("Bridged erc721 deployed to: %s", address(erc721)); 23 | 24 | vm.stopBroadcast(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /script/deploy/l2/tests/TestWithdraw.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | import {Script} from "forge-std/Script.sol"; 7 | 8 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 9 | import {L2StandardBridge} from "@eth-optimism-bedrock/src/L2/L2StandardBridge.sol"; 10 | import {L2ERC721Bridge} from "@eth-optimism-bedrock/src/L2/L2ERC721Bridge.sol"; 11 | 12 | // Withdraws tokens from L2 to L1 to test Base Mainnet's bridging functionality 13 | contract TestWithdraw is Script { 14 | function run(address _tester, address _l2erc20, address _l1erc721, address _l2erc721) public { 15 | vm.startBroadcast(_tester); 16 | L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).withdraw(_l2erc20, 10_000 ether, 200_000, bytes("")); 17 | console.log("erc20 withdrawal initiated"); 18 | 19 | L2ERC721Bridge(payable(Predeploys.L2_ERC721_BRIDGE)).bridgeERC721(_l2erc721, _l1erc721, 0, 200_000, bytes("")); 20 | console.log("erc721 withdrawal initiated"); 21 | 22 | vm.stopBroadcast(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/universal/DoubleNestedMultisigBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {MultisigScript} from "./MultisigScript.sol"; 5 | 6 | /** 7 | * @title DoubleNestedMultisigBuilder 8 | * @custom:deprecated Use `MultisigScript` instead. 9 | */ 10 | abstract contract DoubleNestedMultisigBuilder is MultisigScript { 11 | /* 12 | * @custom:deprecated Use `sign(address[] memory _safes)` instead. 13 | */ 14 | function sign(address _signerSafe, address _intermediateSafe) external { 15 | sign(_toArray(_signerSafe, _intermediateSafe)); 16 | } 17 | 18 | /* 19 | * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. 20 | */ 21 | function verify(address _signerSafe, address _intermediateSafe, bytes memory _signatures) public view { 22 | verify(_toArray(_signerSafe, _intermediateSafe), _signatures); 23 | } 24 | 25 | /* 26 | * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. 27 | */ 28 | function approveOnBehalfOfSignerSafe(address _signerSafe, address _intermediateSafe, bytes memory _signatures) 29 | public 30 | { 31 | approve(_toArray(_signerSafe, _intermediateSafe), _signatures); 32 | } 33 | 34 | /* 35 | * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. 36 | */ 37 | function approveOnBehalfOfIntermediateSafe(address _intermediateSafe) public { 38 | approve(_toArray(_intermediateSafe), ""); 39 | } 40 | 41 | /* 42 | * @custom:deprecated Use `simulate(bytes memory _signatures)` instead, with empty `_signatures`. 43 | */ 44 | function simulate() public { 45 | simulate(""); 46 | } 47 | 48 | /* 49 | * @custom:deprecated Use `run(bytes memory _signatures)` instead, with empty `_signatures`. 50 | */ 51 | function run() public { 52 | run(""); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /script/universal/IGnosisSafe.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only 2 | pragma solidity ^0.8.10; 3 | 4 | /// @title Enum - Collection of enums used in Safe contracts. 5 | /// @author Richard Meissner - @rmeissner 6 | abstract contract Enum { 7 | enum Operation { 8 | Call, 9 | DelegateCall 10 | } 11 | } 12 | 13 | /// @title IGnosisSafe - Gnosis Safe Interface 14 | interface IGnosisSafe { 15 | event AddedOwner(address owner); 16 | event ApproveHash(bytes32 indexed approvedHash, address indexed owner); 17 | event ChangedFallbackHandler(address handler); 18 | event ChangedGuard(address guard); 19 | event ChangedThreshold(uint256 threshold); 20 | event DisabledModule(address module); 21 | event EnabledModule(address module); 22 | event ExecutionFailure(bytes32 txHash, uint256 payment); 23 | event ExecutionFromModuleFailure(address indexed module); 24 | event ExecutionFromModuleSuccess(address indexed module); 25 | event ExecutionSuccess(bytes32 txHash, uint256 payment); 26 | event RemovedOwner(address owner); 27 | event SafeReceived(address indexed sender, uint256 value); 28 | event SafeSetup( 29 | address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler 30 | ); 31 | event SignMsg(bytes32 indexed msgHash); 32 | 33 | function VERSION() external view returns (string memory); 34 | function addOwnerWithThreshold(address owner, uint256 _threshold) external; 35 | function approveHash(bytes32 hashToApprove) external; 36 | function approvedHashes(address, bytes32) external view returns (uint256); 37 | function changeThreshold(uint256 _threshold) external; 38 | function checkNSignatures(bytes32 dataHash, bytes memory data, bytes memory signatures, uint256 requiredSignatures) 39 | external 40 | view; 41 | function checkSignatures(bytes32 dataHash, bytes memory data, bytes memory signatures) external view; 42 | function disableModule(address prevModule, address module) external; 43 | function domainSeparator() external view returns (bytes32); 44 | function enableModule(address module) external; 45 | function encodeTransactionData( 46 | address to, 47 | uint256 value, 48 | bytes memory data, 49 | Enum.Operation operation, 50 | uint256 safeTxGas, 51 | uint256 baseGas, 52 | uint256 gasPrice, 53 | address gasToken, 54 | address refundReceiver, 55 | uint256 _nonce 56 | ) external view returns (bytes memory); 57 | function execTransaction( 58 | address to, 59 | uint256 value, 60 | bytes memory data, 61 | Enum.Operation operation, 62 | uint256 safeTxGas, 63 | uint256 baseGas, 64 | uint256 gasPrice, 65 | address gasToken, 66 | address refundReceiver, 67 | bytes memory signatures 68 | ) external payable returns (bool success); 69 | function execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation) 70 | external 71 | returns (bool success); 72 | function execTransactionFromModuleReturnData(address to, uint256 value, bytes memory data, Enum.Operation operation) 73 | external 74 | returns (bool success, bytes memory returnData); 75 | function getChainId() external view returns (uint256); 76 | function getModulesPaginated(address start, uint256 pageSize) 77 | external 78 | view 79 | returns (address[] memory array, address next); 80 | function getOwners() external view returns (address[] memory); 81 | function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory); 82 | function getThreshold() external view returns (uint256); 83 | function getTransactionHash( 84 | address to, 85 | uint256 value, 86 | bytes memory data, 87 | Enum.Operation operation, 88 | uint256 safeTxGas, 89 | uint256 baseGas, 90 | uint256 gasPrice, 91 | address gasToken, 92 | address refundReceiver, 93 | uint256 _nonce 94 | ) external view returns (bytes32); 95 | function isModuleEnabled(address module) external view returns (bool); 96 | function isOwner(address owner) external view returns (bool); 97 | function nonce() external view returns (uint256); 98 | function removeOwner(address prevOwner, address owner, uint256 _threshold) external; 99 | function requiredTxGas(address to, uint256 value, bytes memory data, Enum.Operation operation) 100 | external 101 | returns (uint256); 102 | function setFallbackHandler(address handler) external; 103 | function setGuard(address guard) external; 104 | function setup( 105 | address[] memory _owners, 106 | uint256 _threshold, 107 | address to, 108 | bytes memory data, 109 | address fallbackHandler, 110 | address paymentToken, 111 | uint256 payment, 112 | address paymentReceiver 113 | ) external; 114 | function signedMessages(bytes32) external view returns (uint256); 115 | function simulateAndRevert(address targetContract, bytes memory calldataPayload) external; 116 | function swapOwner(address prevOwner, address oldOwner, address newOwner) external; 117 | } 118 | -------------------------------------------------------------------------------- /script/universal/MultisigBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {MultisigScript} from "./MultisigScript.sol"; 5 | 6 | /** 7 | * @title MultisigBuilder 8 | * @custom:deprecated Use `MultisigScript` instead. 9 | */ 10 | abstract contract MultisigBuilder is MultisigScript { 11 | /* 12 | * @custom:deprecated Use `sign(address[] memory _safes)` instead, with an empty array. 13 | */ 14 | function sign() external { 15 | sign(new address[](0)); 16 | } 17 | 18 | /* 19 | * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead, 20 | * with an empty array. 21 | */ 22 | function verify(bytes memory _signatures) public view { 23 | verify(new address[](0), _signatures); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /script/universal/MultisigDeploy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | import {SafeProxyFactory} from "lib/safe-smart-account/contracts/proxies/SafeProxyFactory.sol"; 6 | import {SafeL2} from "lib/safe-smart-account/contracts/SafeL2.sol"; 7 | import {Safe} from "lib/safe-smart-account/contracts/Safe.sol"; 8 | import {SafeProxy} from "lib/safe-smart-account/contracts/proxies/SafeProxy.sol"; 9 | 10 | /** 11 | * @title MultisigDeployScript 12 | * @notice Deploys a hierarchy of Safe multisig wallets where later safes can reference earlier ones as owners 13 | * 14 | * @dev This script enables deployment of nested/hierarchical multisig structures for complex governance systems. 15 | * Safes are deployed in array order, allowing later safes to use previously deployed safes as owners. 16 | * 17 | * EXAMPLE JSON CONFIGURATION (config/safes-nested.json): 18 | * { 19 | * "safeCount": 3, 20 | * "safes": [ 21 | * { 22 | * "label": "Treasury", 23 | * "threshold": 2, 24 | * "owners": [ 25 | * "0x1234567890123456789012345678901234567890", 26 | * "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" 27 | * ], 28 | * "ownerRefIndices": [] 29 | * }, 30 | * { 31 | * "label": "Operations", 32 | * "threshold": 1, 33 | * "owners": [ 34 | * "0x9876543210987654321098765432109876543210" 35 | * ], 36 | * "ownerRefIndices": [0] 37 | * }, 38 | * { 39 | * "label": "Governance", 40 | * "threshold": 2, 41 | * "owners": [], 42 | * "ownerRefIndices": [0, 1] 43 | * } 44 | * ] 45 | * } 46 | * 47 | * CONFIGURATION FIELDS: 48 | * - label: Human-readable name for the safe 49 | * - threshold: Number of signatures required for transactions 50 | * - owners: Array of direct address owners (EOAs or other contracts) 51 | * - ownerRefIndices: Array of indices referencing previously deployed safes as owners 52 | * 53 | * DEPLOYMENT ORDER MATTERS: 54 | * - Safes must be ordered so that any referenced safe (via ownerRefIndices) appears earlier in the array 55 | * - This ensures referenced safes are already deployed when needed as owners 56 | */ 57 | contract MultisigDeployScript is Script { 58 | // Safe v1.4.1-3 Addresses 59 | address public constant SINGLETON = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762; 60 | address public constant FACTORY_PROXY = 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67; 61 | address public constant COMPATBILITY_FALLBACK_HANDLER = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99; 62 | 63 | struct SafeWallet { 64 | string label; 65 | uint256 threshold; 66 | address[] owners; 67 | uint256[] ownerRefIndices; 68 | } 69 | 70 | // Track deployed safes and their predicted addresses 71 | mapping(string => address) public deployedSafes; 72 | 73 | SafeWallet[] public safes; 74 | 75 | function setUp() public { 76 | // Read configs from JSON 77 | string memory configPath = vm.envString("MULTISIG_CONFIG_PATH"); 78 | string memory json = vm.readFile(configPath); 79 | console.log("Using config path:", configPath); 80 | 81 | // Read safeCount directly from JSON 82 | uint256 safeCount = vm.parseJsonUint(json, ".safeCount"); 83 | console.log("Reading", safeCount, "safes from configuration"); 84 | 85 | // Parse each safe individually field by field 86 | for (uint256 i = 0; i < safeCount; i++) { 87 | string memory basePath = string(abi.encodePacked(".safes[", vm.toString(i), "]")); 88 | 89 | safes.push(); 90 | 91 | // Parse simple fields (these work reliably) 92 | safes[i].label = vm.parseJsonString(json, string(abi.encodePacked(basePath, ".label"))); 93 | safes[i].threshold = vm.parseJsonUint(json, string(abi.encodePacked(basePath, ".threshold"))); 94 | 95 | // Parse arrays (these are more reliable when done individually) 96 | safes[i].owners = vm.parseJsonAddressArray(json, string(abi.encodePacked(basePath, ".owners"))); 97 | safes[i].ownerRefIndices = 98 | vm.parseJsonUintArray(json, string(abi.encodePacked(basePath, ".ownerRefIndices"))); 99 | } 100 | 101 | // Print out the config to verify parsing worked 102 | for (uint256 i = 0; i < safes.length; i++) { 103 | console.log("Safe:", safes[i].label); 104 | console.log(" Owners:", safes[i].owners.length); 105 | console.log(" OwnerRefIndices:", safes[i].ownerRefIndices.length); 106 | console.log(" Threshold:", safes[i].threshold); 107 | } 108 | 109 | // Print out first safe owners for verification 110 | console.log("First safe owners:"); 111 | for (uint256 j = 0; j < safes[0].owners.length; j++) { 112 | console.log(" Owner", j, ":", safes[0].owners[j]); 113 | } 114 | } 115 | 116 | function run() public { 117 | SafeProxyFactory factory = SafeProxyFactory(FACTORY_PROXY); 118 | 119 | // Start broadcasting transactions 120 | vm.startBroadcast(); 121 | 122 | console.log("Deploying", safes.length, "Safe(s) in sequence"); 123 | console.log("--------------------"); 124 | 125 | uint256 baseNonce = block.timestamp; 126 | console.log("Base nonce:", baseNonce); 127 | 128 | // Deploy each Safe with its configuration in array order 129 | for (uint256 i = 0; i < safes.length; i++) { 130 | SafeWallet memory config = safes[i]; 131 | uint256 saltNonce = baseNonce + i; 132 | 133 | console.log("Deploying Safe:", config.label); 134 | console.log(" Index:", i); 135 | console.log(" Salt Nonce:", saltNonce); 136 | 137 | // Resolve owner addresses (combine direct owners + referenced safe addresses) 138 | address[] memory resolvedOwners = resolveOwnerAddresses(config, safes); 139 | 140 | console.log(" Total Owners:", resolvedOwners.length); 141 | console.log(" Direct Owners:", config.owners.length); 142 | console.log(" Safe References:", config.ownerRefIndices.length); 143 | console.log(" Threshold:", config.threshold); 144 | 145 | // Compose initializer data with resolved owners 146 | bytes memory initializer = abi.encodeCall( 147 | Safe.setup, 148 | ( 149 | resolvedOwners, 150 | config.threshold, 151 | address(0), // to 152 | hex"", // data 153 | COMPATBILITY_FALLBACK_HANDLER, 154 | address(0), // payment token 155 | 0, // payment 156 | payable(address(0)) // payment receiver 157 | ) 158 | ); 159 | 160 | // Deploy Safe with calculated nonce 161 | SafeProxy safe = factory.createProxyWithNonce(SINGLETON, initializer, saltNonce); 162 | 163 | // Store deployed address 164 | deployedSafes[config.label] = address(safe); 165 | 166 | console.log(" Deployed at:", address(safe)); 167 | 168 | // Log resolved owners 169 | for (uint256 k = 0; k < resolvedOwners.length; k++) { 170 | console.log(" Owner", k, ":", resolvedOwners[k]); 171 | } 172 | console.log("--------------------"); 173 | } 174 | 175 | vm.stopBroadcast(); 176 | 177 | // Verify all deployments 178 | console.log("Deployment Summary:"); 179 | console.log("=================="); 180 | for (uint256 i = 0; i < safes.length; i++) { 181 | SafeWallet memory config = safes[i]; 182 | console.log("Safe:", config.label); 183 | console.log(" Address:", deployedSafes[config.label]); 184 | console.log(" Salt Nonce:", baseNonce + i); 185 | console.log("--------------------"); 186 | } 187 | } 188 | 189 | function resolveOwnerAddresses(SafeWallet memory config, SafeWallet[] memory safes) 190 | internal 191 | view 192 | returns (address[] memory) 193 | { 194 | uint256 totalOwners = config.owners.length + config.ownerRefIndices.length; 195 | address[] memory resolved = new address[](totalOwners); 196 | 197 | // Add direct address owners 198 | for (uint256 i = 0; i < config.owners.length; i++) { 199 | resolved[i] = config.owners[i]; 200 | } 201 | 202 | // Add referenced safe addresses (they must already be deployed due to array order) 203 | for (uint256 i = 0; i < config.ownerRefIndices.length; i++) { 204 | uint256 refIndex = config.ownerRefIndices[i]; 205 | string memory refLabel = safes[refIndex].label; 206 | address refAddr = deployedSafes[refLabel]; 207 | require(refAddr != address(0), string(abi.encodePacked("Reference not deployed: ", refLabel))); 208 | resolved[config.owners.length + i] = refAddr; 209 | } 210 | 211 | return resolved; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /script/universal/NestedMultisigBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {MultisigScript} from "./MultisigScript.sol"; 5 | 6 | /** 7 | * @title NestedMultisigBuilder 8 | * @custom:deprecated Use `MultisigScript` instead. 9 | */ 10 | abstract contract NestedMultisigBuilder is MultisigScript { 11 | /* 12 | * @custom:deprecated Use `sign(address[] memory _safes)` instead. 13 | */ 14 | function sign(address _signerSafe) external { 15 | sign(_toArray(_signerSafe)); 16 | } 17 | 18 | /* 19 | * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. 20 | */ 21 | function verify(address _signerSafe, bytes memory _signatures) public view { 22 | verify(_toArray(_signerSafe), _signatures); 23 | } 24 | 25 | /* 26 | * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. 27 | */ 28 | function approve(address _signerSafe, bytes memory _signatures) public { 29 | approve(_toArray(_signerSafe), _signatures); 30 | } 31 | 32 | /* 33 | * @custom:deprecated Use `simulate(bytes memory _signatures)` instead, with empty `_signatures`. 34 | */ 35 | function simulate() public { 36 | simulate(""); 37 | } 38 | 39 | /* 40 | * @custom:deprecated Use `run(bytes memory _signatures)` instead, with empty `_signatures`. 41 | */ 42 | function run() public { 43 | run(""); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /script/universal/Signatures.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | 7 | import {Bytes} from "@eth-optimism-bedrock/src/libraries/Bytes.sol"; 8 | import {LibSort} from "@solady/utils/LibSort.sol"; 9 | 10 | import {IGnosisSafe} from "./IGnosisSafe.sol"; 11 | 12 | library Signatures { 13 | function prepareSignatures(address _safe, bytes32 hash, bytes memory _signatures) 14 | internal 15 | view 16 | returns (bytes memory) 17 | { 18 | // prepend the prevalidated signatures to the signatures 19 | address[] memory approvers = getApprovers(_safe, hash); 20 | bytes memory prevalidatedSignatures = genPrevalidatedSignatures(approvers); 21 | _signatures = bytes.concat(prevalidatedSignatures, _signatures); 22 | 23 | // safe requires all signatures to be unique, and sorted ascending by public key 24 | return sortUniqueSignatures( 25 | _safe, _signatures, hash, IGnosisSafe(_safe).getThreshold(), prevalidatedSignatures.length 26 | ); 27 | } 28 | 29 | function genPrevalidatedSignatures(address[] memory _addresses) internal pure returns (bytes memory) { 30 | LibSort.sort(_addresses); 31 | bytes memory signatures; 32 | for (uint256 i; i < _addresses.length; i++) { 33 | signatures = bytes.concat(signatures, genPrevalidatedSignature(_addresses[i])); 34 | } 35 | return signatures; 36 | } 37 | 38 | function genPrevalidatedSignature(address _address) internal pure returns (bytes memory) { 39 | uint8 v = 1; 40 | bytes32 s = bytes32(0); 41 | bytes32 r = bytes32(uint256(uint160(_address))); 42 | return abi.encodePacked(r, s, v); 43 | } 44 | 45 | function getApprovers(address _safe, bytes32 hash) internal view returns (address[] memory) { 46 | // get a list of owners that have approved this transaction 47 | IGnosisSafe safe = IGnosisSafe(_safe); 48 | uint256 threshold = safe.getThreshold(); 49 | address[] memory owners = safe.getOwners(); 50 | address[] memory approvers = new address[](threshold); 51 | uint256 approverIndex; 52 | for (uint256 i; i < owners.length; i++) { 53 | address owner = owners[i]; 54 | uint256 approved = safe.approvedHashes(owner, hash); 55 | if (approved == 1) { 56 | approvers[approverIndex] = owner; 57 | approverIndex++; 58 | if (approverIndex == threshold) { 59 | return approvers; 60 | } 61 | } 62 | } 63 | address[] memory subset = new address[](approverIndex); 64 | for (uint256 i; i < approverIndex; i++) { 65 | subset[i] = approvers[i]; 66 | } 67 | return subset; 68 | } 69 | 70 | // solhint-disable max-line-length 71 | /** 72 | * @notice Sorts the signatures in ascending order of the signer's address, and removes any duplicates. 73 | * @dev see https://github.com/safe-global/safe-smart-account/blob/1ed486bb148fe40c26be58d1b517cec163980027/contracts/Safe.sol#L265-L334 74 | * @param _safe Address of the Safe that should verify the signatures. 75 | * @param _signatures Signature data that should be verified. 76 | * Can be packed ECDSA signature ({bytes32 r}{bytes32 s}{uint8 v}), contract signature (EIP-1271) or approved hash. 77 | * Can be suffixed with EIP-1271 signatures after threshold*65 bytes. 78 | * @param dataHash Hash that is signed. 79 | * @param threshold Number of signatures required to approve the transaction. 80 | * @param dynamicOffset Offset to add to the `s` value of any EIP-1271 signature. 81 | * Can be used to accommodate any additional signatures prepended to the array. 82 | * If prevalidated signatures were prepended, this should be the length of those signatures. 83 | */ 84 | function sortUniqueSignatures( 85 | address _safe, 86 | bytes memory _signatures, 87 | bytes32 dataHash, 88 | uint256 threshold, 89 | uint256 dynamicOffset 90 | ) internal view returns (bytes memory) { 91 | bytes memory sorted; 92 | uint256 count = uint256(_signatures.length / 0x41); 93 | uint256[] memory addressesAndIndexes = new uint256[](threshold); 94 | address[] memory uniqueAddresses = new address[](threshold); 95 | uint256 j; 96 | for (uint256 i; i < count; i++) { 97 | (address owner, bool isOwner) = extractOwner(_safe, _signatures, dataHash, i); 98 | if (!isOwner) { 99 | continue; 100 | } 101 | 102 | // skip duplicate owners 103 | uint256 k; 104 | for (; k < j; k++) { 105 | if (uniqueAddresses[k] == owner) break; 106 | } 107 | if (k < j) continue; 108 | 109 | uniqueAddresses[j] = owner; 110 | addressesAndIndexes[j] = uint256(uint256(uint160(owner)) << 0x60 | i); // address in first 160 bits, index in second 96 bits 111 | j++; 112 | 113 | // we have enough signatures to reach the threshold 114 | if (j == threshold) break; 115 | } 116 | require(j == threshold, "not enough signatures"); 117 | 118 | LibSort.sort(addressesAndIndexes); 119 | for (uint256 i; i < count; i++) { 120 | uint256 index = addressesAndIndexes[i] & 0xffffffff; 121 | (uint8 v, bytes32 r, bytes32 s) = signatureSplit(_signatures, index); 122 | if (v == 0) { 123 | // The `s` value is used by safe as a lookup into the signature bytes. 124 | // Increment by the offset so that the lookup location remains correct. 125 | s = bytes32(uint256(s) + dynamicOffset); 126 | } 127 | sorted = bytes.concat(sorted, abi.encodePacked(r, s, v)); 128 | } 129 | 130 | // append the non-static part of the signatures (can contain EIP-1271 signatures if contracts are signers) 131 | // if there were any duplicates detected above, they will be safely ignored by Safe's checkNSignatures method 132 | sorted = appendRemainingBytes(sorted, _signatures); 133 | 134 | return sorted; 135 | } 136 | 137 | function extractOwner(address _safe, bytes memory _signatures, bytes32 dataHash, uint256 i) 138 | internal 139 | view 140 | returns (address, bool) 141 | { 142 | (uint8 v, bytes32 r, bytes32 s) = signatureSplit(_signatures, i); 143 | address owner = extractOwner(dataHash, r, s, v); 144 | bool isOwner = IGnosisSafe(_safe).isOwner(owner); 145 | if (!isOwner) { 146 | console.log("---\nSkipping the following signature, which was recovered to a non-owner address %s:", owner); 147 | console.logBytes(abi.encodePacked(r, s, v)); 148 | } 149 | return (owner, isOwner); 150 | } 151 | 152 | function extractOwner(bytes32 dataHash, bytes32 r, bytes32 s, uint8 v) internal pure returns (address) { 153 | if (v <= 1) { 154 | return address(uint160(uint256(r))); 155 | } 156 | if (v > 30) { 157 | return ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s); 158 | } 159 | return ecrecover(dataHash, v, r, s); 160 | } 161 | 162 | // see https://github.com/safe-global/safe-contracts/blob/1ed486bb148fe40c26be58d1b517cec163980027/contracts/common/SignatureDecoder.sol 163 | function signatureSplit(bytes memory signatures, uint256 pos) 164 | internal 165 | pure 166 | returns (uint8 v, bytes32 r, bytes32 s) 167 | { 168 | assembly { 169 | let signaturePos := mul(0x41, pos) 170 | r := mload(add(signatures, add(signaturePos, 0x20))) 171 | s := mload(add(signatures, add(signaturePos, 0x40))) 172 | v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) 173 | } 174 | } 175 | 176 | function appendRemainingBytes(bytes memory a1, bytes memory a2) internal pure returns (bytes memory) { 177 | if (a2.length > a1.length) { 178 | a1 = bytes.concat(a1, Bytes.slice(a2, a1.length, a2.length - a1.length)); 179 | } 180 | return a1; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /script/universal/Simulation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // solhint-disable no-console 5 | import {console} from "forge-std/console.sol"; 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | import {IGnosisSafe} from "./IGnosisSafe.sol"; 9 | 10 | library Simulation { 11 | address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); 12 | Vm internal constant vm = Vm(VM_ADDRESS); 13 | 14 | struct StateOverride { 15 | address contractAddress; 16 | StorageOverride[] overrides; 17 | } 18 | 19 | struct StorageOverride { 20 | bytes32 key; 21 | bytes32 value; 22 | } 23 | 24 | struct Payload { 25 | address from; 26 | address to; 27 | bytes data; 28 | StateOverride[] stateOverrides; 29 | } 30 | 31 | function simulateFromSimPayload(Payload memory simPayload) internal returns (Vm.AccountAccess[] memory) { 32 | // solhint-disable-next-line max-line-length 33 | require(simPayload.from != address(0), "Simulator::simulateFromSimPayload: from address cannot be zero address"); 34 | require(simPayload.to != address(0), "Simulator::simulateFromSimPayload: to address cannot be zero address"); 35 | 36 | // Apply state overrides. 37 | StateOverride[] memory stateOverrides = simPayload.stateOverrides; 38 | for (uint256 i; i < stateOverrides.length; i++) { 39 | StateOverride memory stateOverride = stateOverrides[i]; 40 | StorageOverride[] memory storageOverrides = stateOverride.overrides; 41 | for (uint256 j; j < storageOverrides.length; j++) { 42 | StorageOverride memory storageOverride = storageOverrides[j]; 43 | vm.store(stateOverride.contractAddress, storageOverride.key, storageOverride.value); 44 | } 45 | } 46 | 47 | // Execute the call in forge and return the state diff. 48 | vm.startStateDiffRecording(); 49 | vm.prank(simPayload.from); 50 | (bool ok, bytes memory returnData) = address(simPayload.to).call(simPayload.data); 51 | Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); 52 | require(ok, string.concat("Simulator::simulateFromSimPayload failed: ", vm.toString(returnData))); 53 | require(accesses.length > 0, "Simulator::simulateFromSimPayload: No state changes"); 54 | return accesses; 55 | } 56 | 57 | function overrideSafeThresholdApprovalAndNonce(address _safe, uint256 _nonce, address owner, bytes32 dataHash) 58 | internal 59 | view 60 | returns (StateOverride memory) 61 | { 62 | // solhint-disable-next-line max-line-length 63 | StateOverride memory state = StateOverride({contractAddress: _safe, overrides: new StorageOverride[](0)}); 64 | state = addThresholdOverride(_safe, state); 65 | state = addNonceOverride(_safe, state, _nonce); 66 | state = addApprovalOverride(state, owner, dataHash); 67 | return state; 68 | } 69 | 70 | function overrideSafeThresholdAndNonce(address _safe, uint256 _nonce) 71 | internal 72 | view 73 | returns (StateOverride memory) 74 | { 75 | StateOverride memory state = StateOverride({contractAddress: _safe, overrides: new StorageOverride[](0)}); 76 | state = addThresholdOverride(_safe, state); 77 | state = addNonceOverride(_safe, state, _nonce); 78 | return state; 79 | } 80 | 81 | function addApprovalOverride(StateOverride memory _state, address owner, bytes32 dataHash) 82 | internal 83 | pure 84 | returns (StateOverride memory) 85 | { 86 | return addOverride( 87 | _state, 88 | StorageOverride({ 89 | key: keccak256(abi.encode(dataHash, keccak256(abi.encode(owner, uint256(8))))), 90 | value: bytes32(uint256(0x1)) 91 | }) 92 | ); 93 | } 94 | 95 | function addThresholdOverride(address _safe, StateOverride memory _state) 96 | internal 97 | view 98 | returns (StateOverride memory) 99 | { 100 | // get the threshold and check if we need to override it 101 | if (IGnosisSafe(_safe).getThreshold() == 1) return _state; 102 | 103 | // set the threshold (slot 4) to 1 104 | return addOverride(_state, StorageOverride({key: bytes32(uint256(0x4)), value: bytes32(uint256(0x1))})); 105 | } 106 | 107 | function addOwnerOverride(address _safe, StateOverride memory _state, address _owner) 108 | internal 109 | view 110 | returns (StateOverride memory) 111 | { 112 | // get the owners and check if _owner is an owner 113 | address[] memory owners = IGnosisSafe(_safe).getOwners(); 114 | for (uint256 i; i < owners.length; i++) { 115 | if (owners[i] == _owner) return _state; 116 | } 117 | 118 | // set the ownerCount (slot 3) to 1 119 | _state = addOverride(_state, StorageOverride({key: bytes32(uint256(0x3)), value: bytes32(uint256(0x1))})); 120 | // override the owner mapping (slot 2), which requires two key/value pairs: { 0x1: _owner, _owner: 0x1 } 121 | _state = addOverride( 122 | _state, 123 | StorageOverride({ 124 | key: bytes32(0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0), // keccak256(1 || 2) 125 | value: bytes32(uint256(uint160(_owner))) 126 | }) 127 | ); 128 | return addOverride( 129 | _state, StorageOverride({key: keccak256(abi.encode(_owner, uint256(2))), value: bytes32(uint256(0x1))}) 130 | ); 131 | } 132 | 133 | function addNonceOverride(address _safe, StateOverride memory _state, uint256 _nonce) 134 | internal 135 | view 136 | returns (StateOverride memory) 137 | { 138 | // get the nonce and check if we need to override it 139 | if (IGnosisSafe(_safe).nonce() == _nonce) return _state; 140 | 141 | // set the nonce (slot 5) to the desired value 142 | return addOverride(_state, StorageOverride({key: bytes32(uint256(0x5)), value: bytes32(_nonce)})); 143 | } 144 | 145 | function addOverride(StateOverride memory _state, StorageOverride memory _override) 146 | internal 147 | pure 148 | returns (StateOverride memory) 149 | { 150 | StorageOverride[] memory overrides = new StorageOverride[](_state.overrides.length + 1); 151 | for (uint256 i; i < _state.overrides.length; i++) { 152 | overrides[i] = _state.overrides[i]; 153 | } 154 | overrides[_state.overrides.length] = _override; 155 | return StateOverride({contractAddress: _state.contractAddress, overrides: overrides}); 156 | } 157 | 158 | function logSimulationLink(address _to, bytes memory _data, address _from) internal view { 159 | logSimulationLink(_to, _data, _from, new StateOverride[](0)); 160 | } 161 | 162 | function logSimulationLink(address _to, bytes memory _data, address _from, StateOverride[] memory _overrides) 163 | internal 164 | view 165 | { 166 | string memory proj = vm.envOr("TENDERLY_PROJECT", string("TENDERLY_PROJECT")); 167 | string memory username = vm.envOr("TENDERLY_USERNAME", string("TENDERLY_USERNAME")); 168 | bool includeOverrides; 169 | 170 | // the following characters are url encoded: []{} 171 | string memory stateOverrides = "%5B"; 172 | for (uint256 i; i < _overrides.length; i++) { 173 | StateOverride memory _override = _overrides[i]; 174 | 175 | if (_override.overrides.length == 0) { 176 | continue; 177 | } 178 | 179 | includeOverrides = true; 180 | 181 | if (i > 0) stateOverrides = string.concat(stateOverrides, ","); 182 | stateOverrides = string.concat( 183 | stateOverrides, 184 | "%7B\"contractAddress\":\"", 185 | vm.toString(_override.contractAddress), 186 | "\",\"storage\":%5B" 187 | ); 188 | for (uint256 j; j < _override.overrides.length; j++) { 189 | if (j > 0) stateOverrides = string.concat(stateOverrides, ","); 190 | stateOverrides = string.concat( 191 | stateOverrides, 192 | "%7B\"key\":\"", 193 | vm.toString(_override.overrides[j].key), 194 | "\",\"value\":\"", 195 | vm.toString(_override.overrides[j].value), 196 | "\"%7D" 197 | ); 198 | } 199 | stateOverrides = string.concat(stateOverrides, "%5D%7D"); 200 | } 201 | stateOverrides = string.concat(stateOverrides, "%5D"); 202 | 203 | string memory str = string.concat( 204 | "https://dashboard.tenderly.co/", 205 | username, 206 | "/", 207 | proj, 208 | "/simulator/new?network=", 209 | vm.toString(block.chainid), 210 | "&contractAddress=", 211 | vm.toString(_to), 212 | "&from=", 213 | vm.toString(_from) 214 | ); 215 | 216 | if (includeOverrides) { 217 | str = string.concat(str, "&stateOverrides=", stateOverrides); 218 | } 219 | 220 | if (bytes(str).length + _data.length * 2 > 7980) { 221 | // tenderly's nginx has issues with long URLs, so print the raw input data separately 222 | str = string.concat(str, "\nInsert the following hex into the 'Raw input data' field:"); 223 | console.log(str); 224 | console.log(vm.toString(_data)); 225 | } else { 226 | str = string.concat(str, "&rawFunctionInput=", vm.toString(_data)); 227 | console.log(str); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Challenger1of2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 5 | 6 | /** 7 | * @title Challenger1of2 8 | * @dev This contract serves the role of the Challenger, defined in L2OutputOracle.sol: 9 | * https://github.com/ethereum-optimism/optimism/blob/3580bf1b41d80fcb2b895d5610836bfad27fc989/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol 10 | * It enforces a simple 1 of 2 design, where neither party can remove the other's 11 | * permissions to execute a Challenger call. 12 | */ 13 | contract Challenger1of2 { 14 | using Address for address; 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | CONSTANTS 18 | //////////////////////////////////////////////////////////////*/ 19 | /** 20 | * @dev The address of Optimism's signer (likely a multisig) 21 | */ 22 | address public immutable OP_SIGNER; 23 | 24 | /** 25 | * @dev The address of counterparty's signer (likely a multisig) 26 | */ 27 | address public immutable OTHER_SIGNER; 28 | 29 | /** 30 | * @dev The address of the L2OutputOracleProxy contract. 31 | */ 32 | address public immutable L2_OUTPUT_ORACLE_PROXY; 33 | 34 | /*////////////////////////////////////////////////////////////// 35 | EVENTS 36 | //////////////////////////////////////////////////////////////*/ 37 | /** 38 | * @dev Emitted when a Challenger call is made by a signer. 39 | * @param _caller The signer making the call. 40 | * @param _data The data of the call being made. 41 | * @param _result The result of the call being made. 42 | */ 43 | event ChallengerCallExecuted(address indexed _caller, bytes _data, bytes _result); 44 | 45 | /*////////////////////////////////////////////////////////////// 46 | Constructor 47 | //////////////////////////////////////////////////////////////*/ 48 | /** 49 | * @dev Constructor to set the values of the constants. 50 | * @param _opSigner Address of Optimism signer. 51 | * @param _otherSigner Address of counterparty signer. 52 | * @param _l2OutputOracleProxy Address of the L2OutputOracleProxy contract. 53 | */ 54 | constructor(address _opSigner, address _otherSigner, address _l2OutputOracleProxy) { 55 | require(_opSigner != address(0), "Challenger1of2: opSigner cannot be zero address"); 56 | require(_otherSigner != address(0), "Challenger1of2: otherSigner cannot be zero address"); 57 | require(_l2OutputOracleProxy.isContract(), "Challenger1of2: l2OutputOracleProxy must be a contract"); 58 | 59 | OP_SIGNER = _opSigner; 60 | OTHER_SIGNER = _otherSigner; 61 | L2_OUTPUT_ORACLE_PROXY = _l2OutputOracleProxy; 62 | } 63 | 64 | /*////////////////////////////////////////////////////////////// 65 | External Functions 66 | //////////////////////////////////////////////////////////////*/ 67 | /** 68 | * @dev Executes a call as the Challenger (must be called by 69 | * Optimism or counterparty signer). 70 | * @param _data Data for function call. 71 | */ 72 | function execute(bytes memory _data) external { 73 | require( 74 | msg.sender == OTHER_SIGNER || msg.sender == OP_SIGNER, 75 | "Challenger1of2: must be an approved signer to execute" 76 | ); 77 | 78 | bytes memory result = Address.functionCall(L2_OUTPUT_ORACLE_PROXY, _data, "Challenger1of2: failed to execute"); 79 | 80 | emit ChallengerCallExecuted(msg.sender, _data, result); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract Test { 5 | uint256 public number; 6 | 7 | function setNumber(uint256 newNumber) public { 8 | number = newNumber; 9 | } 10 | 11 | function increment() public { 12 | number++; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/TestOwner.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract TestOwner { 5 | uint256 public number; 6 | address public owner; 7 | 8 | constructor(address _owner) { 9 | require(_owner != address(0), "Owner cannot be zero address"); 10 | number = 0; 11 | owner = _owner; 12 | } 13 | 14 | function increment() external { 15 | if (msg.sender != owner) { 16 | revert("Only owner can increment"); 17 | } 18 | number++; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Vetoer1of2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 5 | import {DelayedVetoable} from "@eth-optimism-bedrock/src/L1/DelayedVetoable.sol"; 6 | 7 | /// @title Vetoer1of2 8 | /// 9 | /// @dev This contract serves the role of the Vetoer, defined in DelayedVetoable.sol: 10 | /// https://github.com/ethereum-optimism/optimism/blob/d72fb46daf3a6831cb01a78931f8c6e0d52ae243/packages/contracts-bedrock/src/L1/DelayedVetoable.sol 11 | /// It enforces a simple 1 of 2 design, where neither party can remove the other's 12 | /// permissions to execute a Veto call. 13 | /// 14 | contract Vetoer1of2 { 15 | using Address for address; 16 | 17 | ///////////////////////////////////////////////////////////// 18 | // CONSTANTS // 19 | ///////////////////////////////////////////////////////////// 20 | 21 | /// @notice The address of Optimism's signer (likely a multisig) 22 | address public immutable opSigner; 23 | 24 | /// @notice The address of counterparty's signer (likely a multisig) 25 | address public immutable otherSigner; 26 | 27 | /// @notice The address of the DelayedVetoable contract. 28 | address public immutable delayedVetoable; 29 | 30 | ////////////////////////////////////////////////////////////// 31 | // EVENTS // 32 | ////////////////////////////////////////////////////////////// 33 | 34 | /// @notice Emitted when a Veto call is made by a signer. 35 | /// 36 | /// @param caller The signer making the call. 37 | /// @param result The result of the call being made. 38 | event VetoCallExecuted(address indexed caller, bytes result); 39 | 40 | ////////////////////////////////////////////////////////////// 41 | // ERRORS // 42 | ////////////////////////////////////////////////////////////// 43 | 44 | /// @notice Thrown at deployment if `opSigner` is the zero address. 45 | error OpSignerCantBeZeroAddress(); 46 | 47 | /// @notice Thrown at deployment if `otherSigner` is the zero address. 48 | error OtherSignerCantBeZeroAddress(); 49 | 50 | /// @notice Thrown at deployment if `initiator` is the zero address. 51 | error InitiatorCantBeZeroAddress(); 52 | 53 | /// @notice Thrown at deployment if `target` is the zero address. 54 | error TargetCantBeZeroAddress(); 55 | 56 | /// @notice Thrown when calling 'veto()' from an unauthorized signer. 57 | error SenderIsNotWhitelistedSigner(); 58 | 59 | ////////////////////////////////////////////////////////////// 60 | // Constructor // 61 | ////////////////////////////////////////////////////////////// 62 | 63 | /// @notice Constructor initializing the immutable variables and deploying the `DelayedVetoable` 64 | /// contract. 65 | /// 66 | /// @dev The `DelayedVetoable` contract is deployed in this constructor to easily establish 67 | /// the link between both contracts. 68 | /// 69 | /// @custom:reverts OpSignerCantBeZeroAddress() if `opSigner_` is the zero address. 70 | /// @custom:reverts OtherSignerCantBeZeroAddress() if `otherSigner_` is the zero address. 71 | /// @custom:reverts InitiatorCantBeZeroAddress() if `initiator` is the zero address. 72 | /// @custom:reverts TargetCantBeZeroAddress() if `target` is the zero address. 73 | /// 74 | /// @param opSigner_ Address of Optimism signer. 75 | /// @param otherSigner_ Address of counterparty signer. 76 | /// @param initiator Address of the initiator. 77 | /// @param target Address of the target. 78 | constructor(address opSigner_, address otherSigner_, address initiator, address target) { 79 | if (opSigner_ == address(0)) { 80 | revert OpSignerCantBeZeroAddress(); 81 | } 82 | 83 | if (otherSigner_ == address(0)) { 84 | revert OtherSignerCantBeZeroAddress(); 85 | } 86 | 87 | if (initiator == address(0)) { 88 | revert InitiatorCantBeZeroAddress(); 89 | } 90 | 91 | if (target == address(0)) { 92 | revert TargetCantBeZeroAddress(); 93 | } 94 | 95 | opSigner = opSigner_; 96 | otherSigner = otherSigner_; 97 | 98 | delayedVetoable = address( 99 | new DelayedVetoable({ 100 | vetoer_: address(this), 101 | initiator_: initiator, 102 | target_: target, 103 | operatingDelay_: 14 days 104 | }) 105 | ); 106 | } 107 | 108 | ////////////////////////////////////////////////////////////// 109 | // External Functions // 110 | ////////////////////////////////////////////////////////////// 111 | 112 | /// @notice Passthrough for either signer to execute a veto on the `DelayedVetoable` contract. 113 | /// 114 | /// @custom:reverts SenderIsNotWhitelistedSigner() if not called by `opSigner` or `otherSigner`. 115 | function veto() external { 116 | if (msg.sender != otherSigner && msg.sender != opSigner) { 117 | revert SenderIsNotWhitelistedSigner(); 118 | } 119 | 120 | bytes memory result = Address.functionCall({ 121 | target: delayedVetoable, 122 | data: msg.data, 123 | errorMessage: "Vetoer1of2: failed to execute" 124 | }); 125 | 126 | emit VetoCallExecuted({caller: msg.sender, result: result}); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/fee-vault-fixes/FeeVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | /// @title FeeVault 5 | /// @notice The FeeVault contract is intended to: 6 | /// 1. Be upgraded to by the Base FeeVault contracts 7 | /// 2. Set `totalProcessed` to the correct value 8 | /// 3. Be upgraded from to back to Optimism's FeeVault 9 | contract FeeVault { 10 | /// @notice Total amount of wei processed by the contract. 11 | uint256 public totalProcessed; 12 | 13 | /** 14 | * @notice Sets total processed to its correct value. 15 | * @param _correctTotalProcessed The correct total processed value. 16 | */ 17 | function setTotalProcessed(uint256 _correctTotalProcessed) external { 18 | totalProcessed = _correctTotalProcessed; 19 | } 20 | 21 | /** 22 | * @notice Allow the contract to receive ETH. 23 | */ 24 | receive() external payable {} 25 | } 26 | -------------------------------------------------------------------------------- /src/revenue-share/BalanceTracker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 5 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 6 | import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 7 | 8 | import {SafeCall} from "@eth-optimism-bedrock/src/libraries/SafeCall.sol"; 9 | 10 | /** 11 | * @title BalanceTracker 12 | * @dev Funds system addresses and sends the remaining profits to the profit wallet. 13 | */ 14 | contract BalanceTracker is ReentrancyGuardUpgradeable { 15 | using Address for address; 16 | /*////////////////////////////////////////////////////////////// 17 | Constants 18 | //////////////////////////////////////////////////////////////*/ 19 | /** 20 | * @dev The maximum number of system addresses that can be funded. 21 | */ 22 | 23 | uint256 public constant MAX_SYSTEM_ADDRESS_COUNT = 20; 24 | 25 | /*////////////////////////////////////////////////////////////// 26 | Immutables 27 | //////////////////////////////////////////////////////////////*/ 28 | /** 29 | * @dev The address of the wallet receiving profits. 30 | */ 31 | address payable public immutable PROFIT_WALLET; 32 | 33 | /*////////////////////////////////////////////////////////////// 34 | VARIABLES 35 | //////////////////////////////////////////////////////////////*/ 36 | /** 37 | * @dev The system addresses being funded. 38 | */ 39 | address payable[] public systemAddresses; 40 | /** 41 | * @dev The target balances for system addresses. 42 | */ 43 | uint256[] public targetBalances; 44 | 45 | /*////////////////////////////////////////////////////////////// 46 | Events 47 | //////////////////////////////////////////////////////////////*/ 48 | /** 49 | * @dev Emitted when the BalanceTracker sends funds to a system address. 50 | * @param _systemAddress The system address being funded. 51 | * @param _success A boolean denoting whether a fund send occurred and its success or failure. 52 | * @param _balanceNeeded The amount of funds the given system address needs to reach its target balance. 53 | * @param _balanceSent The amount of funds sent to the system address. 54 | */ 55 | event ProcessedFunds( 56 | address indexed _systemAddress, bool indexed _success, uint256 _balanceNeeded, uint256 _balanceSent 57 | ); 58 | /** 59 | * @dev Emitted when the BalanceTracker attempts to send funds to the profit wallet. 60 | * @param _profitWallet The address of the profit wallet. 61 | * @param _success A boolean denoting the success or failure of fund send. 62 | * @param _balanceSent The amount of funds sent to the profit wallet. 63 | */ 64 | event SentProfit(address indexed _profitWallet, bool indexed _success, uint256 _balanceSent); 65 | /** 66 | * @dev Emitted when funds are received. 67 | * @param _sender The address sending funds. 68 | * @param _amount The amount of funds received from the sender. 69 | */ 70 | event ReceivedFunds(address indexed _sender, uint256 _amount); 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | Constructor 74 | //////////////////////////////////////////////////////////////*/ 75 | /** 76 | * @dev Constructor for the BalanceTracker contract that sets an immutable variable. 77 | * @param _profitWallet The address to send remaining ETH profits to. 78 | */ 79 | constructor(address payable _profitWallet) { 80 | require(_profitWallet != address(0), "BalanceTracker: PROFIT_WALLET cannot be address(0)"); 81 | 82 | PROFIT_WALLET = _profitWallet; 83 | 84 | _disableInitializers(); 85 | } 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | External Functions 89 | //////////////////////////////////////////////////////////////*/ 90 | /** 91 | * @dev Initializes the BalanceTracker contract. 92 | * @param _systemAddresses The system addresses being funded. 93 | * @param _targetBalances The target balances for system addresses. 94 | */ 95 | function initialize(address payable[] memory _systemAddresses, uint256[] memory _targetBalances) 96 | external 97 | reinitializer(2) 98 | { 99 | uint256 systemAddressesLength = _systemAddresses.length; 100 | require(systemAddressesLength > 0, "BalanceTracker: systemAddresses cannot have a length of zero"); 101 | require( 102 | systemAddressesLength <= MAX_SYSTEM_ADDRESS_COUNT, 103 | "BalanceTracker: systemAddresses cannot have a length greater than 20" 104 | ); 105 | require( 106 | systemAddressesLength == _targetBalances.length, 107 | "BalanceTracker: systemAddresses and targetBalances length must be equal" 108 | ); 109 | for (uint256 i; i < systemAddressesLength;) { 110 | require(_systemAddresses[i] != address(0), "BalanceTracker: systemAddresses cannot contain address(0)"); 111 | require(_targetBalances[i] > 0, "BalanceTracker: targetBalances cannot contain 0 target"); 112 | unchecked { 113 | i++; 114 | } 115 | } 116 | 117 | systemAddresses = _systemAddresses; 118 | targetBalances = _targetBalances; 119 | 120 | __ReentrancyGuard_init(); 121 | } 122 | 123 | /** 124 | * @dev Funds system addresses and sends remaining profits to the profit wallet. 125 | * 126 | */ 127 | function processFees() external nonReentrant { 128 | uint256 systemAddressesLength = systemAddresses.length; 129 | require(systemAddressesLength > 0, "BalanceTracker: systemAddresses cannot have a length of zero"); 130 | // Refills balances of systems addresses up to their target balances 131 | for (uint256 i; i < systemAddressesLength;) { 132 | refillBalanceIfNeeded(systemAddresses[i], targetBalances[i]); 133 | unchecked { 134 | i++; 135 | } 136 | } 137 | 138 | // Send remaining profits to profit wallet 139 | uint256 valueToSend = address(this).balance; 140 | bool success = SafeCall.send(PROFIT_WALLET, gasleft(), valueToSend); 141 | emit SentProfit(PROFIT_WALLET, success, valueToSend); 142 | } 143 | 144 | /** 145 | * @dev Fallback function to receive funds from L2 fee withdrawals and additional top up funds if 146 | * L2 fees are insufficient to fund L1 system addresses. 147 | */ 148 | receive() external payable { 149 | emit ReceivedFunds(msg.sender, msg.value); 150 | } 151 | 152 | /*////////////////////////////////////////////////////////////// 153 | Internal Functions 154 | //////////////////////////////////////////////////////////////*/ 155 | /** 156 | * @dev Checks the balance of the target address and refills it back up to the target balance if needed. 157 | * @param _systemAddress The system address being funded. 158 | * @param _targetBalance The target balance for the system address being funded. 159 | */ 160 | function refillBalanceIfNeeded(address _systemAddress, uint256 _targetBalance) internal { 161 | uint256 systemAddressBalance = _systemAddress.balance; 162 | if (systemAddressBalance >= _targetBalance) { 163 | emit ProcessedFunds(_systemAddress, false, 0, 0); 164 | return; 165 | } 166 | 167 | uint256 valueNeeded = _targetBalance - systemAddressBalance; 168 | uint256 balanceTrackerBalance = address(this).balance; 169 | uint256 valueToSend = valueNeeded > balanceTrackerBalance ? balanceTrackerBalance : valueNeeded; 170 | 171 | bool success = SafeCall.send(_systemAddress, gasleft(), valueToSend); 172 | emit ProcessedFunds(_systemAddress, success, valueNeeded, valueToSend); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/revenue-share/FeeDisburser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 5 | 6 | import {L2StandardBridge} from "@eth-optimism-bedrock/src/L2/L2StandardBridge.sol"; 7 | import {FeeVault} from "@eth-optimism-bedrock/src/universal/FeeVault.sol"; 8 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 9 | import {SafeCall} from "@eth-optimism-bedrock/src/libraries/SafeCall.sol"; 10 | 11 | /** 12 | * @title FeeDisburser 13 | * @dev Withdraws funds from system FeeVault contracts, shares revenue with Optimism, 14 | * and bridges the rest of funds to L1. 15 | */ 16 | contract FeeDisburser { 17 | /*////////////////////////////////////////////////////////////// 18 | Constants 19 | //////////////////////////////////////////////////////////////*/ 20 | /** 21 | * @dev The basis point scale which revenue share splits are denominated in. 22 | */ 23 | uint32 public constant BASIS_POINT_SCALE = 10_000; 24 | /** 25 | * @dev The minimum gas limit for the FeeDisburser withdrawal transaction to L1. 26 | */ 27 | uint32 public constant WITHDRAWAL_MIN_GAS = 35_000; 28 | /** 29 | * @dev The net revenue percentage denominated in basis points that is used in 30 | * Optimism revenue share calculation. 31 | */ 32 | uint256 public constant OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS = 1_500; 33 | /** 34 | * @dev The gross revenue percentage denominated in basis points that is used in 35 | * Optimism revenue share calculation. 36 | */ 37 | uint256 public constant OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS = 250; 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | Immutables 41 | //////////////////////////////////////////////////////////////*/ 42 | /** 43 | * @dev The address of the Optimism wallet that will receive Optimism's revenue share. 44 | */ 45 | address payable public immutable OPTIMISM_WALLET; 46 | /** 47 | * @dev The address of the L1 wallet that will receive the OP chain runner's share of fees. 48 | */ 49 | address public immutable L1_WALLET; 50 | /** 51 | * @dev The minimum amount of time in seconds that must pass between fee disbursals. 52 | */ 53 | uint256 public immutable FEE_DISBURSEMENT_INTERVAL; 54 | 55 | /*////////////////////////////////////////////////////////////// 56 | Variables 57 | //////////////////////////////////////////////////////////////*/ 58 | /** 59 | * @dev The timestamp of the last disbursal. 60 | */ 61 | uint256 public lastDisbursementTime; 62 | /** 63 | * @dev Tracks aggregate net fee revenue which is the sum of sequencer and base fees. 64 | * @dev Explicitly tracking Net Revenue is required to separate L1FeeVault initiated 65 | * withdrawals from Net Revenue calculations. 66 | */ 67 | uint256 public netFeeRevenue; 68 | 69 | /*////////////////////////////////////////////////////////////// 70 | Events 71 | //////////////////////////////////////////////////////////////*/ 72 | /** 73 | * @dev Emitted when fees are disbursed. 74 | * @param _disbursementTime The time of the disbursement. 75 | * @param _paidToOptimism The amount of fees disbursed to Optimism. 76 | * @param _totalFeesDisbursed The total amount of fees disbursed. 77 | */ 78 | event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed); 79 | /** 80 | * @dev Emitted when fees are received from FeeVaults. 81 | * @param _sender The FeeVault that sent the fees. 82 | * @param _amount The amount of fees received. 83 | */ 84 | event FeesReceived(address indexed _sender, uint256 _amount); 85 | /** 86 | * @dev Emitted when no fees are collected from FeeVaults at time of disbursement. 87 | */ 88 | event NoFeesCollected(); 89 | 90 | /*////////////////////////////////////////////////////////////// 91 | Constructor 92 | //////////////////////////////////////////////////////////////*/ 93 | /** 94 | * @dev Constructor for the FeeDisburser contract which validates and sets immutable variables. 95 | * @param _optimismWallet The address which receives Optimism's revenue share. 96 | * @param _l1Wallet The L1 address which receives the remainder of the revenue. 97 | * @param _feeDisbursementInterval The minimum amount of time in seconds that must pass between fee disbursals. 98 | */ 99 | constructor(address payable _optimismWallet, address _l1Wallet, uint256 _feeDisbursementInterval) { 100 | require(_optimismWallet != address(0), "FeeDisburser: OptimismWallet cannot be address(0)"); 101 | require(_l1Wallet != address(0), "FeeDisburser: L1Wallet cannot be address(0)"); 102 | require( 103 | _feeDisbursementInterval >= 24 hours, "FeeDisburser: FeeDisbursementInterval cannot be less than 24 hours" 104 | ); 105 | 106 | OPTIMISM_WALLET = _optimismWallet; 107 | L1_WALLET = _l1Wallet; 108 | FEE_DISBURSEMENT_INTERVAL = _feeDisbursementInterval; 109 | } 110 | 111 | /*////////////////////////////////////////////////////////////// 112 | External Functions 113 | //////////////////////////////////////////////////////////////*/ 114 | /** 115 | * @dev Withdraws funds from FeeVaults, sends Optimism their revenue share, and withdraws remaining funds to L1. 116 | * @dev Implements revenue share business logic as follows: 117 | * Net Revenue = sequencer FeeVault fee revenue + base FeeVault fee revenue 118 | * Gross Revenue = Net Revenue + l1 FeeVault fee revenue 119 | * Optimism Revenue Share = Maximum of 15% of Net Revenue and 2.5% of Gross Revenue 120 | * L1 Wallet Revenue Share = Gross Revenue - Optimism Revenue Share 121 | */ 122 | function disburseFees() external virtual { 123 | require( 124 | block.timestamp >= lastDisbursementTime + FEE_DISBURSEMENT_INTERVAL, 125 | "FeeDisburser: Disbursement interval not reached" 126 | ); 127 | 128 | // Sequencer and base FeeVaults will withdraw fees to the FeeDisburser contract mutating netFeeRevenue 129 | feeVaultWithdrawal(payable(Predeploys.SEQUENCER_FEE_WALLET)); 130 | feeVaultWithdrawal(payable(Predeploys.BASE_FEE_VAULT)); 131 | 132 | feeVaultWithdrawal(payable(Predeploys.L1_FEE_VAULT)); 133 | 134 | // Gross revenue is the sum of all fees 135 | uint256 feeBalance = address(this).balance; 136 | 137 | // Stop execution if no fees were collected 138 | if (feeBalance == 0) { 139 | emit NoFeesCollected(); 140 | return; 141 | } 142 | 143 | lastDisbursementTime = block.timestamp; 144 | 145 | // Net revenue is the sum of sequencer fees and base fees 146 | uint256 optimismNetRevenueShare = netFeeRevenue * OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS / BASIS_POINT_SCALE; 147 | netFeeRevenue = 0; 148 | 149 | uint256 optimismGrossRevenueShare = feeBalance * OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS / BASIS_POINT_SCALE; 150 | 151 | // Optimism's revenue share is the maximum of net and gross revenue 152 | uint256 optimismRevenueShare = Math.max(optimismNetRevenueShare, optimismGrossRevenueShare); 153 | 154 | // Send Optimism their revenue share on L2 155 | require( 156 | SafeCall.send(OPTIMISM_WALLET, gasleft(), optimismRevenueShare), 157 | "FeeDisburser: Failed to send funds to Optimism" 158 | ); 159 | 160 | // Send remaining funds to L1 wallet on L1 161 | L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{value: address(this).balance}( 162 | L1_WALLET, WITHDRAWAL_MIN_GAS, bytes("") 163 | ); 164 | emit FeesDisbursed(lastDisbursementTime, optimismRevenueShare, feeBalance); 165 | } 166 | 167 | /** 168 | * @dev Receives ETH fees withdrawn from L2 FeeVaults. 169 | * @dev Will revert if ETH is not sent from L2 FeeVaults. 170 | */ 171 | receive() external payable virtual { 172 | if (msg.sender == Predeploys.SEQUENCER_FEE_WALLET || msg.sender == Predeploys.BASE_FEE_VAULT) { 173 | // Adds value received to net fee revenue if the sender is the sequencer or base FeeVault 174 | netFeeRevenue += msg.value; 175 | } else if (msg.sender != Predeploys.L1_FEE_VAULT) { 176 | revert("FeeDisburser: Only FeeVaults can send ETH to FeeDisburser"); 177 | } 178 | emit FeesReceived(msg.sender, msg.value); 179 | } 180 | 181 | /*////////////////////////////////////////////////////////////// 182 | Internal Functions 183 | //////////////////////////////////////////////////////////////*/ 184 | /** 185 | * @dev Withdraws fees from a FeeVault. 186 | * @param _feeVault The address of the FeeVault to withdraw from. 187 | * @dev Withdrawal will only occur if the given FeeVault's balance is greater than or equal to 188 | * the minimum withdrawal amount. 189 | */ 190 | function feeVaultWithdrawal(address payable _feeVault) internal { 191 | require( 192 | FeeVault(_feeVault).WITHDRAWAL_NETWORK() == FeeVault.WithdrawalNetwork.L2, 193 | "FeeDisburser: FeeVault must withdraw to L2" 194 | ); 195 | require( 196 | FeeVault(_feeVault).RECIPIENT() == address(this), 197 | "FeeDisburser: FeeVault must withdraw to FeeDisburser contract" 198 | ); 199 | if (_feeVault.balance >= FeeVault(_feeVault).MIN_WITHDRAWAL_AMOUNT()) { 200 | FeeVault(_feeVault).withdraw(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/smart-escrow/SmartEscrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/access/AccessControlDefaultAdminRules.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | /// @title SmartEscrow contract 8 | /// @notice Contract to handle payment of OP tokens over a period of vesting with 9 | /// the ability to terminate the contract. 10 | /// @notice This contract is inspired by OpenZeppelin's VestingWallet contract, but had 11 | /// sufficiently different requirements to where inheriting did not make sense. 12 | contract SmartEscrow is AccessControlDefaultAdminRules { 13 | /// @notice OP token contract. 14 | IERC20 public constant OP_TOKEN = IERC20(0x4200000000000000000000000000000000000042); 15 | 16 | /// @notice Role which can update benefactor address. 17 | bytes32 public constant BENEFACTOR_OWNER_ROLE = keccak256("smartescrow.roles.benefactorowner"); 18 | 19 | /// @notice Role which can update beneficiary address. 20 | bytes32 public constant BENEFICIARY_OWNER_ROLE = keccak256("smartescrow.roles.beneficiaryowner"); 21 | 22 | /// @notice Role which can update call terminate. 23 | bytes32 public constant TERMINATOR_ROLE = keccak256("smartescrow.roles.terminator"); 24 | 25 | /// @notice Timestamp of the start of vesting period. 26 | uint256 public immutable start; 27 | 28 | /// @notice Timestamp of the cliff. 29 | uint256 public immutable cliffStart; 30 | 31 | /// @notice Timestamp of the end of the vesting period. 32 | uint256 public immutable end; 33 | 34 | /// @notice Period of time between each vesting event in seconds. 35 | uint256 public immutable vestingPeriod; 36 | 37 | /// @notice Number of OP tokens which vest at start time. 38 | uint256 public immutable initialTokens; 39 | 40 | /// @notice Number of OP tokens which vest upon each vesting event. 41 | uint256 public immutable vestingEventTokens; 42 | 43 | /// @notice Address which receives funds back in case of contract termination. 44 | address public benefactor; 45 | 46 | /// @notice Address which receives tokens that have vested. 47 | address public beneficiary; 48 | 49 | /// @notice Number of OP tokens which have been released to the beneficiary. 50 | uint256 public released; 51 | 52 | /// @notice Flag for whether the contract is terminated or active. 53 | bool public contractTerminated; 54 | 55 | /// @notice Event emitted when tokens are withdrawn from the contract. 56 | /// @param benefactor The address which received the withdrawn tokens. 57 | /// @param amount The amount of tokens withdrawn. 58 | event TokensWithdrawn(address indexed benefactor, uint256 amount); 59 | 60 | /// @notice Event emitted when tokens are released to the beneficiary. 61 | /// @param beneficiary The address which received the released tokens. 62 | /// @param amount The amount of tokens released. 63 | event TokensReleased(address indexed beneficiary, uint256 amount); 64 | 65 | /// @notice Event emitted when the benefactor is updated. 66 | /// @param oldBenefactor The address of the old benefactor. 67 | /// @param newBenefactor The address of the new benefactor. 68 | event BenefactorUpdated(address indexed oldBenefactor, address indexed newBenefactor); 69 | 70 | /// @notice Event emitted when the beneficiary is updated. 71 | /// @param oldBeneficiary The address of the old beneficiary. 72 | /// @param newBeneficiary The address of the new beneficiary. 73 | event BeneficiaryUpdated(address indexed oldBeneficiary, address indexed newBeneficiary); 74 | 75 | /// @notice Event emitted when the contract is terminated. 76 | event ContractTerminated(); 77 | 78 | /// @notice Event emitted when the contract was terminated and is no longer. 79 | event ContractResumed(); 80 | 81 | /// @notice The error is thrown when an address is not set. 82 | error AddressIsZeroAddress(); 83 | 84 | /// @notice The error is thrown when the start timestamp is greater than the end timestamp. 85 | /// @param startTimestamp The provided start time of the contract. 86 | /// @param endTimestamp The provided end time of the contract. 87 | error StartTimeAfterEndTime(uint256 startTimestamp, uint256 endTimestamp); 88 | 89 | /// @notice The error is thrown when the cliffStart timestamp is less than the start time. 90 | /// @param cliffStartTimestamp The provided start time of the contract. 91 | /// @param startTime The start time 92 | error CliffStartTimeInvalid(uint256 cliffStartTimestamp, uint256 startTime); 93 | 94 | /// @notice The error is thrown when the cliffStart timestamp is greater than the end timestamp. 95 | /// @param cliffStartTimestamp The provided start time of the contract. 96 | /// @param endTimestamp The provided end time of the contract. 97 | error CliffStartTimeAfterEndTime(uint256 cliffStartTimestamp, uint256 endTimestamp); 98 | 99 | /// @notice The error is thrown when the vesting period is zero. 100 | error VestingPeriodIsZeroSeconds(); 101 | 102 | /// @notice The error is thrown when the number of vesting event tokens is zero. 103 | error VestingEventTokensIsZero(); 104 | 105 | /// @notice The error is thrown when vesting period is longer than the contract duration. 106 | /// @param vestingPeriodSeconds The provided vesting period in seconds. 107 | error VestingPeriodExceedsContractDuration(uint256 vestingPeriodSeconds); 108 | 109 | /// @notice The error is thrown when the vesting period does not evenly divide the contract duration. 110 | /// @param vestingPeriodSeconds The provided vesting period in seconds. 111 | /// @param startTimestamp The provided start time of the contract. 112 | /// @param endTimestamp The provided end time of the contract. 113 | error UnevenVestingPeriod(uint256 vestingPeriodSeconds, uint256 startTimestamp, uint256 endTimestamp); 114 | 115 | /// @notice The error is thrown when the contract is terminated, when it should not be. 116 | error ContractIsTerminated(); 117 | 118 | /// @notice The error is thrown when the contract is not terminated, when it should be. 119 | error ContractIsNotTerminated(); 120 | 121 | /// @notice Set initial parameters. 122 | /// @param _benefactor Address which receives tokens back in case of contract termination. 123 | /// @param _beneficiary Address which receives tokens that have vested. 124 | /// @param _benefactorOwner Address which represents the benefactor entity. 125 | /// @param _beneficiaryOwner Address which represents the beneficiary entity. 126 | /// @param _escrowOwner Address which represents both the benefactor and the beneficiary entities. 127 | /// @param _start Timestamp of the start of vesting period. 128 | /// @param _cliffStart Timestamp when tokens start to vest (must be >= _start). 129 | /// @param _end Timestamp of the end of the vesting period. 130 | /// @param _vestingPeriodSeconds Period of time between each vesting event in seconds. 131 | /// @param _initialTokens Number of OP tokens which vest at start time. 132 | /// @param _vestingEventTokens Number of OP tokens which vest upon each vesting event. 133 | constructor( 134 | address _benefactor, 135 | address _beneficiary, 136 | address _benefactorOwner, 137 | address _beneficiaryOwner, 138 | address _escrowOwner, 139 | uint256 _start, 140 | uint256 _cliffStart, 141 | uint256 _end, 142 | uint256 _vestingPeriodSeconds, 143 | uint256 _initialTokens, 144 | uint256 _vestingEventTokens 145 | ) AccessControlDefaultAdminRules(5 days, _escrowOwner) { 146 | if ( 147 | _benefactor == address(0) || _beneficiary == address(0) || _beneficiaryOwner == address(0) 148 | || _benefactorOwner == address(0) 149 | ) { 150 | revert AddressIsZeroAddress(); 151 | } 152 | if (_start >= _end) revert StartTimeAfterEndTime(_start, _end); 153 | if (_cliffStart < _start) revert CliffStartTimeInvalid(_cliffStart, _start); 154 | if (_cliffStart >= _end) revert CliffStartTimeAfterEndTime(_cliffStart, _end); 155 | if (_vestingPeriodSeconds == 0) revert VestingPeriodIsZeroSeconds(); 156 | if (_vestingEventTokens == 0) revert VestingEventTokensIsZero(); 157 | if ((_end - _start) < _vestingPeriodSeconds) { 158 | revert VestingPeriodExceedsContractDuration(_vestingPeriodSeconds); 159 | } 160 | if ((_end - _start) % _vestingPeriodSeconds != 0) { 161 | revert UnevenVestingPeriod(_vestingPeriodSeconds, _start, _end); 162 | } 163 | 164 | benefactor = _benefactor; 165 | beneficiary = _beneficiary; 166 | start = _start; 167 | cliffStart = _cliffStart; 168 | end = _end; 169 | vestingPeriod = _vestingPeriodSeconds; 170 | initialTokens = _initialTokens; 171 | vestingEventTokens = _vestingEventTokens; 172 | 173 | _grantRole(BENEFACTOR_OWNER_ROLE, _benefactorOwner); 174 | _grantRole(TERMINATOR_ROLE, _benefactorOwner); 175 | _grantRole(BENEFICIARY_OWNER_ROLE, _beneficiaryOwner); 176 | _grantRole(TERMINATOR_ROLE, _beneficiaryOwner); 177 | } 178 | 179 | /// @notice Terminates the contract if called by address with TERMINATOR_ROLE. 180 | /// @notice Releases any vested token to the beneficiary before terminating. 181 | /// @notice Emits a {ContractTerminated} event. 182 | function terminate() external onlyRole(TERMINATOR_ROLE) { 183 | release(); 184 | contractTerminated = true; 185 | emit ContractTerminated(); 186 | } 187 | 188 | /// @notice Resumes the contract on the original vesting schedule. 189 | /// @notice Must be called by address with DEFAULT_ADMIN_ROLE role. 190 | /// @notice Emits a {ContractResumed} event. 191 | function resume() external onlyRole(DEFAULT_ADMIN_ROLE) { 192 | if (!contractTerminated) revert ContractIsNotTerminated(); 193 | contractTerminated = false; 194 | emit ContractResumed(); 195 | } 196 | 197 | /// @notice Allow benefactor owner to update benefactor address. 198 | /// @dev This method does not adjust the BENEFACTOR_OWNER_ROLE. Ensure to pair calling this 199 | /// with a role change by DEFAULT_ADMIN if this is the desired outcome. 200 | /// @param _newBenefactor New benefactor address. 201 | /// @notice Emits a {BenefactorUpdated} event. 202 | function updateBenefactor(address _newBenefactor) external onlyRole(BENEFACTOR_OWNER_ROLE) { 203 | if (_newBenefactor == address(0)) revert AddressIsZeroAddress(); 204 | address oldBenefactor = benefactor; 205 | if (oldBenefactor != _newBenefactor) { 206 | benefactor = _newBenefactor; 207 | emit BenefactorUpdated(oldBenefactor, _newBenefactor); 208 | } 209 | } 210 | 211 | /// @notice Allow beneficiary owner to update beneficiary address. 212 | /// @dev This method does not adjust the BENEFICIARY_OWNER_ROLE. Ensure to pair calling this 213 | /// with a role change by DEFAULT_ADMIN if this is the desired outcome. 214 | /// @param _newBeneficiary New beneficiary address. 215 | /// @notice Emits a {BeneficiaryUpdated} event. 216 | function updateBeneficiary(address _newBeneficiary) external onlyRole(BENEFICIARY_OWNER_ROLE) { 217 | if (_newBeneficiary == address(0)) revert AddressIsZeroAddress(); 218 | address oldBeneficiary = beneficiary; 219 | if (oldBeneficiary != _newBeneficiary) { 220 | beneficiary = _newBeneficiary; 221 | emit BeneficiaryUpdated(oldBeneficiary, _newBeneficiary); 222 | } 223 | } 224 | 225 | /// @notice Allow withdrawal of remaining tokens to benefactor address if contract is terminated. 226 | /// @notice Emits a {Transfer} event and a {TokensWithdrawn} event. 227 | function withdrawUnvestedTokens() external onlyRole(DEFAULT_ADMIN_ROLE) { 228 | if (!contractTerminated) revert ContractIsNotTerminated(); 229 | uint256 amount = OP_TOKEN.balanceOf(address(this)); 230 | if (amount > 0) { 231 | OP_TOKEN.transfer(benefactor, amount); 232 | emit TokensWithdrawn(benefactor, amount); 233 | } 234 | } 235 | 236 | /// @notice Release OP tokens that have already vested. 237 | /// @notice Emits a {Transfer} event and a {TokensReleased} event. 238 | function release() public { 239 | if (contractTerminated) revert ContractIsTerminated(); 240 | uint256 amount = releasable(); 241 | if (amount > 0) { 242 | released += amount; 243 | OP_TOKEN.transfer(beneficiary, amount); 244 | emit TokensReleased(beneficiary, amount); 245 | } 246 | } 247 | 248 | /// @notice Getter for the amount of releasable OP. 249 | function releasable() public view returns (uint256) { 250 | return vestedAmount(block.timestamp) - released; 251 | } 252 | 253 | /// @notice Calculates the amount of OP that has already vested. 254 | /// @param _timestamp The timestamp to at which to get the vested amount 255 | function vestedAmount(uint256 _timestamp) public view returns (uint256) { 256 | return _vestingSchedule(_timestamp); 257 | } 258 | 259 | /// @notice Returns the amount vested as a function of time. 260 | /// @param _timestamp The timestamp to at which to get the vested amount 261 | function _vestingSchedule(uint256 _timestamp) internal view returns (uint256) { 262 | if (_timestamp < cliffStart) { 263 | return 0; 264 | } else if (_timestamp > end) { 265 | return OP_TOKEN.balanceOf(address(this)) + released; 266 | } else { 267 | return initialTokens + ((_timestamp - start) / vestingPeriod) * vestingEventTokens; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /test/Challenger1of2.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.15; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | import {Test, StdUtils} from "forge-std/Test.sol"; 6 | 7 | import {L2OutputOracle} from "@eth-optimism-bedrock/src/L1/L2OutputOracle.sol"; 8 | import {ProxyAdmin} from "@eth-optimism-bedrock/src/universal/ProxyAdmin.sol"; 9 | import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol"; 10 | 11 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 12 | import {Challenger1of2} from "src/Challenger1of2.sol"; 13 | 14 | contract Challenger1of2Test is Test { 15 | address deployer = address(1000); 16 | address coinbaseWallet = address(1001); 17 | address optimismWallet = address(1002); 18 | address randomWallet = address(1003); 19 | address proposer = address(1004); 20 | 21 | ProxyAdmin proxyAdmin; 22 | Proxy l2OutputOracleProxy; 23 | L2OutputOracle l2OutputOracle; 24 | Challenger1of2 challenger; 25 | 26 | bytes DELETE_OUTPUTS_SIGNATURE = abi.encodeWithSignature("deleteL2Outputs(uint256)", 1); 27 | bytes NONEXISTENT_SIGNATURE = abi.encodeWithSignature("something()"); 28 | bytes ZERO_OUTPUT = new bytes(0); 29 | 30 | uint256 ZERO = 0; 31 | uint256 NONZERO_INTEGER = 100; 32 | 33 | event ChallengerCallExecuted(address indexed _caller, bytes _data, bytes _result); 34 | 35 | event OutputsDeleted( 36 | address indexed _caller, uint256 indexed prevNextOutputIndex, uint256 indexed newNextOutputIndex 37 | ); 38 | 39 | function setUp() public { 40 | vm.prank(deployer); 41 | proxyAdmin = new ProxyAdmin(deployer); 42 | l2OutputOracleProxy = new Proxy(address(proxyAdmin)); 43 | 44 | challenger = new Challenger1of2(optimismWallet, coinbaseWallet, address(l2OutputOracleProxy)); 45 | 46 | // Initialize L2OutputOracle implementation. 47 | l2OutputOracle = new L2OutputOracle(); 48 | 49 | vm.prank(deployer); 50 | // Upgrade and initialize L2OutputOracle. 51 | proxyAdmin.upgradeAndCall( 52 | payable(l2OutputOracleProxy), 53 | address(l2OutputOracle), 54 | abi.encodeCall( 55 | L2OutputOracle.initialize, 56 | ( 57 | NONZERO_INTEGER, // _submissionInterval 58 | NONZERO_INTEGER, // _l2BlockTime 59 | ZERO, // _startingBlockNumber 60 | ZERO, // _startingTimestamp 61 | proposer, // _proposer 62 | address(challenger), // _challenger 63 | NONZERO_INTEGER // _finalizationPeriodSeconds 64 | ) 65 | ) 66 | ); 67 | } 68 | 69 | function test_constructor_cbSigner_zeroAddress_fails() external { 70 | vm.expectRevert("Challenger1of2: otherSigner cannot be zero address"); 71 | new Challenger1of2(optimismWallet, address(0), address(l2OutputOracleProxy)); 72 | } 73 | 74 | function test_constructor_opSigner_zeroAddress_fails() external { 75 | vm.expectRevert("Challenger1of2: opSigner cannot be zero address"); 76 | new Challenger1of2(address(0), coinbaseWallet, address(l2OutputOracleProxy)); 77 | } 78 | 79 | function test_constructor_l2OO_zeroAddress_fails() external { 80 | vm.expectRevert("Challenger1of2: l2OutputOracleProxy must be a contract"); 81 | new Challenger1of2(optimismWallet, coinbaseWallet, address(0)); 82 | } 83 | 84 | function test_constructor_success() external { 85 | Challenger1of2 challenger2 = new Challenger1of2(optimismWallet, coinbaseWallet, address(l2OutputOracleProxy)); 86 | assertEq(challenger2.OP_SIGNER(), optimismWallet); 87 | assertEq(challenger2.OTHER_SIGNER(), coinbaseWallet); 88 | assertEq(challenger2.L2_OUTPUT_ORACLE_PROXY(), address(l2OutputOracleProxy)); 89 | } 90 | 91 | function test_execute_unauthorized_call_fails() external { 92 | vm.prank(randomWallet); 93 | vm.expectRevert("Challenger1of2: must be an approved signer to execute"); 94 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 95 | } 96 | 97 | function test_execute_call_fails() external { 98 | vm.prank(optimismWallet); 99 | vm.expectRevert("Challenger1of2: failed to execute"); 100 | challenger.execute(NONEXISTENT_SIGNATURE); 101 | } 102 | 103 | function test_unauthorized_challenger_fails() external { 104 | // Try to make a call from a second challenger contract (not the official one) 105 | Challenger1of2 otherChallenger = 106 | new Challenger1of2(optimismWallet, coinbaseWallet, address(l2OutputOracleProxy)); 107 | vm.prank(optimismWallet); 108 | vm.expectRevert("L2OutputOracle: only the challenger address can delete outputs"); 109 | otherChallenger.execute(DELETE_OUTPUTS_SIGNATURE); 110 | } 111 | 112 | function test_execute_opSigner_success() external { 113 | _proposeOutput(); 114 | _proposeOutput(); 115 | 116 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 117 | // Check that the outputs were proposed. 118 | assertFalse(oracle.latestOutputIndex() == ZERO); 119 | 120 | // We expect the OutputsDeleted event to be emitted 121 | vm.expectEmit(true, true, true, true, address(challenger)); 122 | 123 | // Emit the event we expect to see 124 | emit ChallengerCallExecuted(optimismWallet, DELETE_OUTPUTS_SIGNATURE, ZERO_OUTPUT); 125 | 126 | // We expect deleteOutputs to be called 127 | vm.expectCall(address(l2OutputOracleProxy), abi.encodeWithSignature("deleteL2Outputs(uint256)", 1)); 128 | 129 | // Make the call 130 | vm.prank(optimismWallet); 131 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 132 | 133 | // Check that the outputs were deleted. 134 | assertEq(oracle.latestOutputIndex(), ZERO); 135 | } 136 | 137 | function test_execute_cbSigner_success() external { 138 | _proposeOutput(); 139 | _proposeOutput(); 140 | 141 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 142 | // Check that the outputs were proposed. 143 | assertFalse(oracle.latestOutputIndex() == ZERO); 144 | 145 | // We expect the OutputsDeleted event to be emitted 146 | vm.expectEmit(true, true, true, true, address(challenger)); 147 | 148 | // Emit the event we expect to see 149 | emit ChallengerCallExecuted(coinbaseWallet, DELETE_OUTPUTS_SIGNATURE, ZERO_OUTPUT); 150 | 151 | // We expect deleteOutputs to be called 152 | vm.expectCall(address(l2OutputOracleProxy), abi.encodeWithSignature("deleteL2Outputs(uint256)", 1)); 153 | 154 | // Make the call 155 | vm.prank(coinbaseWallet); 156 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 157 | 158 | // Check that the outputs were deleted. 159 | assertEq(oracle.latestOutputIndex(), ZERO); 160 | } 161 | 162 | function _proposeOutput() internal { 163 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 164 | vm.warp(oracle.computeL2Timestamp(oracle.nextBlockNumber()) + 1); 165 | 166 | vm.startPrank(proposer); 167 | oracle.proposeL2Output(bytes32("something"), oracle.nextBlockNumber(), blockhash(10), 10); 168 | vm.stopPrank(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /test/CommonTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | /* Testing utilities */ 5 | import {Test} from "forge-std/Test.sol"; 6 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 7 | 8 | contract CommonTest is Test { 9 | address alice = address(128); 10 | address bob = address(256); 11 | address admin = address(512); 12 | address deployer = address(1024); 13 | 14 | address constant ZERO_ADDRESS = address(0); 15 | address constant NON_ZERO_ADDRESS = address(1); 16 | address constant CONTRACT_MOCK = address(2); 17 | uint256 constant NON_ZERO_VALUE = 100; 18 | uint256 constant ZERO_VALUE = 0; 19 | uint64 constant NON_ZERO_GASLIMIT = 50000; 20 | 21 | string EMPTY_STRING = ""; 22 | string NON_EMPTY_STRING = "non-empty"; 23 | bytes NULL_BYTES = bytes(""); 24 | bytes NON_NULL_BYTES = abi.encodePacked(uint256(1)); 25 | 26 | function setUp() public virtual { 27 | // Give alice and bob some ETH 28 | vm.deal(alice, 1 << 16); 29 | vm.deal(bob, 1 << 16); 30 | vm.deal(admin, 1 << 16); 31 | 32 | vm.label(alice, "alice"); 33 | vm.label(bob, "bob"); 34 | vm.label(admin, "admin"); 35 | 36 | // Make sure we have a non-zero base fee 37 | vm.fee(1000000000); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/MockERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | /** 8 | * See {ERC20-constructor}. 9 | */ 10 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 11 | 12 | /** 13 | * @dev Mints `amount` tokens to the caller. 14 | * 15 | * See {ERC20-_mint}. 16 | */ 17 | function mint(uint256 amount) public virtual { 18 | _mint(_msgSender(), amount); 19 | } 20 | 21 | /** 22 | * @dev Destroys `amount` tokens from the caller. 23 | * 24 | * See {ERC20-_burn}. 25 | */ 26 | function burn(uint256 amount) public virtual { 27 | _burn(_msgSender(), amount); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/fee-vault-fixes/e2e/FeeVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {CommonTest} from "test/CommonTest.t.sol"; 5 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 6 | import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol"; 7 | import {L1FeeVault as L1FeeVault_Final, FeeVault as FeeVault_Final} from "@eth-optimism-bedrock/src/L2/L1FeeVault.sol"; 8 | import {FeeVault as FeeVault_Fix} from "src/fee-vault-fixes/FeeVault.sol"; 9 | 10 | contract L1FeeVaultTest is CommonTest { 11 | uint256 constant BASE_MAINNET_BLOCK = 2116000; 12 | 13 | string BASE_MAINNET_URL = vm.envString("BASE_MAINNET_URL"); 14 | address recipient; 15 | FeeVault_Final.WithdrawalNetwork withdrawalNetwork; 16 | uint256 minimumWithdrawalAmount; 17 | FeeVault_Fix l1FeeVaultFix; 18 | L1FeeVault_Final l1FeeVaultFinal; 19 | 20 | function setUp() public virtual override { 21 | super.setUp(); 22 | vm.createSelectFork(BASE_MAINNET_URL, BASE_MAINNET_BLOCK); 23 | 24 | recipient = L1FeeVault_Final(payable(Predeploys.SEQUENCER_FEE_WALLET)).RECIPIENT(); 25 | minimumWithdrawalAmount = L1FeeVault_Final(payable(Predeploys.SEQUENCER_FEE_WALLET)).MIN_WITHDRAWAL_AMOUNT(); 26 | withdrawalNetwork = L1FeeVault_Final(payable(Predeploys.SEQUENCER_FEE_WALLET)).WITHDRAWAL_NETWORK(); 27 | 28 | l1FeeVaultFix = new FeeVault_Fix(); 29 | l1FeeVaultFinal = new L1FeeVault_Final(recipient, minimumWithdrawalAmount, withdrawalNetwork); 30 | } 31 | 32 | function test_upgradeToFixImplementationThenFinalImplementation_succeeds() public { 33 | bytes memory setTotalProcessedCall = abi.encodeCall(FeeVault_Fix.setTotalProcessed, ZERO_VALUE); 34 | 35 | assertNotEq(L1FeeVault_Final(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 36 | vm.prank(Predeploys.PROXY_ADMIN); 37 | Proxy(payable(Predeploys.L1_FEE_VAULT)).upgradeToAndCall(address(l1FeeVaultFix), setTotalProcessedCall); 38 | assertEq(FeeVault_Fix(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 39 | 40 | vm.prank(Predeploys.PROXY_ADMIN); 41 | Proxy(payable(Predeploys.L1_FEE_VAULT)).upgradeTo(address(l1FeeVaultFinal)); 42 | assertEq(L1FeeVault_Final(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/revenue-share/BalanceTracker.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {CommonTest} from "test/CommonTest.t.sol"; 5 | import {ReenterProcessFees} from "test/revenue-share/mocks/ReenterProcessFees.sol"; 6 | 7 | import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol"; 8 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 9 | 10 | import {BalanceTracker} from "src/revenue-share/BalanceTracker.sol"; 11 | 12 | contract BalanceTrackerTest is CommonTest { 13 | event ProcessedFunds( 14 | address indexed _systemAddress, bool indexed _success, uint256 _balanceNeeded, uint256 _balanceSent 15 | ); 16 | event SentProfit(address indexed _profitWallet, bool indexed _success, uint256 _balanceSent); 17 | event ReceivedFunds(address indexed _sender, uint256 _amount); 18 | 19 | uint256 constant MAX_SYSTEM_ADDRESS_COUNT = 20; 20 | uint256 constant INITIAL_BALANCE_TRACKER_BALANCE = 2_000 ether; 21 | 22 | Proxy balanceTrackerProxy; 23 | BalanceTracker balanceTrackerImplementation; 24 | BalanceTracker balanceTracker; 25 | 26 | address payable l1StandardBridge = payable(address(1000)); 27 | address payable profitWallet = payable(address(1001)); 28 | address payable batchSender = payable(address(1002)); 29 | address payable l2OutputProposer = payable(address(1003)); 30 | uint256 batchSenderTargetBalance = 1_000 ether; 31 | uint256 l2OutputProposerTargetBalance = 100 ether; 32 | address payable[] systemAddresses = [batchSender, l2OutputProposer]; 33 | uint256[] targetBalances = [batchSenderTargetBalance, l2OutputProposerTargetBalance]; 34 | address proxyAdminOwner = address(2048); 35 | 36 | function setUp() public override { 37 | super.setUp(); 38 | 39 | balanceTrackerImplementation = new BalanceTracker(profitWallet); 40 | balanceTrackerProxy = new Proxy(proxyAdminOwner); 41 | vm.prank(proxyAdminOwner); 42 | balanceTrackerProxy.upgradeTo(address(balanceTrackerImplementation)); 43 | balanceTracker = BalanceTracker(payable(address(balanceTrackerProxy))); 44 | } 45 | 46 | function test_constructor_fail_profitWallet_zeroAddress() external { 47 | vm.expectRevert("BalanceTracker: PROFIT_WALLET cannot be address(0)"); 48 | new BalanceTracker(payable(ZERO_ADDRESS)); 49 | } 50 | 51 | function test_constructor_success() external { 52 | balanceTracker = new BalanceTracker(profitWallet); 53 | 54 | assertEq(balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(), MAX_SYSTEM_ADDRESS_COUNT); 55 | assertEq(balanceTracker.PROFIT_WALLET(), profitWallet); 56 | } 57 | 58 | function test_initializer_fail_systemAddresses_zeroLength() external { 59 | delete systemAddresses; 60 | vm.expectRevert("BalanceTracker: systemAddresses cannot have a length of zero"); 61 | balanceTracker.initialize(systemAddresses, targetBalances); 62 | } 63 | 64 | function test_initializer_fail_systemAddresses_greaterThanMaxLength() external { 65 | for (; systemAddresses.length <= balanceTracker.MAX_SYSTEM_ADDRESS_COUNT();) { 66 | systemAddresses.push(payable(address(0))); 67 | } 68 | 69 | vm.expectRevert("BalanceTracker: systemAddresses cannot have a length greater than 20"); 70 | balanceTracker.initialize(systemAddresses, targetBalances); 71 | } 72 | 73 | function test_initializer_fail_systemAddresses_lengthNotEqualToTargetBalancesLength() external { 74 | systemAddresses.push(payable(address(0))); 75 | 76 | vm.expectRevert("BalanceTracker: systemAddresses and targetBalances length must be equal"); 77 | balanceTracker.initialize(systemAddresses, targetBalances); 78 | } 79 | 80 | function test_initializer_fail_systemAddresses_containsZeroAddress() external { 81 | systemAddresses[1] = payable(address(0)); 82 | 83 | vm.expectRevert("BalanceTracker: systemAddresses cannot contain address(0)"); 84 | balanceTracker.initialize(systemAddresses, targetBalances); 85 | } 86 | 87 | function test_initializer_fail_targetBalances_containsZero() external { 88 | targetBalances[1] = ZERO_VALUE; 89 | 90 | vm.expectRevert("BalanceTracker: targetBalances cannot contain 0 target"); 91 | balanceTracker.initialize(systemAddresses, targetBalances); 92 | } 93 | 94 | function test_initializer_success() external { 95 | balanceTracker.initialize(systemAddresses, targetBalances); 96 | 97 | assertEq(balanceTracker.systemAddresses(0), systemAddresses[0]); 98 | assertEq(balanceTracker.systemAddresses(1), systemAddresses[1]); 99 | assertEq(balanceTracker.targetBalances(0), targetBalances[0]); 100 | assertEq(balanceTracker.targetBalances(1), targetBalances[1]); 101 | } 102 | 103 | function test_processFees_success_cannotBeReentered() external { 104 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 105 | uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; 106 | address payable reentrancySystemAddress = payable(address(new ReenterProcessFees())); 107 | systemAddresses[0] = reentrancySystemAddress; 108 | balanceTracker.initialize(systemAddresses, targetBalances); 109 | 110 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 111 | emit ProcessedFunds(reentrancySystemAddress, false, batchSenderTargetBalance, batchSenderTargetBalance); 112 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 113 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 114 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 115 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 116 | 117 | balanceTracker.processFees(); 118 | 119 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 120 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 121 | assertEq(batchSender.balance, ZERO_VALUE); 122 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 123 | } 124 | 125 | function test_processFees_fail_whenNotInitialized() external { 126 | vm.expectRevert("BalanceTracker: systemAddresses cannot have a length of zero"); 127 | 128 | balanceTracker.processFees(); 129 | } 130 | 131 | function test_processFees_success_continuesWhenSystemAddressReverts() external { 132 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 133 | uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; 134 | balanceTracker.initialize(systemAddresses, targetBalances); 135 | vm.mockCallRevert(batchSender, bytes(""), abi.encode("revert message")); 136 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 137 | emit ProcessedFunds(batchSender, false, batchSenderTargetBalance, batchSenderTargetBalance); 138 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 139 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 140 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 141 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 142 | 143 | balanceTracker.processFees(); 144 | 145 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 146 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 147 | assertEq(batchSender.balance, ZERO_VALUE); 148 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 149 | } 150 | 151 | function test_processFees_success_fundsSystemAddresses() external { 152 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 153 | uint256 expectedProfitWalletBalance = 154 | INITIAL_BALANCE_TRACKER_BALANCE - batchSenderTargetBalance - l2OutputProposerTargetBalance; 155 | balanceTracker.initialize(systemAddresses, targetBalances); 156 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 157 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, batchSenderTargetBalance); 158 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 159 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 160 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 161 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 162 | 163 | balanceTracker.processFees(); 164 | 165 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 166 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 167 | assertEq(batchSender.balance, batchSenderTargetBalance); 168 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 169 | } 170 | 171 | function test_processFees_success_noFunds() external { 172 | balanceTracker.initialize(systemAddresses, targetBalances); 173 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 174 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, ZERO_VALUE); 175 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 176 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, ZERO_VALUE); 177 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 178 | emit SentProfit(profitWallet, true, ZERO_VALUE); 179 | 180 | balanceTracker.processFees(); 181 | 182 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 183 | assertEq(profitWallet.balance, ZERO_VALUE); 184 | assertEq(batchSender.balance, ZERO_VALUE); 185 | assertEq(l2OutputProposer.balance, ZERO_VALUE); 186 | } 187 | 188 | function test_processFees_success_partialFunds() external { 189 | uint256 partialBalanceTrackerBalance = INITIAL_BALANCE_TRACKER_BALANCE / 3; 190 | vm.deal(address(balanceTracker), partialBalanceTrackerBalance); 191 | balanceTracker.initialize(systemAddresses, targetBalances); 192 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 193 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, partialBalanceTrackerBalance); 194 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 195 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, ZERO_VALUE); 196 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 197 | emit SentProfit(profitWallet, true, ZERO_VALUE); 198 | 199 | balanceTracker.processFees(); 200 | 201 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 202 | assertEq(profitWallet.balance, ZERO_VALUE); 203 | assertEq(batchSender.balance, partialBalanceTrackerBalance); 204 | assertEq(l2OutputProposer.balance, ZERO_VALUE); 205 | } 206 | 207 | function test_processFees_success_skipsAddressesAtTargetBalance() external { 208 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 209 | vm.deal(batchSender, batchSenderTargetBalance); 210 | vm.deal(l2OutputProposer, l2OutputProposerTargetBalance); 211 | balanceTracker.initialize(systemAddresses, targetBalances); 212 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 213 | emit ProcessedFunds(batchSender, false, ZERO_VALUE, ZERO_VALUE); 214 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 215 | emit ProcessedFunds(l2OutputProposer, false, ZERO_VALUE, ZERO_VALUE); 216 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 217 | emit SentProfit(profitWallet, true, INITIAL_BALANCE_TRACKER_BALANCE); 218 | 219 | balanceTracker.processFees(); 220 | 221 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 222 | assertEq(profitWallet.balance, INITIAL_BALANCE_TRACKER_BALANCE); 223 | assertEq(batchSender.balance, batchSenderTargetBalance); 224 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 225 | } 226 | 227 | function test_processFees_success_maximumSystemAddresses() external { 228 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 229 | delete systemAddresses; 230 | delete targetBalances; 231 | for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { 232 | systemAddresses.push(payable(address(uint160(i + 100)))); 233 | targetBalances.push(l2OutputProposerTargetBalance); 234 | } 235 | balanceTracker.initialize(systemAddresses, targetBalances); 236 | 237 | balanceTracker.processFees(); 238 | 239 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 240 | for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { 241 | assertEq(systemAddresses[i].balance, l2OutputProposerTargetBalance); 242 | } 243 | assertEq(profitWallet.balance, ZERO_VALUE); 244 | } 245 | 246 | function test_receive_success() external { 247 | vm.deal(l1StandardBridge, NON_ZERO_VALUE); 248 | 249 | vm.prank(l1StandardBridge); 250 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 251 | emit ReceivedFunds(l1StandardBridge, NON_ZERO_VALUE); 252 | 253 | (bool success,) = payable(address(balanceTracker)).call{value: NON_ZERO_VALUE}(""); 254 | assertTrue(success); 255 | 256 | assertEq(address(balanceTracker).balance, NON_ZERO_VALUE); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /test/revenue-share/FeeDisburser.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {CommonTest} from "test/CommonTest.t.sol"; 5 | import {FeeVaultRevert} from "test/revenue-share/mocks/FeeVaultRevert.sol"; 6 | import {OptimismWalletRevert} from "test/revenue-share/mocks/OptimismWalletRevert.sol"; 7 | 8 | import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 9 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 10 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 11 | 12 | import {L2StandardBridge} from "@eth-optimism-bedrock/src/L2/L2StandardBridge.sol"; 13 | import {SequencerFeeVault, FeeVault} from "@eth-optimism-bedrock/src/L2/SequencerFeeVault.sol"; 14 | import {BaseFeeVault} from "@eth-optimism-bedrock/src/L2/BaseFeeVault.sol"; 15 | import {L1FeeVault} from "@eth-optimism-bedrock/src/L2/L1FeeVault.sol"; 16 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 17 | 18 | import {FeeDisburser} from "src/revenue-share/FeeDisburser.sol"; 19 | 20 | contract FeeDisburserTest is CommonTest { 21 | event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed); 22 | event FeesReceived(address indexed _sender, uint256 _amount); 23 | event NoFeesCollected(); 24 | 25 | uint256 constant BASIS_POINTS_SCALE = 10_000; 26 | uint256 constant WITHDRAWAL_MIN_GAS = 35_000; 27 | 28 | TransparentUpgradeableProxy feeDisburserProxy; 29 | FeeDisburser feeDisburserImplementation; 30 | FeeDisburser feeDisburser; 31 | SequencerFeeVault sequencerFeeVault; 32 | BaseFeeVault baseFeeVault; 33 | L1FeeVault l1FeeVault; 34 | address payable optimismWallet = payable(address(1000)); 35 | address payable l1Wallet = payable(address(1001)); 36 | // 15% denominated in base points 37 | uint256 optimismNetRevenueShareBasisPoints = 1_500; 38 | // 2.5% denominated in base points 39 | uint256 optimismGrossRevenueShareBasisPoints = 250; 40 | // 101% denominated in basis points 41 | uint256 tooLargeBasisPoints = 10_001; 42 | // Denominated in seconds 43 | uint256 feeDisbursementInterval = 24 hours; 44 | uint256 minimumWithdrawalAmount = 10 ether; 45 | address proxyAdminOwner = address(2048); 46 | 47 | bytes MINIMUM_WITHDRAWAL_AMOUNT_SIGNATURE = abi.encodeWithSignature("MIN_WITHDRAWAL_AMOUNT()"); 48 | bytes WITHDRAW_SIGNATURE = abi.encodeWithSignature("withdraw()"); 49 | 50 | function setUp() public override { 51 | super.setUp(); 52 | vm.warp(feeDisbursementInterval); 53 | 54 | feeDisburserImplementation = new FeeDisburser(optimismWallet, l1Wallet, feeDisbursementInterval); 55 | feeDisburserProxy = 56 | new TransparentUpgradeableProxy(address(feeDisburserImplementation), proxyAdminOwner, NULL_BYTES); 57 | feeDisburser = FeeDisburser(payable(address(feeDisburserProxy))); 58 | 59 | sequencerFeeVault = new SequencerFeeVault( 60 | payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2 61 | ); 62 | baseFeeVault = 63 | new BaseFeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 64 | l1FeeVault = 65 | new L1FeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 66 | 67 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 68 | vm.etch(Predeploys.BASE_FEE_VAULT, address(baseFeeVault).code); 69 | vm.etch(Predeploys.L1_FEE_VAULT, address(l1FeeVault).code); 70 | } 71 | 72 | function test_constructor_fail_optimismWallet_ZeroAddress() external { 73 | vm.expectRevert("FeeDisburser: OptimismWallet cannot be address(0)"); 74 | new FeeDisburser(payable(address(0)), l1Wallet, feeDisbursementInterval); 75 | } 76 | 77 | function test_constructor_fail_l1Wallet_ZeroAddress() external { 78 | vm.expectRevert("FeeDisburser: L1Wallet cannot be address(0)"); 79 | new FeeDisburser(optimismWallet, payable(address(0)), feeDisbursementInterval); 80 | } 81 | 82 | function test_constructor_fail_feeDisbursementInterval_lessThan24Hours() external { 83 | vm.expectRevert("FeeDisburser: FeeDisbursementInterval cannot be less than 24 hours"); 84 | new FeeDisburser(optimismWallet, l1Wallet, 24 hours - 1); 85 | } 86 | 87 | function test_constructor_success() external { 88 | feeDisburserImplementation = new FeeDisburser(optimismWallet, l1Wallet, feeDisbursementInterval); 89 | assertEq(feeDisburserImplementation.OPTIMISM_WALLET(), optimismWallet); 90 | assertEq(feeDisburserImplementation.L1_WALLET(), l1Wallet); 91 | } 92 | 93 | function test_disburseFees_fail_feeDisbursementInterval_Zero() external { 94 | // Setup so that the first disburse fees actually does a disbursal and doesn't return early 95 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, minimumWithdrawalAmount * 2); 96 | vm.mockCall( 97 | Predeploys.L2_STANDARD_BRIDGE, 98 | abi.encodeWithSignature("bridgeETHTo(address,uint256,bytes)", l1Wallet, WITHDRAWAL_MIN_GAS, NULL_BYTES), 99 | NULL_BYTES 100 | ); 101 | 102 | feeDisburser.disburseFees(); 103 | vm.expectRevert("FeeDisburser: Disbursement interval not reached"); 104 | feeDisburser.disburseFees(); 105 | } 106 | 107 | function test_disburseFees_fail_feeVaultWithdrawalToL1() external { 108 | sequencerFeeVault = new SequencerFeeVault( 109 | payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L1 110 | ); 111 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 112 | 113 | vm.expectRevert("FeeDisburser: FeeVault must withdraw to L2"); 114 | feeDisburser.disburseFees(); 115 | } 116 | 117 | function test_disburseFees_fail_feeVaultWithdrawalToAnotherAddress() external { 118 | sequencerFeeVault = new SequencerFeeVault(admin, minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 119 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 120 | 121 | vm.expectRevert("FeeDisburser: FeeVault must withdraw to FeeDisburser contract"); 122 | feeDisburser.disburseFees(); 123 | } 124 | 125 | function test_disburseFees_fail_sendToOptimismFails() external { 126 | // Define a new feeDisburser for which the OP Wallet always reverts when receiving funds 127 | OptimismWalletRevert optimismWalletRevert = new OptimismWalletRevert(); 128 | FeeDisburser feeDisburser2 = 129 | new FeeDisburser(payable(address(optimismWalletRevert)), l1Wallet, feeDisbursementInterval); 130 | 131 | // Have the fee vaults point to the new fee disburser contract 132 | sequencerFeeVault = new SequencerFeeVault( 133 | payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2 134 | ); 135 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 136 | baseFeeVault = 137 | new BaseFeeVault(payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 138 | vm.etch(Predeploys.BASE_FEE_VAULT, address(baseFeeVault).code); 139 | l1FeeVault = 140 | new L1FeeVault(payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 141 | vm.etch(Predeploys.L1_FEE_VAULT, address(l1FeeVault).code); 142 | 143 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, minimumWithdrawalAmount); 144 | 145 | vm.expectRevert("FeeDisburser: Failed to send funds to Optimism"); 146 | feeDisburser2.disburseFees(); 147 | } 148 | 149 | function test_disburseFees_fail_minimumWithdrawalReversion() external { 150 | FeeVaultRevert feeVaultRevert = new FeeVaultRevert(address(feeDisburser)); 151 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(feeVaultRevert).code); 152 | 153 | vm.expectRevert("revert message"); 154 | feeDisburser.disburseFees(); 155 | } 156 | 157 | function test_disburseFees_fail_withdrawalReversion() external { 158 | vm.mockCall(Predeploys.SEQUENCER_FEE_WALLET, MINIMUM_WITHDRAWAL_AMOUNT_SIGNATURE, abi.encode(ZERO_VALUE)); 159 | 160 | vm.expectRevert("FeeVault: withdrawal amount must be greater than minimum withdrawal amount"); 161 | feeDisburser.disburseFees(); 162 | } 163 | 164 | function test_disburseFees_success_noFees() external { 165 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 166 | emit NoFeesCollected(); 167 | feeDisburser.disburseFees(); 168 | 169 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, ZERO_VALUE); 170 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, ZERO_VALUE); 171 | } 172 | 173 | function test_disburseFees_success_netRevenueMax() external { 174 | // 15% of minimumWithdrawalAmount * 2 > 2.5 % of minimumWithdrawalAmount * 11 175 | uint256 sequencerFeeVaultBalance = minimumWithdrawalAmount; 176 | uint256 baseFeeVaultBalance = minimumWithdrawalAmount; 177 | uint256 l1FeeVaultBalance = minimumWithdrawalAmount * 9; 178 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 179 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 180 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 181 | 182 | uint256 netFeeVaultBalance = sequencerFeeVaultBalance + baseFeeVaultBalance; 183 | uint256 totalFeeVaultBalance = netFeeVaultBalance + l1FeeVaultBalance; 184 | uint256 expectedOptimismWalletBalance = 185 | netFeeVaultBalance * optimismNetRevenueShareBasisPoints / BASIS_POINTS_SCALE; 186 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 187 | 188 | vm.mockCall( 189 | Predeploys.L2_STANDARD_BRIDGE, 190 | abi.encodeWithSignature("bridgeETHTo(address,uint256,bytes)", l1Wallet, WITHDRAWAL_MIN_GAS, NULL_BYTES), 191 | NULL_BYTES 192 | ); 193 | 194 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 195 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 196 | feeDisburser.disburseFees(); 197 | 198 | assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); 199 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 200 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 201 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 202 | } 203 | 204 | function test_disburseFees_success_grossRevenueMax() external { 205 | // 15% of minimumWithdrawalAmount * 2 > 2.5 % of minimumWithdrawalAmount * 13 206 | uint256 sequencerFeeVaultBalance = minimumWithdrawalAmount; 207 | uint256 baseFeeVaultBalance = minimumWithdrawalAmount; 208 | uint256 l1FeeVaultBalance = minimumWithdrawalAmount * 11; 209 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 210 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 211 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 212 | 213 | uint256 totalFeeVaultBalance = sequencerFeeVaultBalance + baseFeeVaultBalance + l1FeeVaultBalance; 214 | uint256 expectedOptimismWalletBalance = 215 | totalFeeVaultBalance * optimismGrossRevenueShareBasisPoints / BASIS_POINTS_SCALE; 216 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 217 | 218 | vm.mockCall( 219 | Predeploys.L2_STANDARD_BRIDGE, 220 | abi.encodeWithSignature("bridgeETHTo(address,uint256,bytes)", l1Wallet, WITHDRAWAL_MIN_GAS, NULL_BYTES), 221 | NULL_BYTES 222 | ); 223 | 224 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 225 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 226 | feeDisburser.disburseFees(); 227 | 228 | assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); 229 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 230 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 231 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 232 | } 233 | 234 | function test_fuzz_success_disburseFees( 235 | uint256 sequencerFeeVaultBalance, 236 | uint256 baseFeeVaultBalance, 237 | uint256 l1FeeVaultBalance 238 | ) external { 239 | vm.assume(sequencerFeeVaultBalance < 10 ** 36); 240 | vm.assume(baseFeeVaultBalance < 10 ** 36); 241 | vm.assume(l1FeeVaultBalance < 10 ** 36); 242 | 243 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 244 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 245 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 246 | 247 | uint256 netFeeVaultBalance = sequencerFeeVaultBalance >= minimumWithdrawalAmount ? sequencerFeeVaultBalance : 0; 248 | netFeeVaultBalance += baseFeeVaultBalance >= minimumWithdrawalAmount ? baseFeeVaultBalance : 0; 249 | uint256 totalFeeVaultBalance = 250 | netFeeVaultBalance + (l1FeeVaultBalance >= minimumWithdrawalAmount ? l1FeeVaultBalance : 0); 251 | 252 | uint256 optimismNetRevenue = netFeeVaultBalance * optimismNetRevenueShareBasisPoints / BASIS_POINTS_SCALE; 253 | uint256 optimismGrossRevenue = totalFeeVaultBalance * optimismGrossRevenueShareBasisPoints / BASIS_POINTS_SCALE; 254 | uint256 expectedOptimismWalletBalance = Math.max(optimismNetRevenue, optimismGrossRevenue); 255 | 256 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 257 | 258 | vm.mockCall( 259 | Predeploys.L2_STANDARD_BRIDGE, 260 | abi.encodeWithSignature("bridgeETHTo(address,uint256,bytes)", l1Wallet, WITHDRAWAL_MIN_GAS, NULL_BYTES), 261 | NULL_BYTES 262 | ); 263 | 264 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 265 | if (totalFeeVaultBalance == 0) { 266 | emit NoFeesCollected(); 267 | } else { 268 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 269 | } 270 | 271 | feeDisburser.disburseFees(); 272 | 273 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 274 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 275 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 276 | } 277 | 278 | function test_receive_fail_unauthorizedCaller() external { 279 | vm.expectRevert("FeeDisburser: Only FeeVaults can send ETH to FeeDisburser"); 280 | vm.prank(alice); 281 | (bool success,) = payable(address(feeDisburser)).call{value: NON_ZERO_VALUE}(""); 282 | assertTrue(success); 283 | } 284 | 285 | function test_receive_success() external { 286 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, NON_ZERO_VALUE); 287 | 288 | vm.prank(Predeploys.SEQUENCER_FEE_WALLET); 289 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 290 | 291 | assertEq(feeDisburser.netFeeRevenue(), NON_ZERO_VALUE); 292 | assertEq(address(feeDisburser).balance, NON_ZERO_VALUE); 293 | } 294 | 295 | function test_receive_success_fromMultipleFeeVaults() external { 296 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, NON_ZERO_VALUE); 297 | vm.deal(Predeploys.BASE_FEE_VAULT, NON_ZERO_VALUE); 298 | vm.deal(Predeploys.L1_FEE_VAULT, NON_ZERO_VALUE); 299 | uint256 expectedNetFeeRevenue = NON_ZERO_VALUE * 2; 300 | uint256 expectedTotalValue = NON_ZERO_VALUE * 3; 301 | 302 | vm.prank(Predeploys.SEQUENCER_FEE_WALLET); 303 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 304 | 305 | vm.prank(Predeploys.BASE_FEE_VAULT); 306 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 307 | 308 | assertEq(feeDisburser.netFeeRevenue(), expectedNetFeeRevenue); 309 | assertEq(address(feeDisburser).balance, expectedNetFeeRevenue); 310 | 311 | vm.prank(Predeploys.L1_FEE_VAULT); 312 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 313 | 314 | assertEq(feeDisburser.netFeeRevenue(), expectedNetFeeRevenue); 315 | assertEq(address(feeDisburser).balance, expectedTotalValue); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /test/revenue-share/mocks/FeeVaultRevert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {FeeVault} from "@eth-optimism-bedrock/src/universal/FeeVault.sol"; 5 | 6 | contract FeeVaultRevert { 7 | address internal immutable _RECIPIENT; 8 | 9 | constructor(address _recipient) { 10 | _RECIPIENT = _recipient; 11 | } 12 | 13 | function RECIPIENT() external view returns (address) { 14 | return _RECIPIENT; 15 | } 16 | 17 | function WITHDRAWAL_NETWORK() external pure returns (FeeVault.WithdrawalNetwork) { 18 | return FeeVault.WithdrawalNetwork.L2; 19 | } 20 | 21 | function MIN_WITHDRAWAL_AMOUNT() external pure returns (uint256) { 22 | revert("revert message"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/revenue-share/mocks/OptimismWalletRevert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract OptimismWalletRevert { 5 | receive() external payable { 6 | revert(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/revenue-share/mocks/ReenterProcessFees.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {BalanceTracker} from "src/revenue-share/BalanceTracker.sol"; 5 | 6 | contract ReenterProcessFees { 7 | receive() external payable { 8 | BalanceTracker(payable(msg.sender)).processFees(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/smart-escrow/BaseSmartEscrow.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {CommonTest} from "test/CommonTest.t.sol"; 5 | import {MockERC20} from "test/MockERC20.t.sol"; 6 | import "src/smart-escrow/SmartEscrow.sol"; 7 | 8 | contract BaseSmartEscrowTest is CommonTest { 9 | event Transfer(address indexed from, address indexed to, uint256 value); 10 | event BenefactorUpdated(address indexed oldBenefactor, address indexed newBenefactor); 11 | event BeneficiaryUpdated(address indexed oldBeneficiary, address indexed newBeneficiary); 12 | event ContractTerminated(); 13 | event ContractResumed(); 14 | event TokensWithdrawn(address indexed benefactor, uint256 amount); 15 | event TokensReleased(address indexed beneficiary, uint256 amount); 16 | 17 | MockERC20 public constant OP_TOKEN = MockERC20(0x4200000000000000000000000000000000000042); 18 | bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; 19 | bytes32 public constant BENEFACTOR_OWNER_ROLE = keccak256("smartescrow.roles.benefactorowner"); 20 | bytes32 public constant BENEFICIARY_OWNER_ROLE = keccak256("smartescrow.roles.beneficiaryowner"); 21 | bytes32 public constant TERMINATOR_ROLE = keccak256("smartescrow.roles.terminator"); 22 | 23 | SmartEscrow public smartEscrow; 24 | address public benefactor = address(1); 25 | address public benefactorOwner = address(2); 26 | address public beneficiary = address(3); 27 | address public beneficiaryOwner = address(4); 28 | address public escrowOwner = address(5); 29 | uint256 public start = 1720674000; 30 | uint256 public cliffStart = 1724976000; 31 | uint256 public end = 1878462000; 32 | uint256 public vestingPeriod = 7889400; 33 | uint256 public initialTokens = 17895697; 34 | uint256 public vestingEventTokens = 4473924; 35 | uint256 public totalTokensToRelease = 107374177; 36 | 37 | function setUp() public override { 38 | smartEscrow = new SmartEscrow( 39 | benefactor, 40 | beneficiary, 41 | benefactorOwner, 42 | beneficiaryOwner, 43 | escrowOwner, 44 | start, 45 | cliffStart, 46 | end, 47 | vestingPeriod, 48 | initialTokens, 49 | vestingEventTokens 50 | ); 51 | 52 | MockERC20 opToken = new MockERC20("Optimism", "OP"); 53 | vm.etch(0x4200000000000000000000000000000000000042, address(opToken).code); 54 | 55 | vm.prank(address(smartEscrow)); 56 | OP_TOKEN.mint(totalTokensToRelease); 57 | } 58 | 59 | function accessControlErrorMessage(address account, bytes32 role) internal pure returns (bytes memory) { 60 | return bytes( 61 | abi.encodePacked( 62 | "AccessControl: account ", 63 | Strings.toHexString(account), 64 | " is missing role ", 65 | Strings.toHexString(uint256(role), 32) 66 | ) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/smart-escrow/Constructor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract ConstructorSmartEscrow is BaseSmartEscrowTest { 7 | function test_constructor_zeroAddressBenefactor_fails() public { 8 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 9 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 10 | new SmartEscrow( 11 | address(0), 12 | beneficiary, 13 | benefactorOwner, 14 | beneficiaryOwner, 15 | escrowOwner, 16 | start, 17 | cliffStart, 18 | end, 19 | vestingPeriod, 20 | initialTokens, 21 | vestingEventTokens 22 | ); 23 | } 24 | 25 | function test_constructor_zeroAddressBeneficiary_fails() public { 26 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 27 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 28 | new SmartEscrow( 29 | benefactor, 30 | address(0), 31 | benefactorOwner, 32 | beneficiaryOwner, 33 | escrowOwner, 34 | start, 35 | cliffStart, 36 | end, 37 | vestingPeriod, 38 | initialTokens, 39 | vestingEventTokens 40 | ); 41 | } 42 | 43 | function test_constructor_zeroAddressBenefactorOwner_fails() public { 44 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 45 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 46 | new SmartEscrow( 47 | benefactor, 48 | beneficiary, 49 | address(0), 50 | beneficiaryOwner, 51 | escrowOwner, 52 | start, 53 | cliffStart, 54 | end, 55 | vestingPeriod, 56 | initialTokens, 57 | vestingEventTokens 58 | ); 59 | } 60 | 61 | function test_constructor_zeroAddressBeneficiaryOwner_fails() public { 62 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 63 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 64 | new SmartEscrow( 65 | benefactor, 66 | beneficiary, 67 | benefactorOwner, 68 | address(0), 69 | escrowOwner, 70 | start, 71 | cliffStart, 72 | end, 73 | vestingPeriod, 74 | initialTokens, 75 | vestingEventTokens 76 | ); 77 | } 78 | 79 | function test_constructor_zeroAddressEscrowOwner_fails() public { 80 | vm.expectRevert("AccessControl: 0 default admin"); 81 | new SmartEscrow( 82 | benefactor, 83 | beneficiary, 84 | benefactorOwner, 85 | beneficiaryOwner, 86 | address(0), 87 | start, 88 | cliffStart, 89 | end, 90 | vestingPeriod, 91 | initialTokens, 92 | vestingEventTokens 93 | ); 94 | } 95 | 96 | function test_constructor_cliffStartTimeZero_fails() public { 97 | vm.warp(100); 98 | bytes4 pastStartTimeSelector = bytes4(keccak256("CliffStartTimeInvalid(uint256,uint256)")); 99 | vm.expectRevert(abi.encodeWithSelector(pastStartTimeSelector, 0, start)); 100 | new SmartEscrow( 101 | benefactor, 102 | beneficiary, 103 | benefactorOwner, 104 | beneficiaryOwner, 105 | escrowOwner, 106 | start, 107 | 0, 108 | end, 109 | vestingPeriod, 110 | initialTokens, 111 | vestingEventTokens 112 | ); 113 | } 114 | 115 | function test_constructor_startAfterEnd_fails() public { 116 | bytes4 startAfterEndSelector = bytes4(keccak256("StartTimeAfterEndTime(uint256,uint256)")); 117 | vm.expectRevert(abi.encodeWithSelector(startAfterEndSelector, end, end)); 118 | new SmartEscrow( 119 | benefactor, 120 | beneficiary, 121 | benefactorOwner, 122 | beneficiaryOwner, 123 | escrowOwner, 124 | end, 125 | cliffStart, 126 | end, 127 | vestingPeriod, 128 | initialTokens, 129 | vestingEventTokens 130 | ); 131 | } 132 | 133 | function test_constructor_cliffStartAfterEnd_fails() public { 134 | bytes4 startAfterEndSelector = bytes4(keccak256("CliffStartTimeAfterEndTime(uint256,uint256)")); 135 | vm.expectRevert(abi.encodeWithSelector(startAfterEndSelector, end, end)); 136 | new SmartEscrow( 137 | benefactor, 138 | beneficiary, 139 | benefactorOwner, 140 | beneficiaryOwner, 141 | escrowOwner, 142 | start, 143 | end, 144 | end, 145 | vestingPeriod, 146 | initialTokens, 147 | vestingEventTokens 148 | ); 149 | } 150 | 151 | function test_constructor_vestingPeriodZero_fails() public { 152 | bytes4 vestingPeriodZeroSelector = bytes4(keccak256("VestingPeriodIsZeroSeconds()")); 153 | vm.expectRevert(abi.encodeWithSelector(vestingPeriodZeroSelector)); 154 | new SmartEscrow( 155 | benefactor, 156 | beneficiary, 157 | benefactorOwner, 158 | beneficiaryOwner, 159 | escrowOwner, 160 | start, 161 | cliffStart, 162 | end, 163 | 0, 164 | initialTokens, 165 | vestingEventTokens 166 | ); 167 | } 168 | 169 | function test_constructor_vestingEventTokensZero_fails() public { 170 | bytes4 vestingEventTokensZeroSelector = bytes4(keccak256("VestingEventTokensIsZero()")); 171 | vm.expectRevert(abi.encodeWithSelector(vestingEventTokensZeroSelector)); 172 | new SmartEscrow( 173 | benefactor, 174 | beneficiary, 175 | benefactorOwner, 176 | beneficiaryOwner, 177 | escrowOwner, 178 | start, 179 | cliffStart, 180 | end, 181 | vestingPeriod, 182 | initialTokens, 183 | 0 184 | ); 185 | } 186 | 187 | function test_constructor_vestingPeriodExceedsContractDuration_fails() public { 188 | bytes4 vestingPeriodExceedsContractDurationSelector = 189 | bytes4(keccak256("VestingPeriodExceedsContractDuration(uint256)")); 190 | vm.expectRevert(abi.encodeWithSelector(vestingPeriodExceedsContractDurationSelector, end)); 191 | new SmartEscrow( 192 | benefactor, 193 | beneficiary, 194 | benefactorOwner, 195 | beneficiaryOwner, 196 | escrowOwner, 197 | start, 198 | cliffStart, 199 | end, 200 | end, 201 | initialTokens, 202 | vestingEventTokens 203 | ); 204 | } 205 | 206 | function test_constructor_unevenVestingPeriod_fails() public { 207 | bytes4 unevenVestingPeriodSelector = bytes4(keccak256("UnevenVestingPeriod(uint256,uint256,uint256)")); 208 | uint256 unevenVestingPeriod = 7; 209 | vm.expectRevert(abi.encodeWithSelector(unevenVestingPeriodSelector, unevenVestingPeriod, start, end)); 210 | new SmartEscrow( 211 | benefactor, 212 | beneficiary, 213 | benefactorOwner, 214 | beneficiaryOwner, 215 | escrowOwner, 216 | start, 217 | cliffStart, 218 | end, 219 | unevenVestingPeriod, 220 | initialTokens, 221 | vestingEventTokens 222 | ); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /test/smart-escrow/Release.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract ReleaseSmartEscrow is BaseSmartEscrowTest { 7 | function test_release_beforeScheduleStart_succeeds() public { 8 | vm.warp(start - 1); // before start 9 | smartEscrow.release(); 10 | assertEq(OP_TOKEN.balanceOf(beneficiary), 0); 11 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 12 | } 13 | 14 | function test_release_afterCliffStart_succeeds() public { 15 | vm.expectEmit(true, true, true, true); 16 | emit Transfer(address(smartEscrow), beneficiary, initialTokens); 17 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 18 | emit TokensReleased(beneficiary, initialTokens); 19 | vm.warp(cliffStart + 1); // after start, before first vesting period 20 | smartEscrow.release(); 21 | assertEq(OP_TOKEN.balanceOf(beneficiary), initialTokens); 22 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease - initialTokens); 23 | } 24 | 25 | function test_release_afterVestingPeriods_succeeds() public { 26 | vm.warp(start + 2 * vestingPeriod); // after 2 vesting periods, includes cliff 27 | uint256 expectedTokens = initialTokens + 2 * vestingEventTokens; 28 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 29 | emit Transfer(address(smartEscrow), beneficiary, expectedTokens); 30 | 31 | smartEscrow.release(); 32 | assertEq(OP_TOKEN.balanceOf(beneficiary), expectedTokens); 33 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease - expectedTokens); 34 | } 35 | 36 | function test_release_afterScheduleEnd_succeeds() public { 37 | vm.warp(end + 1); // after end time 38 | 39 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 40 | emit Transfer(address(smartEscrow), beneficiary, totalTokensToRelease); 41 | 42 | smartEscrow.release(); 43 | assertEq(OP_TOKEN.balanceOf(beneficiary), totalTokensToRelease); 44 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), 0); 45 | } 46 | 47 | function test_release_notEnoughTokens_reverts() public { 48 | vm.prank(address(smartEscrow)); 49 | OP_TOKEN.burn(totalTokensToRelease); 50 | 51 | vm.expectRevert("ERC20: transfer amount exceeds balance"); 52 | 53 | vm.warp(cliffStart + 1); 54 | smartEscrow.release(); 55 | assertEq(OP_TOKEN.balanceOf(beneficiary), 0); 56 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), 0); 57 | } 58 | 59 | function testFuzz_release(uint256 timestamp) public { 60 | vm.warp(timestamp); 61 | uint256 releasable = smartEscrow.releasable(); 62 | smartEscrow.release(); 63 | 64 | // assert releasable tokens were sent to beneficiary 65 | assertEq(OP_TOKEN.balanceOf(beneficiary), releasable); 66 | 67 | // assert amount released is amount we expected to released 68 | assertEq(smartEscrow.released(), releasable); 69 | 70 | // assert total tokens released is correct 71 | assertEq(smartEscrow.released() + OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 72 | 73 | if (timestamp > start && timestamp < cliffStart) { 74 | // assert that the token vesting is happening in increments 75 | assertEq(releasable, 0); 76 | } 77 | 78 | if (timestamp > cliffStart && timestamp < end) { 79 | // assert that the token vesting is happening in increments 80 | assertEq((releasable - initialTokens) % uint256(vestingEventTokens), 0); 81 | } 82 | 83 | // assert all tokens are released after the end period 84 | if (timestamp > end) { 85 | assertEq(smartEscrow.released(), totalTokensToRelease); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/smart-escrow/Resume.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract ResumeSmartEscrow is BaseSmartEscrowTest { 7 | function test_resume_succeeds() public { 8 | // Contract was terminated 9 | vm.prank(benefactorOwner); 10 | smartEscrow.terminate(); 11 | 12 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 13 | emit ContractResumed(); 14 | 15 | // Contract is resumed 16 | vm.prank(escrowOwner); 17 | smartEscrow.resume(); 18 | 19 | vm.warp(end + 1); // All tokens are releasable at this time 20 | smartEscrow.release(); 21 | 22 | // All tokens should have been released 23 | assertEq(OP_TOKEN.balanceOf(beneficiary), totalTokensToRelease); 24 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), 0); 25 | } 26 | 27 | function test_resume_unauthorizedCall_fails() public { 28 | // Contract was terminated 29 | vm.prank(benefactorOwner); 30 | smartEscrow.terminate(); 31 | 32 | // Unauthorized call to resume 33 | vm.expectRevert(accessControlErrorMessage(benefactorOwner, DEFAULT_ADMIN_ROLE)); 34 | vm.prank(benefactorOwner); 35 | smartEscrow.resume(); 36 | 37 | // Attempt to release tokens 38 | bytes4 selector = bytes4(keccak256("ContractIsTerminated()")); 39 | vm.expectRevert(abi.encodeWithSelector(selector)); 40 | smartEscrow.release(); 41 | 42 | // All tokens should remain in the contract 43 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 44 | } 45 | 46 | function test_resume_calledWhenContractNotTerminated_fails() public { 47 | bytes4 selector = bytes4(keccak256("ContractIsNotTerminated()")); 48 | vm.expectRevert(abi.encodeWithSelector(selector)); 49 | vm.prank(escrowOwner); 50 | smartEscrow.resume(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/smart-escrow/Terminate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract TerminateSmartEscrow is BaseSmartEscrowTest { 7 | function test_terminate_byBenefactorOwner_succeeds() public { 8 | vm.warp(start - 1); // before start 9 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 10 | emit ContractTerminated(); 11 | 12 | vm.prank(benefactorOwner); 13 | smartEscrow.terminate(); 14 | 15 | // Additional calls to release should fail 16 | bytes4 selector = bytes4(keccak256("ContractIsTerminated()")); 17 | vm.expectRevert(abi.encodeWithSelector(selector)); 18 | smartEscrow.release(); 19 | 20 | // All tokens should remain in the contract 21 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 22 | } 23 | 24 | function test_terminate_byBeneficiaryOwner_succeeds() public { 25 | vm.warp(start + 2 * vestingPeriod); // after 2 vesting periods 26 | uint256 expectedReleased = initialTokens + 2 * vestingEventTokens; 27 | 28 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 29 | emit Transfer(address(smartEscrow), beneficiary, expectedReleased); 30 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 31 | emit ContractTerminated(); 32 | 33 | // Calling terminate should release vested tokens to beneficiary before pausing 34 | vm.prank(beneficiaryOwner); 35 | smartEscrow.terminate(); 36 | 37 | // Beneficiary should have received vested tokens, rest remain in the contract 38 | assertEq(OP_TOKEN.balanceOf(beneficiary), expectedReleased); 39 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease - expectedReleased); 40 | 41 | // Additional calls to release should fail 42 | bytes4 selector = bytes4(keccak256("ContractIsTerminated()")); 43 | vm.expectRevert(abi.encodeWithSelector(selector)); 44 | smartEscrow.release(); 45 | 46 | // Balances should not have changed 47 | assertEq(OP_TOKEN.balanceOf(beneficiary), expectedReleased); 48 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease - expectedReleased); 49 | } 50 | 51 | function test_terminate_withdrawAfterTermination_succeeds() public { 52 | vm.warp(start + 2 * vestingPeriod); // after 2 vesting periods 53 | uint256 expectedReleased = initialTokens + 2 * vestingEventTokens; 54 | 55 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 56 | emit Transfer(address(smartEscrow), beneficiary, expectedReleased); 57 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 58 | emit ContractTerminated(); 59 | 60 | // Calling terminate should release vested tokens to beneficiary before pausing 61 | vm.prank(benefactorOwner); 62 | smartEscrow.terminate(); 63 | 64 | // Both parties agreed to fully terminate the contract and withdraw unvested tokens 65 | vm.prank(escrowOwner); 66 | smartEscrow.withdrawUnvestedTokens(); 67 | 68 | // Expected that some tokens released to beneficiary, rest to benefactor and none remain in the contract 69 | assertEq(OP_TOKEN.balanceOf(beneficiary), expectedReleased); 70 | assertEq(OP_TOKEN.balanceOf(benefactor), totalTokensToRelease - expectedReleased); 71 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), 0); 72 | } 73 | 74 | function test_terminate_unauthorizedCall_fails() public { 75 | vm.expectRevert(accessControlErrorMessage(alice, TERMINATOR_ROLE)); 76 | vm.prank(alice); 77 | smartEscrow.terminate(); 78 | 79 | // All tokens should remain in the contract 80 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 81 | } 82 | 83 | function test_terminate_calledTwice_fails() public { 84 | vm.prank(benefactorOwner); 85 | smartEscrow.terminate(); 86 | 87 | // Second call to terminate should fail 88 | bytes4 selector = bytes4(keccak256("ContractIsTerminated()")); 89 | vm.expectRevert(abi.encodeWithSelector(selector)); 90 | vm.prank(benefactorOwner); 91 | smartEscrow.terminate(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/smart-escrow/UpdateBenefactor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract UpdateBenefactorSmartEscrow is BaseSmartEscrowTest { 7 | function test_updateBenefactor_succeeds() public { 8 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 9 | emit BenefactorUpdated(benefactor, alice); 10 | vm.prank(benefactorOwner); 11 | smartEscrow.updateBenefactor(alice); 12 | assertEq(smartEscrow.benefactor(), alice); 13 | } 14 | 15 | function test_updateBenefactor_newBenefactorOwner_succeeds() public { 16 | address newBenefactorOwner = address(100); 17 | vm.prank(escrowOwner); 18 | smartEscrow.grantRole(BENEFACTOR_OWNER_ROLE, newBenefactorOwner); 19 | assertEq(smartEscrow.hasRole(BENEFACTOR_OWNER_ROLE, newBenefactorOwner), true); 20 | 21 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 22 | emit BenefactorUpdated(benefactor, alice); 23 | vm.prank(newBenefactorOwner); 24 | smartEscrow.updateBenefactor(alice); 25 | assertEq(smartEscrow.benefactor(), alice); 26 | } 27 | 28 | function test_updateBenefactor_zeroAddress_fails() public { 29 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 30 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 31 | 32 | vm.prank(benefactorOwner); 33 | smartEscrow.updateBenefactor(address(0)); 34 | 35 | // Benefactor remains the same 36 | assertEq(smartEscrow.benefactor(), benefactor); 37 | } 38 | 39 | function test_updateBenefactor_unauthorizedCall_fails() public { 40 | vm.expectRevert(accessControlErrorMessage(escrowOwner, BENEFACTOR_OWNER_ROLE)); 41 | vm.prank(escrowOwner); 42 | smartEscrow.updateBenefactor(alice); 43 | 44 | // Benefactor owner remains the same 45 | assertEq(smartEscrow.benefactor(), benefactor); 46 | } 47 | 48 | function test_updateBenefactor_oldOwner_fails() public { 49 | // Remove role from benefactor owner 50 | vm.prank(escrowOwner); 51 | smartEscrow.revokeRole(BENEFACTOR_OWNER_ROLE, benefactorOwner); 52 | 53 | vm.expectRevert(accessControlErrorMessage(benefactorOwner, BENEFACTOR_OWNER_ROLE)); 54 | vm.prank(benefactorOwner); 55 | smartEscrow.updateBenefactor(alice); 56 | 57 | // Benefactor owner remains the same 58 | assertEq(smartEscrow.benefactor(), benefactor); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/smart-escrow/UpdateBeneficiary.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract UpdateBeneficiarySmartEscrow is BaseSmartEscrowTest { 7 | function test_updateBeneficiary_succeeds() public { 8 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 9 | emit BeneficiaryUpdated(beneficiary, alice); 10 | vm.prank(beneficiaryOwner); 11 | smartEscrow.updateBeneficiary(alice); 12 | assertEq(smartEscrow.beneficiary(), alice); 13 | } 14 | 15 | function test_updateBeneficiary_newBeneficiaryOwner_succeeds() public { 16 | address newBeneficiaryOwner = address(1000); 17 | vm.prank(escrowOwner); 18 | smartEscrow.grantRole(BENEFICIARY_OWNER_ROLE, newBeneficiaryOwner); 19 | assertEq(smartEscrow.hasRole(BENEFICIARY_OWNER_ROLE, newBeneficiaryOwner), true); 20 | 21 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 22 | emit BeneficiaryUpdated(beneficiary, alice); 23 | vm.prank(newBeneficiaryOwner); 24 | smartEscrow.updateBeneficiary(alice); 25 | assertEq(smartEscrow.beneficiary(), alice); 26 | } 27 | 28 | function test_updateBeneficiary_zeroAddress_fails() public { 29 | bytes4 zeroAddressSelector = bytes4(keccak256("AddressIsZeroAddress()")); 30 | vm.expectRevert(abi.encodeWithSelector(zeroAddressSelector)); 31 | 32 | vm.prank(beneficiaryOwner); 33 | smartEscrow.updateBeneficiary(address(0)); 34 | 35 | // Beneficiary remains the same 36 | assertEq(smartEscrow.beneficiary(), beneficiary); 37 | } 38 | 39 | function test_updateBeneficiary_unauthorizedCall_fails() public { 40 | vm.expectRevert(accessControlErrorMessage(escrowOwner, BENEFICIARY_OWNER_ROLE)); 41 | vm.prank(escrowOwner); 42 | smartEscrow.updateBeneficiary(alice); 43 | 44 | // Beneficiary owner remains the same 45 | assertEq(smartEscrow.beneficiary(), beneficiary); 46 | } 47 | 48 | function test_updateBeneficiary_oldOwner_fails() public { 49 | // Remove role from beneficiary owner 50 | vm.prank(escrowOwner); 51 | smartEscrow.revokeRole(BENEFICIARY_OWNER_ROLE, beneficiaryOwner); 52 | 53 | vm.expectRevert(accessControlErrorMessage(beneficiaryOwner, BENEFICIARY_OWNER_ROLE)); 54 | vm.prank(beneficiaryOwner); 55 | smartEscrow.updateBeneficiary(alice); 56 | 57 | // Beneficiary owner remains the same 58 | assertEq(smartEscrow.beneficiary(), beneficiary); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/smart-escrow/WithdrawUnvestedTokens.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./BaseSmartEscrow.t.sol"; 5 | 6 | contract WithdrawUnvestedTokensSmartEscrow is BaseSmartEscrowTest { 7 | function test_withdrawUnvestedTokens_succeeds() public { 8 | // Contract terminated 9 | vm.prank(benefactorOwner); 10 | smartEscrow.terminate(); 11 | 12 | // We expect a Transfer and TokensWithdrawn events to be emitted 13 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 14 | emit Transfer(address(smartEscrow), benefactor, totalTokensToRelease); 15 | vm.expectEmit(true, true, true, true, address(smartEscrow)); 16 | emit TokensWithdrawn(benefactor, totalTokensToRelease); 17 | 18 | // Tokens withdrawn to benefactor 19 | vm.prank(escrowOwner); 20 | smartEscrow.withdrawUnvestedTokens(); 21 | 22 | // Benefactor updated 23 | vm.prank(benefactorOwner); 24 | smartEscrow.updateBenefactor(alice); 25 | 26 | // Additional tokens sent to contract which can be withdrawn 27 | vm.prank(address(smartEscrow)); 28 | OP_TOKEN.mint(totalTokensToRelease); 29 | 30 | // We expect a Transfer event to be emitted 31 | vm.expectEmit(true, true, true, true, address(OP_TOKEN)); 32 | emit Transfer(address(smartEscrow), alice, totalTokensToRelease); 33 | 34 | vm.prank(escrowOwner); 35 | smartEscrow.withdrawUnvestedTokens(); 36 | 37 | // Tokens were released to benefactor on termination and to Alice on the additional withdraw 38 | assertEq(OP_TOKEN.balanceOf(benefactor), totalTokensToRelease); 39 | assertEq(OP_TOKEN.balanceOf(alice), totalTokensToRelease); 40 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), 0); 41 | } 42 | 43 | function test_withdrawUnvestedTokens_unauthorizedCall_fails() public { 44 | vm.expectRevert(accessControlErrorMessage(benefactorOwner, DEFAULT_ADMIN_ROLE)); 45 | vm.prank(benefactorOwner); 46 | smartEscrow.withdrawUnvestedTokens(); 47 | 48 | vm.expectRevert(accessControlErrorMessage(beneficiaryOwner, DEFAULT_ADMIN_ROLE)); 49 | vm.prank(beneficiaryOwner); 50 | smartEscrow.withdrawUnvestedTokens(); 51 | 52 | // No tokens were released 53 | assertEq(OP_TOKEN.balanceOf(benefactor), 0); 54 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 55 | } 56 | 57 | function test_withdrawUnvestedTokens_contractStillActive_fails() public { 58 | bytes4 notTerminatedSelector = bytes4(keccak256("ContractIsNotTerminated()")); 59 | vm.expectRevert(abi.encodeWithSelector(notTerminatedSelector)); 60 | vm.prank(escrowOwner); 61 | smartEscrow.withdrawUnvestedTokens(); 62 | 63 | // No tokens were released 64 | assertEq(OP_TOKEN.balanceOf(address(smartEscrow)), totalTokensToRelease); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/universal/Counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract Counter { 5 | address internal immutable OWNER; 6 | uint256 public count = 0; 7 | 8 | constructor(address owner) { 9 | OWNER = owner; 10 | } 11 | 12 | function increment() external { 13 | require(msg.sender == OWNER, "only owner can increment"); 14 | 15 | count += 1; 16 | } 17 | 18 | function incrementPayable() external payable { 19 | require(msg.value != 0, "value must be greater than 0"); 20 | count += 1; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/universal/DoubleNestedMultisigBuilder.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.sol"; 9 | 10 | import {DoubleNestedMultisigBuilder} from "../../script/universal/DoubleNestedMultisigBuilder.sol"; 11 | import {Simulation} from "../../script/universal/Simulation.sol"; 12 | import {IGnosisSafe} from "../../script/universal/IGnosisSafe.sol"; 13 | import {Counter} from "./Counter.sol"; 14 | 15 | contract DoubleNestedMultisigBuilderTest is Test, DoubleNestedMultisigBuilder { 16 | Vm.Wallet internal wallet1 = vm.createWallet("1"); 17 | Vm.Wallet internal wallet2 = vm.createWallet("2"); 18 | 19 | address internal safe1 = address(1001); 20 | address internal safe2 = address(1002); 21 | address internal safe3 = address(1003); 22 | address internal safe4 = address(1004); 23 | Counter internal counter = new Counter(address(safe4)); 24 | 25 | bytes internal dataToSign1 = 26 | // solhint-disable max-line-length 27 | hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c79f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; 28 | bytes internal dataToSign2 = 29 | hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d171579f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; 30 | 31 | function setUp() public { 32 | bytes memory safeCode = Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid); 33 | vm.etch(safe1, safeCode); 34 | vm.etch(safe2, safeCode); 35 | vm.etch(safe3, safeCode); 36 | vm.etch(safe4, safeCode); 37 | vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); 38 | 39 | address[] memory owners1 = new address[](1); 40 | owners1[0] = wallet1.addr; 41 | IGnosisSafe(safe1).setup(owners1, 1, address(0), "", address(0), address(0), 0, address(0)); 42 | 43 | address[] memory owners2 = new address[](1); 44 | owners2[0] = wallet2.addr; 45 | IGnosisSafe(safe2).setup(owners2, 1, address(0), "", address(0), address(0), 0, address(0)); 46 | 47 | address[] memory owners3 = new address[](2); 48 | owners3[0] = safe1; 49 | owners3[1] = safe2; 50 | IGnosisSafe(safe3).setup(owners3, 2, address(0), "", address(0), address(0), 0, address(0)); 51 | 52 | address[] memory owners4 = new address[](1); 53 | owners4[0] = safe3; 54 | IGnosisSafe(safe4).setup(owners4, 1, address(0), "", address(0), address(0), 0, address(0)); 55 | } 56 | 57 | function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { 58 | // Check that the counter has been incremented 59 | uint256 counterValue = counter.count(); 60 | require(counterValue == 1, "Counter value is not 1"); 61 | } 62 | 63 | function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { 64 | IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); 65 | 66 | calls[0] = IMulticall3.Call3Value({ 67 | target: address(counter), 68 | allowFailure: false, 69 | callData: abi.encodeCall(Counter.increment, ()), 70 | value: 0 71 | }); 72 | 73 | return calls; 74 | } 75 | 76 | function _ownerSafe() internal view override returns (address) { 77 | return safe4; 78 | } 79 | 80 | function test_sign_double_nested_safe1() external { 81 | vm.recordLogs(); 82 | bytes memory txData = abi.encodeCall(DoubleNestedMultisigBuilder.sign, (safe1, safe3)); 83 | vm.prank(wallet1.addr); 84 | (bool success,) = address(this).call(txData); 85 | vm.assertTrue(success); 86 | Vm.Log[] memory logs = vm.getRecordedLogs(); 87 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); 88 | } 89 | 90 | function test_sign_double_nested_safe2() external { 91 | vm.recordLogs(); 92 | bytes memory txData = abi.encodeCall(DoubleNestedMultisigBuilder.sign, (safe2, safe3)); 93 | vm.prank(wallet2.addr); 94 | (bool success,) = address(this).call(txData); 95 | vm.assertTrue(success); 96 | Vm.Log[] memory logs = vm.getRecordedLogs(); 97 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); 98 | } 99 | 100 | function test_approveInit_double_nested_safe1() external { 101 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); 102 | approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r, s, v)); 103 | } 104 | 105 | function test_approveInit_double_nested_safe2() external { 106 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); 107 | approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r, s, v)); 108 | } 109 | 110 | function test_approveInit_double_nested_notOwner() external { 111 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); 112 | bytes memory data = abi.encodeCall(this.approveOnBehalfOfSignerSafe, (safe2, safe3, abi.encodePacked(r, s, v))); 113 | (bool success, bytes memory result) = address(this).call(data); 114 | assertFalse(success); 115 | assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); 116 | } 117 | 118 | function test_runInit_double_nested() external { 119 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); 120 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); 121 | approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); 122 | approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r2, s2, v2)); 123 | approveOnBehalfOfIntermediateSafe(safe3); 124 | } 125 | 126 | function test_runInit_double_nested_notApproved() external { 127 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); 128 | approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); 129 | bytes memory data = abi.encodeCall(this.approveOnBehalfOfIntermediateSafe, (safe3)); 130 | (bool success, bytes memory result) = address(this).call(data); 131 | assertFalse(success); 132 | assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); 133 | } 134 | 135 | function test_run_double_nested() external { 136 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); 137 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); 138 | approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); 139 | approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r2, s2, v2)); 140 | approveOnBehalfOfIntermediateSafe(safe3); 141 | 142 | run(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/universal/MultisigBuilder.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.sol"; 9 | 10 | import {MultisigBuilder} from "../../script/universal/MultisigBuilder.sol"; 11 | import {Simulation} from "../../script/universal/Simulation.sol"; 12 | import {IGnosisSafe} from "../../script/universal/IGnosisSafe.sol"; 13 | import {Counter} from "./Counter.sol"; 14 | 15 | contract MultisigBuilderTest is Test, MultisigBuilder { 16 | Vm.Wallet internal wallet1 = vm.createWallet("1"); 17 | Vm.Wallet internal wallet2 = vm.createWallet("2"); 18 | 19 | address internal safe = address(1001); 20 | Counter internal counter = new Counter(address(safe)); 21 | 22 | function () internal view returns (IMulticall3.Call3Value[] memory) buildCallsInternal; 23 | 24 | bytes internal dataToSignNoValue = 25 | // solhint-disable-next-line max-line-length 26 | hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280cd0722aa57d06d71497c199147817c38ae160e5b355d3fb5ccbe34c3dbadeae6d"; 27 | 28 | bytes internal dataToSignWithValue = 29 | // solhint-disable-next-line max-line-length 30 | hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280cd150dbb03d4bb38e5325a914ff3861da880437fd5856c0f7e39054e64e05aed0"; 31 | 32 | function setUp() public { 33 | vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); 34 | vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); 35 | vm.deal(safe, 10 ether); 36 | 37 | address[] memory owners = new address[](2); 38 | owners[0] = wallet1.addr; 39 | owners[1] = wallet2.addr; 40 | IGnosisSafe(safe).setup(owners, 2, address(0), "", address(0), address(0), 0, address(0)); 41 | } 42 | 43 | function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { 44 | // Check that the counter has been incremented 45 | uint256 counterValue = counter.count(); 46 | require(counterValue == 1, "Counter value is not 1"); 47 | } 48 | 49 | function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { 50 | return buildCallsInternal(); 51 | } 52 | 53 | function _ownerSafe() internal view override returns (address) { 54 | return address(safe); 55 | } 56 | 57 | function test_sign_no_value() external { 58 | buildCallsInternal = _buildCallsNoValue; 59 | 60 | vm.recordLogs(); 61 | bytes memory txData = abi.encodeCall(MultisigBuilder.sign, ()); 62 | vm.prank(wallet1.addr); 63 | (bool success,) = address(this).call(txData); 64 | vm.assertTrue(success); 65 | Vm.Log[] memory logs = vm.getRecordedLogs(); 66 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignNoValue))); 67 | } 68 | 69 | function test_sign_with_value() external { 70 | buildCallsInternal = _buildCallsWithValue; 71 | 72 | vm.recordLogs(); 73 | bytes memory txData = abi.encodeCall(MultisigBuilder.sign, ()); 74 | vm.prank(wallet1.addr); 75 | (bool success,) = address(this).call(txData); 76 | vm.assertTrue(success); 77 | Vm.Log[] memory logs = vm.getRecordedLogs(); 78 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignWithValue))); 79 | } 80 | 81 | function test_run() external { 82 | buildCallsInternal = _buildCallsNoValue; 83 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSignNoValue)); 84 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSignNoValue)); 85 | bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2); 86 | run(signatures); 87 | } 88 | 89 | function _buildCallsNoValue() internal view returns (IMulticall3.Call3Value[] memory) { 90 | IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); 91 | 92 | calls[0] = IMulticall3.Call3Value({ 93 | target: address(counter), 94 | allowFailure: false, 95 | callData: abi.encodeCall(Counter.increment, ()), 96 | value: 0 97 | }); 98 | 99 | return calls; 100 | } 101 | 102 | function _buildCallsWithValue() internal view returns (IMulticall3.Call3Value[] memory) { 103 | IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); 104 | 105 | calls[0] = IMulticall3.Call3Value({ 106 | target: address(counter), 107 | allowFailure: false, 108 | callData: abi.encodeCall(Counter.incrementPayable, ()), 109 | value: 1 ether 110 | }); 111 | 112 | return calls; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/universal/NestedMultisigBuilder.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.sol"; 9 | 10 | import {NestedMultisigBuilder} from "../../script/universal/NestedMultisigBuilder.sol"; 11 | import {Simulation} from "../../script/universal/Simulation.sol"; 12 | import {IGnosisSafe} from "../../script/universal/IGnosisSafe.sol"; 13 | import {Counter} from "./Counter.sol"; 14 | 15 | contract NestedMultisigBuilderTest is Test, NestedMultisigBuilder { 16 | Vm.Wallet internal wallet1 = vm.createWallet("1"); 17 | Vm.Wallet internal wallet2 = vm.createWallet("2"); 18 | 19 | address internal safe1 = address(1001); 20 | address internal safe2 = address(1002); 21 | address internal safe3 = address(1003); 22 | Counter internal counter = new Counter(address(safe3)); 23 | 24 | bytes internal dataToSign1 = 25 | // solhint-disable max-line-length 26 | hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c5f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; 27 | bytes internal dataToSign2 = 28 | hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d17155f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; 29 | 30 | function setUp() public { 31 | bytes memory safeCode = Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid); 32 | vm.etch(safe1, safeCode); 33 | vm.etch(safe2, safeCode); 34 | vm.etch(safe3, safeCode); 35 | vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); 36 | 37 | address[] memory owners1 = new address[](1); 38 | owners1[0] = wallet1.addr; 39 | IGnosisSafe(safe1).setup(owners1, 1, address(0), "", address(0), address(0), 0, address(0)); 40 | 41 | address[] memory owners2 = new address[](1); 42 | owners2[0] = wallet2.addr; 43 | IGnosisSafe(safe2).setup(owners2, 1, address(0), "", address(0), address(0), 0, address(0)); 44 | 45 | address[] memory owners3 = new address[](2); 46 | owners3[0] = safe1; 47 | owners3[1] = safe2; 48 | IGnosisSafe(safe3).setup(owners3, 2, address(0), "", address(0), address(0), 0, address(0)); 49 | } 50 | 51 | function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { 52 | // Check that the counter has been incremented 53 | uint256 counterValue = counter.count(); 54 | require(counterValue == 1, "Counter value is not 1"); 55 | } 56 | 57 | function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { 58 | IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); 59 | 60 | calls[0] = IMulticall3.Call3Value({ 61 | target: address(counter), 62 | allowFailure: false, 63 | callData: abi.encodeCall(Counter.increment, ()), 64 | value: 0 65 | }); 66 | 67 | return calls; 68 | } 69 | 70 | function _ownerSafe() internal view override returns (address) { 71 | return address(safe3); 72 | } 73 | 74 | function test_sign_safe1() external { 75 | vm.recordLogs(); 76 | bytes memory txData = abi.encodeCall(NestedMultisigBuilder.sign, (safe1)); 77 | vm.prank(wallet1.addr); 78 | (bool success,) = address(this).call(txData); 79 | vm.assertTrue(success); 80 | Vm.Log[] memory logs = vm.getRecordedLogs(); 81 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); 82 | } 83 | 84 | function test_sign_safe2() external { 85 | vm.recordLogs(); 86 | bytes memory txData = abi.encodeCall(NestedMultisigBuilder.sign, (safe2)); 87 | vm.prank(wallet2.addr); 88 | (bool success,) = address(this).call(txData); 89 | vm.assertTrue(success); 90 | Vm.Log[] memory logs = vm.getRecordedLogs(); 91 | assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); 92 | } 93 | 94 | function test_approve_safe1() external { 95 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); 96 | approve(safe1, abi.encodePacked(r, s, v)); 97 | } 98 | 99 | function test_approve_safe2() external { 100 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); 101 | approve(safe2, abi.encodePacked(r, s, v)); 102 | } 103 | 104 | function test_approve_notOwner() external { 105 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); 106 | bytes memory data = 107 | abi.encodeWithSelector(bytes4(keccak256("approve(address,bytes)")), safe2, abi.encodePacked(r, s, v)); 108 | (bool success, bytes memory result) = address(this).call(data); 109 | assertFalse(success); 110 | assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); 111 | } 112 | 113 | function test_run() external { 114 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); 115 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); 116 | approve(safe1, abi.encodePacked(r1, s1, v1)); 117 | approve(safe2, abi.encodePacked(r2, s2, v2)); 118 | run(); 119 | } 120 | 121 | function test_run_notApproved() external { 122 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); 123 | approve(safe1, abi.encodePacked(r1, s1, v1)); 124 | bytes memory data = abi.encodeWithSelector(bytes4(keccak256("run()"))); 125 | (bool success, bytes memory result) = address(this).call(data); 126 | assertFalse(success); 127 | assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); 128 | } 129 | } 130 | --------------------------------------------------------------------------------