├── .env.example ├── .solhint.json ├── logo.webp ├── .gitignore ├── bindings ├── go.mod ├── BalanceTracker.abi ├── FeeDisburser.abi └── FeeDisburser.go ├── test ├── revenue-share │ ├── mocks │ │ ├── OptimismWalletRevert.sol │ │ ├── ReenterProcessFees.sol │ │ └── FeeVaultRevert.sol │ ├── BalanceTracker.t.sol │ └── FeeDisburser.t.sol ├── MockERC20.t.sol ├── CommonTest.t.sol ├── smart-escrow │ ├── Resume.t.sol │ ├── UpdateBenefactor.t.sol │ ├── UpdateBeneficiary.t.sol │ ├── WithdrawUnvestedTokens.t.sol │ ├── BaseSmartEscrow.t.sol │ ├── Release.t.sol │ ├── Terminate.t.sol │ └── Constructor.t.sol ├── fee-vault-fixes │ └── e2e │ │ └── FeeVault.t.sol └── Challenger1of2.t.sol ├── foundry.toml ├── remappings.txt ├── src ├── Test.sol ├── TestOwner.sol ├── fee-vault-fixes │ └── FeeVault.sol ├── Challenger1of2.sol ├── Vetoer1of2.sol ├── revenue-share │ ├── BalanceTracker.sol │ └── FeeDisburser.sol └── smart-escrow │ └── SmartEscrow.sol ├── Makefile ├── LICENSE ├── script ├── deploy │ ├── l1 │ │ └── tests │ │ │ ├── DeployTestTokenContracts.s.sol │ │ │ └── TestDeposits.s.sol │ ├── l2 │ │ └── tests │ │ │ ├── TestWithdraw.s.sol │ │ │ └── DeployTestTokenContracts.s.sol │ └── Utils.sol └── universal │ ├── MultisigBuilder.sol │ ├── Simulator.sol │ ├── MultisigBase.sol │ └── NestedMultisigBuilder.sol ├── SECURITY.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | OP_COMMIT= 2 | BASE_MAINNET_URL= 3 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } 4 | -------------------------------------------------------------------------------- /logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexicon179/contracts/HEAD/logo.webp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | cache/*** 3 | out/** 4 | 5 | .env 6 | .gitmodules 7 | 8 | /.idea/ 9 | -------------------------------------------------------------------------------- /bindings/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/base-org/contracts/bindings 2 | 3 | go 1.19 4 | 5 | require ( 6 | ) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | 7 | contract MockERC20 is ERC20 { 8 | /** 9 | * See {ERC20-constructor}. 10 | */ 11 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 12 | 13 | /** 14 | * @dev Mints `amount` tokens to the caller. 15 | * 16 | * See {ERC20-_mint}. 17 | */ 18 | function mint(uint256 amount) public virtual { 19 | _mint(_msgSender(), amount); 20 | } 21 | 22 | /** 23 | * @dev Destroys `amount` tokens from the caller. 24 | * 25 | * See {ERC20-_burn}. 26 | */ 27 | function burn(uint256 amount) public virtual { 28 | _burn(_msgSender(), amount); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | 19 | .PHONY: test 20 | test: 21 | forge test --ffi -vvv 22 | 23 | .PHONY: clean-lib 24 | clean-lib: 25 | rm -rf lib 26 | 27 | .PHONY: checkout-op-commit 28 | checkout-op-commit: 29 | [ -n "$(OP_COMMIT)" ] || (echo "OP_COMMIT must be set in .env" && exit 1) 30 | rm -rf lib/optimism 31 | mkdir -p lib/optimism 32 | cd lib/optimism; \ 33 | git init; \ 34 | git remote add origin https://github.com/ethereum-optimism/optimism.git; \ 35 | git fetch --depth=1 origin $(OP_COMMIT); \ 36 | git reset --hard FETCH_HEAD 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/deploy/l1/tests/DeployTestTokenContracts.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "forge-std/console.sol"; 5 | import "forge-std/Script.sol"; 6 | 7 | import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 8 | import {ERC721PresetMinterPauserAutoId} from "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 9 | 10 | // Deploys test token contracts on L1 to test Base Mainnet's bridging functionality 11 | contract DeployTestTokenContracts is Script { 12 | function run(address _tester) public { 13 | vm.startBroadcast(_tester); 14 | ERC20PresetMinterPauser erc20 = new ERC20PresetMinterPauser("L1 TEST ERC20", "L1T20"); 15 | console.log("TEST ERC20 deployed to: %s", address(erc20)); 16 | 17 | ERC721PresetMinterPauserAutoId erc721 = new ERC721PresetMinterPauserAutoId("L1 TEST ERC721", "L1T721", "not applicable"); 18 | console.log("TEST ERC721 deployed to: %s", address(erc721)); 19 | 20 | erc20.mint(_tester, 1_000_000 ether); 21 | erc721.mint(_tester); 22 | console.log("Minting to tester complete"); 23 | 24 | vm.stopBroadcast(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/deploy/l2/tests/TestWithdraw.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "forge-std/console.sol"; 5 | import "forge-std/Script.sol"; 6 | 7 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 8 | import {L2StandardBridge} from "@eth-optimism-bedrock/src/L2/L2StandardBridge.sol"; 9 | import {L2ERC721Bridge} from "@eth-optimism-bedrock/src/L2/L2ERC721Bridge.sol"; 10 | 11 | // Withdraws tokens from L2 to L1 to test Base Mainnet's bridging functionality 12 | contract TestWithdraw is Script { 13 | function run( 14 | address _tester, 15 | address _l2erc20, 16 | address _l1erc721, 17 | address _l2erc721 18 | ) public { 19 | vm.startBroadcast(_tester); 20 | L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).withdraw( 21 | _l2erc20, 22 | 10_000 ether, 23 | 200_000, 24 | bytes("") 25 | ); 26 | console.log("erc20 withdrawal initiated"); 27 | 28 | L2ERC721Bridge(payable(Predeploys.L2_ERC721_BRIDGE)).bridgeERC721( 29 | _l2erc721, 30 | _l1erc721, 31 | 0, 32 | 200_000, 33 | bytes("") 34 | ); 35 | console.log("erc721 withdrawal initiated"); 36 | 37 | vm.stopBroadcast(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/deploy/l2/tests/DeployTestTokenContracts.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "forge-std/console.sol"; 5 | import "forge-std/Script.sol"; 6 | 7 | import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; 8 | import {OptimismMintableERC20Factory} from "@eth-optimism-bedrock/src/universal/OptimismMintableERC20Factory.sol"; 9 | import {OptimismMintableERC721Factory} from "@eth-optimism-bedrock/src/universal/OptimismMintableERC721Factory.sol"; 10 | 11 | // Deploys test token contracts on L2 to test Base Mainnet functionality 12 | contract DeployTestTokenContracts is Script { 13 | function run( 14 | address _tester, 15 | address _l1erc20, 16 | address _l1erc721 17 | ) public { 18 | vm.startBroadcast(_tester); 19 | address erc20 = OptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).createOptimismMintableERC20( 20 | _l1erc20, 21 | "L2 TEST ERC20", 22 | "L2T20" 23 | ); 24 | console.log("Bridged erc20 deployed to: %s", address(erc20)); 25 | 26 | address erc721 = OptimismMintableERC721Factory(payable(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY)).createOptimismMintableERC721( 27 | _l1erc721, 28 | "L2 TEST ERC721", 29 | "L1T721" 30 | ); 31 | console.log("Bridged erc721 deployed to: %s", address(erc721)); 32 | 33 | vm.stopBroadcast(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /script/deploy/l1/tests/TestDeposits.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "forge-std/console.sol"; 5 | import "forge-std/Script.sol"; 6 | 7 | import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 8 | import {ERC721PresetMinterPauserAutoId} from "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 9 | 10 | import {L1StandardBridge} from "@eth-optimism-bedrock/src/L1/L1StandardBridge.sol"; 11 | import {L1ERC721Bridge} from "@eth-optimism-bedrock/src/L1/L1ERC721Bridge.sol"; 12 | 13 | // Deposits funds to Base Mainnet to test its functionality 14 | contract DeployTestContracts is Script { 15 | function run( 16 | address _tester, 17 | address payable _l1StandardBirdge, 18 | address _l1erc721Bridge, 19 | address payable _l1erc20, 20 | address _l1erc721, 21 | address _l2erc20, 22 | address _l2erc721 23 | ) public { 24 | vm.startBroadcast(_tester); 25 | ERC20PresetMinterPauser(_l1erc20).approve(_l1StandardBirdge, 1_000_000 ether); 26 | ERC721PresetMinterPauserAutoId(_l1erc721).approve(_l1erc721Bridge, 0); 27 | 28 | console.log("Approvals to bridge contracts complete"); 29 | 30 | L1StandardBridge(_l1StandardBirdge).depositERC20( 31 | _l1erc20, 32 | _l2erc20, 33 | 1_000_000 ether, 34 | 200_000, 35 | bytes("") 36 | ); 37 | 38 | console.log("L1StandardBridge erc20 deposit complete"); 39 | 40 | L1ERC721Bridge(_l1erc721Bridge).bridgeERC721( 41 | _l1erc721, 42 | _l2erc721, 43 | 0, 44 | 200_000, 45 | bytes("") 46 | ); 47 | 48 | console.log("L1ERC721Bridge erc721 deposit complete"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | - Copy `.env.example` to `.env` and fill in the variables. 34 | - `make deps` 35 | - Test contracts: `make test` 36 | -------------------------------------------------------------------------------- /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( 34 | FeeVault_Fix.setTotalProcessed, 35 | ZERO_VALUE 36 | ); 37 | 38 | assertNotEq(L1FeeVault_Final(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 39 | vm.prank(Predeploys.PROXY_ADMIN); 40 | Proxy(payable(Predeploys.L1_FEE_VAULT)).upgradeToAndCall(address(l1FeeVaultFix), setTotalProcessedCall); 41 | assertEq(FeeVault_Fix(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 42 | 43 | vm.prank(Predeploys.PROXY_ADMIN); 44 | Proxy(payable(Predeploys.L1_FEE_VAULT)).upgradeTo(address(l1FeeVaultFinal)); 45 | assertEq(L1FeeVault_Final(payable(Predeploys.L1_FEE_VAULT)).totalProcessed(), ZERO_VALUE); 46 | } 47 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 counter party'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( 44 | address indexed _caller, 45 | bytes _data, 46 | bytes _result 47 | ); 48 | 49 | /*////////////////////////////////////////////////////////////// 50 | Constructor 51 | //////////////////////////////////////////////////////////////*/ 52 | /** 53 | * @dev Constructor to set the values of the constants. 54 | * @param _opSigner Address of Optimism signer. 55 | * @param _otherSigner Address of counter party signer. 56 | * @param _l2OutputOracleProxy Address of the L2OutputOracleProxy contract. 57 | */ 58 | constructor(address _opSigner, address _otherSigner, address _l2OutputOracleProxy) { 59 | require(_opSigner != address(0), "Challenger1of2: opSigner cannot be zero address"); 60 | require(_otherSigner != address(0), "Challenger1of2: otherSigner cannot be zero address"); 61 | require( 62 | _l2OutputOracleProxy.isContract(), 63 | "Challenger1of2: l2OutputOracleProxy must be a contract" 64 | ); 65 | 66 | OP_SIGNER = _opSigner; 67 | OTHER_SIGNER = _otherSigner; 68 | L2_OUTPUT_ORACLE_PROXY = _l2OutputOracleProxy; 69 | } 70 | 71 | /*////////////////////////////////////////////////////////////// 72 | External Functions 73 | //////////////////////////////////////////////////////////////*/ 74 | /** 75 | * @dev Executes a call as the Challenger (must be called by 76 | * Optimism or counter party signer). 77 | * @param _data Data for function call. 78 | */ 79 | function execute(bytes memory _data) external { 80 | require( 81 | msg.sender == OTHER_SIGNER || msg.sender == OP_SIGNER, 82 | "Challenger1of2: must be an approved signer to execute" 83 | ); 84 | 85 | bytes memory result = Address.functionCall( 86 | L2_OUTPUT_ORACLE_PROXY, 87 | _data, 88 | "Challenger1of2: failed to execute" 89 | ); 90 | 91 | emit ChallengerCallExecuted(msg.sender, _data, result); 92 | } 93 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /bindings/BalanceTracker.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address payable", 6 | "name": "_profitWallet", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": false, 18 | "internalType": "uint8", 19 | "name": "version", 20 | "type": "uint8" 21 | } 22 | ], 23 | "name": "Initialized", 24 | "type": "event" 25 | }, 26 | { 27 | "anonymous": false, 28 | "inputs": [ 29 | { 30 | "indexed": true, 31 | "internalType": "address", 32 | "name": "_systemAddress", 33 | "type": "address" 34 | }, 35 | { 36 | "indexed": true, 37 | "internalType": "bool", 38 | "name": "_success", 39 | "type": "bool" 40 | }, 41 | { 42 | "indexed": false, 43 | "internalType": "uint256", 44 | "name": "_balanceNeeded", 45 | "type": "uint256" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "uint256", 50 | "name": "_balanceSent", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "ProcessedFunds", 55 | "type": "event" 56 | }, 57 | { 58 | "anonymous": false, 59 | "inputs": [ 60 | { 61 | "indexed": true, 62 | "internalType": "address", 63 | "name": "_sender", 64 | "type": "address" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "uint256", 69 | "name": "_amount", 70 | "type": "uint256" 71 | } 72 | ], 73 | "name": "ReceivedFunds", 74 | "type": "event" 75 | }, 76 | { 77 | "anonymous": false, 78 | "inputs": [ 79 | { 80 | "indexed": true, 81 | "internalType": "address", 82 | "name": "_profitWallet", 83 | "type": "address" 84 | }, 85 | { 86 | "indexed": true, 87 | "internalType": "bool", 88 | "name": "_success", 89 | "type": "bool" 90 | }, 91 | { 92 | "indexed": false, 93 | "internalType": "uint256", 94 | "name": "_balanceSent", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "SentProfit", 99 | "type": "event" 100 | }, 101 | { 102 | "inputs": [], 103 | "name": "MAX_SYSTEM_ADDRESS_COUNT", 104 | "outputs": [ 105 | { 106 | "internalType": "uint256", 107 | "name": "", 108 | "type": "uint256" 109 | } 110 | ], 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "inputs": [], 116 | "name": "PROFIT_WALLET", 117 | "outputs": [ 118 | { 119 | "internalType": "address payable", 120 | "name": "", 121 | "type": "address" 122 | } 123 | ], 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "inputs": [ 129 | { 130 | "internalType": "address payable[]", 131 | "name": "_systemAddresses", 132 | "type": "address[]" 133 | }, 134 | { 135 | "internalType": "uint256[]", 136 | "name": "_targetBalances", 137 | "type": "uint256[]" 138 | } 139 | ], 140 | "name": "initialize", 141 | "outputs": [], 142 | "stateMutability": "nonpayable", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [], 147 | "name": "processFees", 148 | "outputs": [], 149 | "stateMutability": "nonpayable", 150 | "type": "function" 151 | }, 152 | { 153 | "inputs": [ 154 | { 155 | "internalType": "uint256", 156 | "name": "", 157 | "type": "uint256" 158 | } 159 | ], 160 | "name": "systemAddresses", 161 | "outputs": [ 162 | { 163 | "internalType": "address payable", 164 | "name": "", 165 | "type": "address" 166 | } 167 | ], 168 | "stateMutability": "view", 169 | "type": "function" 170 | }, 171 | { 172 | "inputs": [ 173 | { 174 | "internalType": "uint256", 175 | "name": "", 176 | "type": "uint256" 177 | } 178 | ], 179 | "name": "targetBalances", 180 | "outputs": [ 181 | { 182 | "internalType": "uint256", 183 | "name": "", 184 | "type": "uint256" 185 | } 186 | ], 187 | "stateMutability": "view", 188 | "type": "function" 189 | }, 190 | { 191 | "stateMutability": "payable", 192 | "type": "receive" 193 | } 194 | ] 195 | -------------------------------------------------------------------------------- /bindings/FeeDisburser.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address payable", 6 | "name": "_optimismWallet", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_l1Wallet", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "_feeDisbursementInterval", 17 | "type": "uint256" 18 | } 19 | ], 20 | "stateMutability": "nonpayable", 21 | "type": "constructor" 22 | }, 23 | { 24 | "anonymous": false, 25 | "inputs": [ 26 | { 27 | "indexed": false, 28 | "internalType": "uint256", 29 | "name": "_disbursementTime", 30 | "type": "uint256" 31 | }, 32 | { 33 | "indexed": false, 34 | "internalType": "uint256", 35 | "name": "_paidToOptimism", 36 | "type": "uint256" 37 | }, 38 | { 39 | "indexed": false, 40 | "internalType": "uint256", 41 | "name": "_totalFeesDisbursed", 42 | "type": "uint256" 43 | } 44 | ], 45 | "name": "FeesDisbursed", 46 | "type": "event" 47 | }, 48 | { 49 | "anonymous": false, 50 | "inputs": [ 51 | { 52 | "indexed": true, 53 | "internalType": "address", 54 | "name": "_sender", 55 | "type": "address" 56 | }, 57 | { 58 | "indexed": false, 59 | "internalType": "uint256", 60 | "name": "_amount", 61 | "type": "uint256" 62 | } 63 | ], 64 | "name": "FeesReceived", 65 | "type": "event" 66 | }, 67 | { 68 | "anonymous": false, 69 | "inputs": [], 70 | "name": "NoFeesCollected", 71 | "type": "event" 72 | }, 73 | { 74 | "inputs": [], 75 | "name": "BASIS_POINT_SCALE", 76 | "outputs": [ 77 | { 78 | "internalType": "uint32", 79 | "name": "", 80 | "type": "uint32" 81 | } 82 | ], 83 | "stateMutability": "view", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [], 88 | "name": "FEE_DISBURSEMENT_INTERVAL", 89 | "outputs": [ 90 | { 91 | "internalType": "uint256", 92 | "name": "", 93 | "type": "uint256" 94 | } 95 | ], 96 | "stateMutability": "view", 97 | "type": "function" 98 | }, 99 | { 100 | "inputs": [], 101 | "name": "L1_WALLET", 102 | "outputs": [ 103 | { 104 | "internalType": "address", 105 | "name": "", 106 | "type": "address" 107 | } 108 | ], 109 | "stateMutability": "view", 110 | "type": "function" 111 | }, 112 | { 113 | "inputs": [], 114 | "name": "OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS", 115 | "outputs": [ 116 | { 117 | "internalType": "uint256", 118 | "name": "", 119 | "type": "uint256" 120 | } 121 | ], 122 | "stateMutability": "view", 123 | "type": "function" 124 | }, 125 | { 126 | "inputs": [], 127 | "name": "OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS", 128 | "outputs": [ 129 | { 130 | "internalType": "uint256", 131 | "name": "", 132 | "type": "uint256" 133 | } 134 | ], 135 | "stateMutability": "view", 136 | "type": "function" 137 | }, 138 | { 139 | "inputs": [], 140 | "name": "OPTIMISM_WALLET", 141 | "outputs": [ 142 | { 143 | "internalType": "address payable", 144 | "name": "", 145 | "type": "address" 146 | } 147 | ], 148 | "stateMutability": "view", 149 | "type": "function" 150 | }, 151 | { 152 | "inputs": [], 153 | "name": "WITHDRAWAL_MIN_GAS", 154 | "outputs": [ 155 | { 156 | "internalType": "uint32", 157 | "name": "", 158 | "type": "uint32" 159 | } 160 | ], 161 | "stateMutability": "view", 162 | "type": "function" 163 | }, 164 | { 165 | "inputs": [], 166 | "name": "disburseFees", 167 | "outputs": [], 168 | "stateMutability": "nonpayable", 169 | "type": "function" 170 | }, 171 | { 172 | "inputs": [], 173 | "name": "lastDisbursementTime", 174 | "outputs": [ 175 | { 176 | "internalType": "uint256", 177 | "name": "", 178 | "type": "uint256" 179 | } 180 | ], 181 | "stateMutability": "view", 182 | "type": "function" 183 | }, 184 | { 185 | "inputs": [], 186 | "name": "netFeeRevenue", 187 | "outputs": [ 188 | { 189 | "internalType": "uint256", 190 | "name": "", 191 | "type": "uint256" 192 | } 193 | ], 194 | "stateMutability": "view", 195 | "type": "function" 196 | }, 197 | { 198 | "stateMutability": "payable", 199 | "type": "receive" 200 | } 201 | ] 202 | -------------------------------------------------------------------------------- /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 counter party'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 unhautorized 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 counter party 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 | -------------------------------------------------------------------------------- /script/deploy/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "forge-std/Script.sol"; 5 | import "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 = vm.serializeAddress( 119 | json, "OptimismMintableERC721Factory", cfg.OptimismMintableERC721Factory 120 | ); 121 | 122 | finalJson.write(string.concat("unsortedl2Impls.json")); 123 | } 124 | } -------------------------------------------------------------------------------- /script/universal/MultisigBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "./MultisigBase.sol"; 5 | 6 | import { console } from "forge-std/console.sol"; 7 | import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; 8 | import { Vm } from "forge-std/Vm.sol"; 9 | 10 | /** 11 | * @title MultisigBuilder 12 | * @notice Modeled from Optimism's SafeBuilder, but using signatures instead of approvals. 13 | */ 14 | abstract contract MultisigBuilder is MultisigBase { 15 | /** 16 | * ----------------------------------------------------------- 17 | * Virtual Functions 18 | * ----------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * @notice Follow up assertions to ensure that the script ran to completion. 23 | */ 24 | function _postCheck(Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) internal virtual; 25 | 26 | /** 27 | * @notice Creates the calldata 28 | */ 29 | function _buildCalls() internal virtual view returns (IMulticall3.Call3[] memory); 30 | 31 | /** 32 | * @notice Returns the safe address to execute the transaction from 33 | */ 34 | function _ownerSafe() internal virtual view returns (address); 35 | 36 | /** 37 | * ----------------------------------------------------------- 38 | * Implemented Functions 39 | * ----------------------------------------------------------- 40 | */ 41 | 42 | /** 43 | * Step 1 44 | * ====== 45 | * Generate a transaction execution data to sign. This method should be called by a threshold-1 46 | * of members of the multisig that will execute the transaction. Signers will pass their 47 | * signature to the final signer of this multisig. 48 | * 49 | * Alternatively, this method can be called by a threshold of signers, and those signatures 50 | * used by a separate tx executor address in step 2, which doesn't have to be a signer. 51 | */ 52 | function sign() public { 53 | address safe = _ownerSafe(); 54 | 55 | // Snapshot and restore Safe nonce after simulation, otherwise the data logged to sign 56 | // would not match the actual data we need to sign, because the simulation 57 | // would increment the nonce. 58 | uint256 originalNonce = _getNonce(IGnosisSafe(safe)); 59 | 60 | IMulticall3.Call3[] memory calls = _buildCalls(); 61 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _simulateForSigner(safe, calls); 62 | _postCheck(accesses, simPayload); 63 | 64 | // Restore the original nonce. 65 | vm.store(safe, SAFE_NONCE_SLOT, bytes32(uint256(originalNonce))); 66 | 67 | _printDataToSign(safe, calls); 68 | } 69 | 70 | /** 71 | * Step 2 72 | * ====== 73 | * Verify the signatures generated from step 1 are valid. 74 | * This allow transactions to be pre-signed and stored safely before execution. 75 | */ 76 | function verify(bytes memory _signatures) public view { 77 | _checkSignatures(_ownerSafe(), _buildCalls(), _signatures); 78 | } 79 | 80 | function nonce() public view { 81 | IGnosisSafe safe = IGnosisSafe(payable(_ownerSafe())); 82 | console.log("Nonce:", safe.nonce()); 83 | } 84 | 85 | /** 86 | * Step 3 87 | * ====== 88 | * Simulate the transaction. This method should be called by the final member of the multisig 89 | * that will execute the transaction. Signatures from step 1 are required. 90 | */ 91 | function simulateSigned(bytes memory _signatures) public { 92 | address _safe = _ownerSafe(); 93 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 94 | uint256 _nonce = _getNonce(safe); 95 | vm.store(_safe, SAFE_NONCE_SLOT, bytes32(uint256(_nonce))); 96 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _executeTransaction(_safe, _buildCalls(), _signatures); 97 | _postCheck(accesses, simPayload); 98 | } 99 | 100 | /** 101 | * Step 4 102 | * ====== 103 | * Execute the transaction. This method should be called by the final member of the multisig 104 | * that will execute the transaction. Signatures from step 1 are required. 105 | * 106 | * Alternatively, this method can be called after a threshold of signatures is collected from 107 | * step 1. In this scenario, the caller doesn't need to be a signer of the multisig. 108 | */ 109 | function run(bytes memory _signatures) public { 110 | vm.startBroadcast(); 111 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _executeTransaction(_ownerSafe(), _buildCalls(), _signatures); 112 | vm.stopBroadcast(); 113 | 114 | _postCheck(accesses, simPayload); 115 | } 116 | 117 | function _simulateForSigner(address _safe, IMulticall3.Call3[] memory _calls) 118 | internal 119 | returns (Vm.AccountAccess[] memory, SimulationPayload memory) 120 | { 121 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 122 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); 123 | 124 | SimulationStateOverride[] memory overrides = new SimulationStateOverride[](2); 125 | overrides[0] = _addOverrides(_safe); 126 | overrides[1] = _addGenericOverrides(); 127 | 128 | bytes memory txData = abi.encodeCall(safe.execTransaction, 129 | ( 130 | address(multicall), 131 | 0, 132 | data, 133 | Enum.Operation.DelegateCall, 134 | 0, 135 | 0, 136 | 0, 137 | address(0), 138 | payable(address(0)), 139 | prevalidatedSignature(msg.sender) 140 | ) 141 | ); 142 | 143 | logSimulationLink({ 144 | _to: _safe, 145 | _data: txData, 146 | _from: msg.sender, 147 | _overrides: overrides 148 | }); 149 | 150 | // Forge simulation of the data logged in the link. If the simulation fails 151 | // we revert to make it explicit that the simulation failed. 152 | SimulationPayload memory simPayload = SimulationPayload({ 153 | to: _safe, 154 | data: txData, 155 | from: msg.sender, 156 | stateOverrides: overrides 157 | }); 158 | Vm.AccountAccess[] memory accesses = simulateFromSimPayload(simPayload); 159 | return (accesses, simPayload); 160 | } 161 | 162 | // The state change simulation can set the threshold, owner address and/or nonce. 163 | // This allows a non-signing owner to simulate the transaction 164 | // State changes reflected in the simulation as a result of these overrides 165 | // will not be reflected in the prod execution. 166 | // This particular implementation can be overwritten by an inheriting script. The 167 | // default logic is vestigial for backwards compatibility. 168 | function _addOverrides(address _safe) internal virtual view returns (SimulationStateOverride memory) { 169 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 170 | uint256 _nonce = _getNonce(safe); 171 | return overrideSafeThresholdAndNonce(_safe, _nonce); 172 | } 173 | 174 | // Tenderly simulations can accept generic state overrides. This hook enables this functionality. 175 | // By default, an empty (no-op) override is returned 176 | function _addGenericOverrides() internal virtual view returns (SimulationStateOverride memory override_) {} 177 | } 178 | -------------------------------------------------------------------------------- /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( 34 | address indexed _caller, 35 | bytes _data, 36 | bytes _result 37 | ); 38 | 39 | event OutputsDeleted( 40 | address indexed _caller, 41 | uint256 indexed prevNextOutputIndex, 42 | uint256 indexed newNextOutputIndex 43 | ); 44 | 45 | function setUp() public { 46 | vm.prank(deployer); 47 | proxyAdmin = new ProxyAdmin(deployer); 48 | l2OutputOracleProxy = new Proxy(address(proxyAdmin)); 49 | 50 | challenger = new Challenger1of2( 51 | optimismWallet, coinbaseWallet, address(l2OutputOracleProxy) 52 | ); 53 | 54 | // Initialize L2OutputOracle implementation. 55 | l2OutputOracle = new L2OutputOracle(); 56 | 57 | vm.prank(deployer); 58 | // Upgrade and initialize L2OutputOracle. 59 | proxyAdmin.upgradeAndCall( 60 | payable(l2OutputOracleProxy), 61 | address(l2OutputOracle), 62 | abi.encodeCall( 63 | L2OutputOracle.initialize, 64 | ( 65 | NONZERO_INTEGER, // _submissionInterval 66 | NONZERO_INTEGER, // _l2BlockTime 67 | ZERO, // _startingBlockNumber 68 | NONZERO_INTEGER, // _startingTimestamp 69 | proposer, // _proposer 70 | address(challenger), // _challenger 71 | NONZERO_INTEGER // _finalizationPeriodSeconds 72 | ) 73 | ) 74 | ); 75 | } 76 | 77 | function test_constructor_cbSigner_zeroAddress_fails() external { 78 | vm.expectRevert("Challenger1of2: otherSigner cannot be zero address"); 79 | new Challenger1of2(optimismWallet, address(0), address(l2OutputOracleProxy)); 80 | } 81 | 82 | function test_constructor_opSigner_zeroAddress_fails() external { 83 | vm.expectRevert("Challenger1of2: opSigner cannot be zero address"); 84 | new Challenger1of2(address(0), coinbaseWallet, address(l2OutputOracleProxy)); 85 | } 86 | 87 | function test_constructor_l2OO_zeroAddress_fails() external { 88 | vm.expectRevert("Challenger1of2: l2OutputOracleProxy must be a contract"); 89 | new Challenger1of2(optimismWallet, coinbaseWallet, address(0)); 90 | } 91 | 92 | function test_constructor_success() external { 93 | Challenger1of2 challenger2 = new Challenger1of2( 94 | optimismWallet, coinbaseWallet, address(l2OutputOracleProxy) 95 | ); 96 | assertEq(challenger2.OP_SIGNER(), optimismWallet); 97 | assertEq(challenger2.OTHER_SIGNER(), coinbaseWallet); 98 | assertEq(challenger2.L2_OUTPUT_ORACLE_PROXY(), address(l2OutputOracleProxy)); 99 | } 100 | 101 | function test_execute_unauthorized_call_fails() external { 102 | vm.prank(randomWallet); 103 | vm.expectRevert("Challenger1of2: must be an approved signer to execute"); 104 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 105 | } 106 | 107 | function test_execute_call_fails() external { 108 | vm.prank(optimismWallet); 109 | vm.expectRevert("Challenger1of2: failed to execute"); 110 | challenger.execute(NONEXISTENT_SIGNATURE); 111 | } 112 | 113 | function test_unauthorized_challenger_fails() external { 114 | // Try to make a call from a second challenger contract (not the official one) 115 | Challenger1of2 otherChallenger = new Challenger1of2( 116 | optimismWallet, coinbaseWallet, address(l2OutputOracleProxy) 117 | ); 118 | vm.prank(optimismWallet); 119 | vm.expectRevert("L2OutputOracle: only the challenger address can delete outputs"); 120 | otherChallenger.execute(DELETE_OUTPUTS_SIGNATURE); 121 | } 122 | 123 | function test_execute_opSigner_success() external { 124 | _proposeOutput(); 125 | _proposeOutput(); 126 | 127 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 128 | // Check that the outputs were proposed. 129 | assertFalse(oracle.latestOutputIndex() == ZERO); 130 | 131 | // We expect the OutputsDeleted event to be emitted 132 | vm.expectEmit(true, true, true, true, address(challenger)); 133 | 134 | // Emit the event we expect to see 135 | emit ChallengerCallExecuted(optimismWallet, DELETE_OUTPUTS_SIGNATURE, ZERO_OUTPUT); 136 | 137 | // We expect deleteOutputs to be called 138 | vm.expectCall( 139 | address(l2OutputOracleProxy), 140 | abi.encodeWithSignature("deleteL2Outputs(uint256)", 1) 141 | ); 142 | 143 | // Make the call 144 | vm.prank(optimismWallet); 145 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 146 | 147 | // Check that the outputs were deleted. 148 | assertEq(oracle.latestOutputIndex(), ZERO); 149 | } 150 | 151 | function test_execute_cbSigner_success() external { 152 | _proposeOutput(); 153 | _proposeOutput(); 154 | 155 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 156 | // Check that the outputs were proposed. 157 | assertFalse(oracle.latestOutputIndex() == ZERO); 158 | 159 | // We expect the OutputsDeleted event to be emitted 160 | vm.expectEmit(true, true, true, true, address(challenger)); 161 | 162 | // Emit the event we expect to see 163 | emit ChallengerCallExecuted(coinbaseWallet, DELETE_OUTPUTS_SIGNATURE, ZERO_OUTPUT); 164 | 165 | // We expect deleteOutputs to be called 166 | vm.expectCall( 167 | address(l2OutputOracleProxy), 168 | abi.encodeWithSignature("deleteL2Outputs(uint256)", 1) 169 | ); 170 | 171 | // Make the call 172 | vm.prank(coinbaseWallet); 173 | challenger.execute(DELETE_OUTPUTS_SIGNATURE); 174 | 175 | // Check that the outputs were deleted. 176 | assertEq(oracle.latestOutputIndex(), ZERO); 177 | } 178 | 179 | function _proposeOutput() internal { 180 | L2OutputOracle oracle = L2OutputOracle(address(l2OutputOracleProxy)); 181 | vm.warp(oracle.computeL2Timestamp(oracle.nextBlockNumber()) + 1); 182 | 183 | vm.startPrank(proposer); 184 | oracle.proposeL2Output( 185 | bytes32("something"), 186 | oracle.nextBlockNumber(), 187 | blockhash(10), 188 | 10 189 | ); 190 | vm.stopPrank(); 191 | } 192 | } -------------------------------------------------------------------------------- /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 } 7 | from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 8 | 9 | import { SafeCall } from "@eth-optimism-bedrock/src/libraries/SafeCall.sol"; 10 | 11 | /** 12 | * @title BalanceTracker 13 | * @dev Funds system addresses and sends the remaining profits to the profit wallet. 14 | */ 15 | contract BalanceTracker is ReentrancyGuardUpgradeable { 16 | using Address for address; 17 | /*////////////////////////////////////////////////////////////// 18 | Constants 19 | //////////////////////////////////////////////////////////////*/ 20 | /** 21 | * @dev The maximum number of system addresses that can be funded. 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, 57 | bool indexed _success, 58 | uint256 _balanceNeeded, 59 | uint256 _balanceSent 60 | ); 61 | /** 62 | * @dev Emitted when the BalanceTracker attempts to send funds to the profit wallet. 63 | * @param _profitWallet The address of the profit wallet. 64 | * @param _success A boolean denoting the success or failure of fund send. 65 | * @param _balanceSent The amount of funds sent to the profit wallet. 66 | */ 67 | event SentProfit( 68 | address indexed _profitWallet, 69 | bool indexed _success, 70 | uint256 _balanceSent 71 | ); 72 | /** 73 | * @dev Emitted when funds are received. 74 | * @param _sender The address sending funds. 75 | * @param _amount The amount of funds received from the sender. 76 | */ 77 | event ReceivedFunds( 78 | address indexed _sender, 79 | uint256 _amount 80 | ); 81 | 82 | /*////////////////////////////////////////////////////////////// 83 | Constructor 84 | //////////////////////////////////////////////////////////////*/ 85 | /** 86 | * @dev Constructor for the BalanceTracker contract that sets an immutable variable. 87 | * @param _profitWallet The address to send remaining ETH profits to. 88 | */ 89 | constructor( 90 | address payable _profitWallet 91 | ) { 92 | require(_profitWallet != address(0), "BalanceTracker: PROFIT_WALLET cannot be address(0)"); 93 | 94 | PROFIT_WALLET = _profitWallet; 95 | 96 | _disableInitializers(); 97 | } 98 | 99 | /*////////////////////////////////////////////////////////////// 100 | External Functions 101 | //////////////////////////////////////////////////////////////*/ 102 | /** 103 | * @dev Initializes the BalanceTracker contract. 104 | * @param _systemAddresses The system addresses being funded. 105 | * @param _targetBalances The target balances for system addresses. 106 | */ 107 | function initialize( 108 | address payable[] memory _systemAddresses, 109 | uint256[] memory _targetBalances 110 | ) external initializer { 111 | uint256 systemAddresesLength = _systemAddresses.length; 112 | require(systemAddresesLength > 0, 113 | "BalanceTracker: systemAddresses cannot have a length of zero"); 114 | require(systemAddresesLength <= MAX_SYSTEM_ADDRESS_COUNT, 115 | "BalanceTracker: systemAddresses cannot have a length greater than 20"); 116 | require(systemAddresesLength == _targetBalances.length, 117 | "BalanceTracker: systemAddresses and targetBalances length must be equal"); 118 | for (uint256 i; i < systemAddresesLength;) { 119 | require(_systemAddresses[i] != address(0), "BalanceTracker: systemAddresses cannot contain address(0)"); 120 | require(_targetBalances[i] > 0, "BalanceTracker: targetBalances cannot contain 0 target"); 121 | unchecked { i++; } 122 | } 123 | 124 | systemAddresses = _systemAddresses; 125 | targetBalances = _targetBalances; 126 | 127 | __ReentrancyGuard_init(); 128 | } 129 | 130 | /** 131 | * @dev Funds system addresses and sends remaining profits to the profit wallet. 132 | * 133 | */ 134 | function processFees() external nonReentrant { 135 | uint256 systemAddresesLength = systemAddresses.length; 136 | require(systemAddresesLength > 0, 137 | "BalanceTracker: systemAddresses cannot have a length of zero"); 138 | // Refills balances of systems addresses up to their target balances 139 | for (uint256 i; i < systemAddresesLength;) { 140 | refillBalanceIfNeeded(systemAddresses[i], targetBalances[i]); 141 | unchecked { i++; } 142 | } 143 | 144 | // Send remaining profits to profit wallet 145 | uint256 valueToSend = address(this).balance; 146 | bool success = SafeCall.send(PROFIT_WALLET, gasleft(), valueToSend); 147 | emit SentProfit(PROFIT_WALLET, success, valueToSend); 148 | } 149 | 150 | /** 151 | * @dev Fallback function to receive funds from L2 fee withdrawals and additional top up funds if 152 | * L2 fees are insufficient to fund L1 system addresses. 153 | */ 154 | receive() external payable { 155 | emit ReceivedFunds(msg.sender, msg.value); 156 | } 157 | 158 | /*////////////////////////////////////////////////////////////// 159 | Internal Functions 160 | //////////////////////////////////////////////////////////////*/ 161 | /** 162 | * @dev Checks the balance of the target address and refills it back up to the target balance if needed. 163 | * @param _systemAddress The system address being funded. 164 | * @param _targetBalance The target balance for the system address being funded. 165 | */ 166 | function refillBalanceIfNeeded(address _systemAddress, uint256 _targetBalance) internal { 167 | uint256 systemAddressBalance = _systemAddress.balance; 168 | if (systemAddressBalance >= _targetBalance) { 169 | emit ProcessedFunds(_systemAddress, false, 0, 0); 170 | return; 171 | } 172 | 173 | uint256 valueNeeded = _targetBalance - systemAddressBalance; 174 | uint256 balanceTrackerBalance = address(this).balance; 175 | uint256 valueToSend = valueNeeded > balanceTrackerBalance ? balanceTrackerBalance : valueNeeded; 176 | 177 | bool success = SafeCall.send(_systemAddress, gasleft(), valueToSend); 178 | emit ProcessedFunds(_systemAddress, success, valueNeeded, valueToSend); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /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_startTimeZero_fails() public { 97 | vm.warp(100); 98 | bytes4 pastStartTimeSelector = bytes4(keccak256("StartTimeCannotBeInPast(uint256,uint256)")); 99 | vm.expectRevert(abi.encodeWithSelector(pastStartTimeSelector, 0, 100)); 100 | new SmartEscrow( 101 | benefactor, 102 | beneficiary, 103 | benefactorOwner, 104 | beneficiaryOwner, 105 | escrowOwner, 106 | 0, 107 | cliffStart, 108 | end, 109 | vestingPeriod, 110 | initialTokens, 111 | vestingEventTokens 112 | ); 113 | } 114 | 115 | function test_constructor_cliffStartTimeZero_fails() public { 116 | vm.warp(100); 117 | bytes4 pastStartTimeSelector = bytes4(keccak256("CliffStartTimeInvalid(uint256,uint256)")); 118 | vm.expectRevert(abi.encodeWithSelector(pastStartTimeSelector, 0, start)); 119 | new SmartEscrow( 120 | benefactor, 121 | beneficiary, 122 | benefactorOwner, 123 | beneficiaryOwner, 124 | escrowOwner, 125 | start, 126 | 0, 127 | end, 128 | vestingPeriod, 129 | initialTokens, 130 | vestingEventTokens 131 | ); 132 | } 133 | 134 | function test_constructor_startAfterEnd_fails() public { 135 | bytes4 startAfterEndSelector = bytes4(keccak256("StartTimeAfterEndTime(uint256,uint256)")); 136 | vm.expectRevert(abi.encodeWithSelector(startAfterEndSelector, end, end)); 137 | new SmartEscrow( 138 | benefactor, 139 | beneficiary, 140 | benefactorOwner, 141 | beneficiaryOwner, 142 | escrowOwner, 143 | end, 144 | cliffStart, 145 | end, 146 | vestingPeriod, 147 | initialTokens, 148 | vestingEventTokens 149 | ); 150 | } 151 | 152 | function test_constructor_cliffStartAfterEnd_fails() public { 153 | bytes4 startAfterEndSelector = bytes4(keccak256("CliffStartTimeAfterEndTime(uint256,uint256)")); 154 | vm.expectRevert(abi.encodeWithSelector(startAfterEndSelector, end, end)); 155 | new SmartEscrow( 156 | benefactor, 157 | beneficiary, 158 | benefactorOwner, 159 | beneficiaryOwner, 160 | escrowOwner, 161 | start, 162 | end, 163 | end, 164 | vestingPeriod, 165 | initialTokens, 166 | vestingEventTokens 167 | ); 168 | } 169 | 170 | function test_constructor_vestingPeriodZero_fails() public { 171 | bytes4 vestingPeriodZeroSelector = bytes4(keccak256("VestingPeriodIsZeroSeconds()")); 172 | vm.expectRevert(abi.encodeWithSelector(vestingPeriodZeroSelector)); 173 | new SmartEscrow( 174 | benefactor, 175 | beneficiary, 176 | benefactorOwner, 177 | beneficiaryOwner, 178 | escrowOwner, 179 | start, 180 | cliffStart, 181 | end, 182 | 0, 183 | initialTokens, 184 | vestingEventTokens 185 | ); 186 | } 187 | 188 | function test_constructor_vestingEventTokensZero_fails() public { 189 | bytes4 vestingEventTokensZeroSelector = bytes4(keccak256("VestingEventTokensIsZero()")); 190 | vm.expectRevert(abi.encodeWithSelector(vestingEventTokensZeroSelector)); 191 | new SmartEscrow( 192 | benefactor, 193 | beneficiary, 194 | benefactorOwner, 195 | beneficiaryOwner, 196 | escrowOwner, 197 | start, 198 | cliffStart, 199 | end, 200 | vestingPeriod, 201 | initialTokens, 202 | 0 203 | ); 204 | } 205 | 206 | function test_constructor_vestingPeriodExceedsContractDuration_fails() public { 207 | bytes4 vestingPeriodExceedsContractDurationSelector = bytes4(keccak256("VestingPeriodExceedsContractDuration(uint256)")); 208 | vm.expectRevert(abi.encodeWithSelector(vestingPeriodExceedsContractDurationSelector, end)); 209 | new SmartEscrow( 210 | benefactor, 211 | beneficiary, 212 | benefactorOwner, 213 | beneficiaryOwner, 214 | escrowOwner, 215 | start, 216 | cliffStart, 217 | end, 218 | end, 219 | initialTokens, 220 | vestingEventTokens 221 | ); 222 | } 223 | 224 | function test_constructor_unevenVestingPeriod_fails() public { 225 | bytes4 unevenVestingPeriodSelector = bytes4(keccak256("UnevenVestingPeriod(uint256,uint256,uint256)")); 226 | uint256 unevenVestingPeriod = 7; 227 | vm.expectRevert(abi.encodeWithSelector(unevenVestingPeriodSelector, unevenVestingPeriod, start, end)); 228 | new SmartEscrow( 229 | benefactor, 230 | beneficiary, 231 | benefactorOwner, 232 | beneficiaryOwner, 233 | escrowOwner, 234 | start, 235 | cliffStart, 236 | end, 237 | unevenVestingPeriod, 238 | initialTokens, 239 | vestingEventTokens 240 | ); 241 | } 242 | } -------------------------------------------------------------------------------- /script/universal/Simulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import { console } from "forge-std/console.sol"; 5 | import { CommonBase } from "forge-std/Base.sol"; 6 | import { Vm } from "forge-std/Vm.sol"; 7 | 8 | abstract contract Simulator is CommonBase { 9 | struct SimulationStateOverride { 10 | address contractAddress; 11 | SimulationStorageOverride[] overrides; 12 | } 13 | 14 | struct SimulationStorageOverride { 15 | bytes32 key; 16 | bytes32 value; 17 | } 18 | 19 | struct SimulationPayload { 20 | address from; 21 | address to; 22 | bytes data; 23 | SimulationStateOverride[] stateOverrides; 24 | } 25 | 26 | function simulateFromSimPayload(SimulationPayload memory simPayload) internal returns (Vm.AccountAccess[] memory) { 27 | require(simPayload.from != address(0), "Simulator::simulateFromSimPayload: from address cannot be zero address"); 28 | require(simPayload.to != address(0), "Simulator::simulateFromSimPayload: to address cannot be zero address"); 29 | 30 | // Apply state overrides. 31 | SimulationStateOverride[] memory stateOverrides = simPayload.stateOverrides; 32 | for (uint256 i; i < stateOverrides.length; i++) { 33 | SimulationStateOverride memory stateOverride = stateOverrides[i]; 34 | SimulationStorageOverride[] memory storageOverrides = stateOverride.overrides; 35 | for (uint256 j; j < storageOverrides.length; j++) { 36 | SimulationStorageOverride memory storageOverride = storageOverrides[j]; 37 | vm.store(stateOverride.contractAddress, storageOverride.key, storageOverride.value); 38 | } 39 | } 40 | 41 | // Execute the call in forge and return the state diff. 42 | vm.startStateDiffRecording(); 43 | vm.prank(simPayload.from); 44 | (bool ok, bytes memory returnData) = address(simPayload.to).call(simPayload.data); 45 | Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); 46 | require(ok, string.concat("Simulator::simulateFromSimPayload failed: ", vm.toString(returnData))); 47 | require(accesses.length > 0, "Simulator::simulateFromSimPayload: No state changes"); 48 | return accesses; 49 | } 50 | 51 | function overrideSafeThreshold(address _safe) public pure returns (SimulationStateOverride memory) { 52 | return addThresholdOverride(SimulationStateOverride({ 53 | contractAddress: _safe, 54 | overrides: new SimulationStorageOverride[](0) 55 | })); 56 | } 57 | 58 | function overrideSafeThresholdAndNonce(address _safe, uint256 _nonce) public view returns (SimulationStateOverride memory) { 59 | SimulationStateOverride memory state = overrideSafeThreshold(_safe); 60 | state = addNonceOverride(_safe, state, _nonce); 61 | return state; 62 | } 63 | 64 | function overrideSafeThresholdAndOwner(address _safe, address _owner) public pure returns (SimulationStateOverride memory) { 65 | SimulationStateOverride memory state = overrideSafeThreshold(_safe); 66 | state = addOwnerOverride(state, _owner); 67 | return state; 68 | } 69 | 70 | function overrideSafeThresholdOwnerAndNonce(address _safe, address _owner, uint256 _nonce) public view returns (SimulationStateOverride memory) { 71 | SimulationStateOverride memory state = overrideSafeThresholdAndOwner(_safe, _owner); 72 | state = addNonceOverride(_safe, state, _nonce); 73 | return state; 74 | } 75 | 76 | function addThresholdOverride(SimulationStateOverride memory _state) internal pure returns (SimulationStateOverride memory) { 77 | // set the threshold (slot 4) to 1 78 | return addOverride(_state, SimulationStorageOverride({ 79 | key: bytes32(uint256(0x4)), 80 | value: bytes32(uint256(0x1)) 81 | })); 82 | } 83 | 84 | function addOwnerOverride(SimulationStateOverride memory _state, address _owner) internal pure returns (SimulationStateOverride memory) { 85 | // set the ownerCount (slot 3) to 1 86 | _state = addOverride(_state, SimulationStorageOverride({ 87 | key: bytes32(uint256(0x3)), 88 | value: bytes32(uint256(0x1)) 89 | })); 90 | // override the owner mapping (slot 2), which requires two key/value pairs: { 0x1: _owner, _owner: 0x1 } 91 | _state = addOverride(_state, SimulationStorageOverride({ 92 | key: bytes32(0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0), // keccak256(1 || 2) 93 | value: bytes32(uint256(uint160(_owner))) 94 | })); 95 | return addOverride(_state, SimulationStorageOverride({ 96 | key: keccak256(abi.encode(_owner, uint256(2))), 97 | value: bytes32(uint256(0x1)) 98 | })); 99 | } 100 | 101 | function addNonceOverride(address _safe, SimulationStateOverride memory _state, uint256 _nonce) internal view returns (SimulationStateOverride memory) { 102 | // get the nonce and check if we need to override it 103 | (, bytes memory nonceBytes) = _safe.staticcall(abi.encodeWithSignature("nonce()")); 104 | uint256 nonce = abi.decode(nonceBytes, (uint256)); 105 | if (nonce == _nonce) return _state; 106 | // set the nonce (slot 5) to the desired value 107 | return addOverride(_state, SimulationStorageOverride({ 108 | key: bytes32(uint256(0x5)), 109 | value: bytes32(_nonce) 110 | })); 111 | } 112 | 113 | function addOverride(SimulationStateOverride memory _state, SimulationStorageOverride memory _override) internal pure returns (SimulationStateOverride memory) { 114 | SimulationStorageOverride[] memory overrides = new SimulationStorageOverride[](_state.overrides.length + 1); 115 | for (uint256 i; i < _state.overrides.length; i++) { 116 | overrides[i] = _state.overrides[i]; 117 | } 118 | overrides[_state.overrides.length] = _override; 119 | return SimulationStateOverride({ 120 | contractAddress: _state.contractAddress, 121 | overrides: overrides 122 | }); 123 | } 124 | 125 | function logSimulationLink(address _to, bytes memory _data, address _from) public view { 126 | logSimulationLink(_to, _data, _from, new SimulationStateOverride[](0)); 127 | } 128 | 129 | function logSimulationLink(address _to, bytes memory _data, address _from, SimulationStateOverride[] memory _overrides) public view { 130 | (, bytes memory projData) = VM_ADDRESS.staticcall( 131 | abi.encodeWithSignature("envOr(string,string)", "TENDERLY_PROJECT", "TENDERLY_PROJECT") 132 | ); 133 | string memory proj = abi.decode(projData, (string)); 134 | 135 | (, bytes memory userData) = VM_ADDRESS.staticcall( 136 | abi.encodeWithSignature("envOr(string,string)", "TENDERLY_USERNAME", "TENDERLY_USERNAME") 137 | ); 138 | string memory username = abi.decode(userData, (string)); 139 | 140 | // the following characters are url encoded: []{} 141 | string memory stateOverrides = "%5B"; 142 | for (uint256 i; i < _overrides.length; i++) { 143 | SimulationStateOverride memory _override = _overrides[i]; 144 | if (i > 0) stateOverrides = string.concat(stateOverrides, ","); 145 | stateOverrides = string.concat( 146 | stateOverrides, 147 | "%7B\"contractAddress\":\"", 148 | vm.toString(_override.contractAddress), 149 | "\",\"storage\":%5B" 150 | ); 151 | for (uint256 j; j < _override.overrides.length; j++) { 152 | if (j > 0) stateOverrides = string.concat(stateOverrides, ","); 153 | stateOverrides = string.concat( 154 | stateOverrides, 155 | "%7B\"key\":\"", 156 | vm.toString(_override.overrides[j].key), 157 | "\",\"value\":\"", 158 | vm.toString(_override.overrides[j].value), 159 | "\"%7D" 160 | ); 161 | } 162 | stateOverrides = string.concat(stateOverrides, "%5D%7D"); 163 | } 164 | stateOverrides = string.concat(stateOverrides, "%5D"); 165 | 166 | string memory str = string.concat( 167 | "https://dashboard.tenderly.co/", 168 | username, 169 | "/", 170 | proj, 171 | "/simulator/new?network=", 172 | vm.toString(block.chainid), 173 | "&contractAddress=", 174 | vm.toString(_to), 175 | "&from=", 176 | vm.toString(_from), 177 | "&stateOverrides=", 178 | stateOverrides 179 | ); 180 | if (bytes(str).length + _data.length * 2 > 7980) { 181 | // tenderly's nginx has issues with long URLs, so print the raw input data separately 182 | str = string.concat(str, "\nInsert the following hex into the 'Raw input data' field:"); 183 | console.log(str); 184 | console.log(vm.toString(_data)); 185 | } else { 186 | str = string.concat(str, "&rawFunctionInput=", vm.toString(_data)); 187 | console.log(str); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /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 Explicity tracking Net Revenue is required to seperate L1FeeVault initiated 65 | * withdrawals from Net Revenue calculations. 66 | */ 67 | uint256 public netFeeRevenue; 68 | 69 | 70 | /*////////////////////////////////////////////////////////////// 71 | Events 72 | //////////////////////////////////////////////////////////////*/ 73 | /** 74 | * @dev Emitted when fees are disbursed. 75 | * @param _disbursementTime The time of the disbursement. 76 | * @param _paidToOptimism The amount of fees disbursed to Optimism. 77 | * @param _totalFeesDisbursed The total amount of fees disbursed. 78 | */ 79 | event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed); 80 | /** 81 | * @dev Emitted when fees are received from FeeVaults. 82 | * @param _sender The FeeVault that sent the fees. 83 | * @param _amount The amount of fees received. 84 | */ 85 | event FeesReceived(address indexed _sender, uint256 _amount); 86 | /** 87 | * @dev Emitted when no fees are collected from FeeVaults at time of disbursement. 88 | */ 89 | event NoFeesCollected(); 90 | 91 | /*////////////////////////////////////////////////////////////// 92 | Constructor 93 | //////////////////////////////////////////////////////////////*/ 94 | /** 95 | * @dev Constructor for the FeeDisburser contract which validates and sets immutable variables. 96 | * @param _optimismWallet The address which receives Optimism's revenue share. 97 | * @param _l1Wallet The L1 address which receives the remainder of the revenue. 98 | * @param _feeDisbursementInterval The minimum amount of time in seconds that must pass between fee disbursals. 99 | */ 100 | constructor( 101 | address payable _optimismWallet, 102 | address _l1Wallet, 103 | uint256 _feeDisbursementInterval 104 | ) { 105 | require(_optimismWallet != address(0), "FeeDisburser: OptimismWallet cannot be address(0)"); 106 | require(_l1Wallet != address(0), "FeeDisburser: L1Wallet cannot be address(0)"); 107 | require(_feeDisbursementInterval >= 24 hours, "FeeDisburser: FeeDisbursementInterval cannot be less than 24 hours"); 108 | 109 | OPTIMISM_WALLET = _optimismWallet; 110 | L1_WALLET = _l1Wallet; 111 | FEE_DISBURSEMENT_INTERVAL = _feeDisbursementInterval; 112 | } 113 | 114 | /*////////////////////////////////////////////////////////////// 115 | External Functions 116 | //////////////////////////////////////////////////////////////*/ 117 | /** 118 | * @dev Withdraws funds from FeeVaults, sends Optimism their revenue share, and withdraws remaining funds to L1. 119 | * @dev Implements revenue share business logic as follows: 120 | * Net Revenue = sequencer FeeVault fee revenue + base FeeVault fee revenue 121 | * Gross Revenue = Net Revenue + l1 FeeVault fee revenue 122 | * Optimism Revenue Share = Maximum of 15% of Net Revenue and 2.5% of Gross Revenue 123 | * L1 Wallet Revenue Share = Gross Revenue - Optimism Revenue Share 124 | */ 125 | function disburseFees() external virtual { 126 | require( 127 | block.timestamp >= lastDisbursementTime + FEE_DISBURSEMENT_INTERVAL, 128 | "FeeDisburser: Disbursement interval not reached" 129 | ); 130 | 131 | // Sequencer and base FeeVaults will withdraw fees to the FeeDisburser contract mutating netFeeRevenue 132 | feeVaultWithdrawal(payable(Predeploys.SEQUENCER_FEE_WALLET)); 133 | feeVaultWithdrawal(payable(Predeploys.BASE_FEE_VAULT)); 134 | 135 | feeVaultWithdrawal(payable(Predeploys.L1_FEE_VAULT)); 136 | 137 | // Gross revenue is the sum of all fees 138 | uint256 feeBalance = address(this).balance; 139 | 140 | // Stop execution if no fees were collected 141 | if (feeBalance == 0) { 142 | emit NoFeesCollected(); 143 | return; 144 | } 145 | 146 | lastDisbursementTime = block.timestamp; 147 | 148 | // Net revenue is the sum of sequencer fees and base fees 149 | uint256 optimismNetRevenueShare = netFeeRevenue * OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS / BASIS_POINT_SCALE; 150 | netFeeRevenue = 0; 151 | 152 | uint256 optimismGrossRevenueShare = feeBalance * OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS / BASIS_POINT_SCALE; 153 | 154 | // Optimism's revenue share is the maximum of net and gross revenue 155 | uint256 optimismRevenueShare = Math.max(optimismNetRevenueShare, optimismGrossRevenueShare); 156 | 157 | // Send Optimism their revenue share on L2 158 | require( 159 | SafeCall.send(OPTIMISM_WALLET, gasleft(), optimismRevenueShare), 160 | "FeeDisburser: Failed to send funds to Optimism" 161 | ); 162 | 163 | // Send remaining funds to L1 wallet on L1 164 | L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{ value: address(this).balance }( 165 | L1_WALLET, 166 | WITHDRAWAL_MIN_GAS, 167 | bytes("") 168 | ); 169 | emit FeesDisbursed(lastDisbursementTime, optimismRevenueShare, feeBalance); 170 | } 171 | 172 | /** 173 | * @dev Receives ETH fees withdrawn from L2 FeeVaults. 174 | * @dev Will revert if ETH is not sent from L2 FeeVaults. 175 | */ 176 | receive() external virtual payable { 177 | if (msg.sender == Predeploys.SEQUENCER_FEE_WALLET || 178 | msg.sender == Predeploys.BASE_FEE_VAULT) { 179 | // Adds value received to net fee revenue if the sender is the sequencer or base FeeVault 180 | netFeeRevenue += msg.value; 181 | } else if (msg.sender != Predeploys.L1_FEE_VAULT) { 182 | revert("FeeDisburser: Only FeeVaults can send ETH to FeeDisburser"); 183 | } 184 | emit FeesReceived(msg.sender, msg.value); 185 | } 186 | 187 | /*////////////////////////////////////////////////////////////// 188 | Internal Functions 189 | //////////////////////////////////////////////////////////////*/ 190 | /** 191 | * @dev Withdraws fees from a FeeVault. 192 | * @param _feeVault The address of the FeeVault to withdraw from. 193 | * @dev Withdrawal will only occur if the given FeeVault's balance is greater than or equal to 194 | the minimum withdrawal amount. 195 | */ 196 | function feeVaultWithdrawal(address payable _feeVault) internal { 197 | require( 198 | FeeVault(_feeVault).WITHDRAWAL_NETWORK() == FeeVault.WithdrawalNetwork.L2, 199 | "FeeDisburser: FeeVault must withdraw to L2" 200 | ); 201 | require( 202 | FeeVault(_feeVault).RECIPIENT() == address(this), 203 | "FeeDisburser: FeeVault must withdraw to FeeDisburser contract" 204 | ); 205 | if (_feeVault.balance >= FeeVault(_feeVault).MIN_WITHDRAWAL_AMOUNT()) { 206 | FeeVault(_feeVault).withdraw(); 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /script/universal/MultisigBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; 6 | import {IGnosisSafe, Enum} from "@eth-optimism-bedrock/scripts/interfaces/IGnosisSafe.sol"; 7 | import {LibSort} from "solady/utils/LibSort.sol"; 8 | import "./Simulator.sol"; 9 | 10 | abstract contract MultisigBase is Simulator { 11 | IMulticall3 internal constant multicall = IMulticall3(MULTICALL3_ADDRESS); 12 | bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5)); 13 | 14 | function _getTransactionHash(address _safe, IMulticall3.Call3[] memory calls) internal view returns (bytes32) { 15 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (calls)); 16 | return _getTransactionHash(_safe, data); 17 | } 18 | 19 | function _getTransactionHash(address _safe, bytes memory _data) internal view returns (bytes32) { 20 | return keccak256(_encodeTransactionData(_safe, _data)); 21 | } 22 | 23 | // Virtual method which can be overwritten 24 | // Default logic here is vestigial for backwards compatibility 25 | // IMPORTANT: this method is used in the sign, simulate, AND execution contexts 26 | // If you override it, ensure that the behavior is correct for all contexts 27 | // As an example, if you are pre-signing a message that needs safe.nonce+1 (before safe.nonce is executed), 28 | // you should explicitly set the nonce value with an env var. 29 | // Overwriting this method with safe.nonce + 1 will cause issues upon execution because the transaction 30 | // hash will differ from the one signed. 31 | function _getNonce(IGnosisSafe safe) internal view virtual returns (uint256 nonce) { 32 | nonce = safe.nonce(); 33 | console.log("Safe current nonce:", nonce); 34 | try vm.envUint("SAFE_NONCE") { 35 | nonce = vm.envUint("SAFE_NONCE"); 36 | console.log("Creating transaction with nonce:", nonce); 37 | } 38 | catch {} 39 | } 40 | 41 | function _encodeTransactionData(address _safe, bytes memory _data) internal view returns (bytes memory) { 42 | // Ensure that the required contracts exist 43 | require(address(multicall).code.length > 0, "multicall3 not deployed"); 44 | require(_safe.code.length > 0, "no code at safe address"); 45 | 46 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 47 | uint256 nonce = _getNonce(safe); 48 | 49 | return safe.encodeTransactionData({ 50 | to: address(multicall), 51 | value: 0, 52 | data: _data, 53 | operation: Enum.Operation.DelegateCall, 54 | safeTxGas: 0, 55 | baseGas: 0, 56 | gasPrice: 0, 57 | gasToken: address(0), 58 | refundReceiver: address(0), 59 | _nonce: nonce 60 | }); 61 | } 62 | 63 | function _printDataToSign(address _safe, IMulticall3.Call3[] memory _calls) internal view { 64 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); 65 | bytes memory txData = _encodeTransactionData(_safe, data); 66 | 67 | console.log("---\nData to sign:"); 68 | console.log("vvvvvvvv"); 69 | console.logBytes(txData); 70 | console.log("^^^^^^^^\n"); 71 | 72 | console.log("########## IMPORTANT ##########"); 73 | console.log("Please make sure that the 'Data to sign' displayed above matches what you see in the simulation and on your hardware wallet."); 74 | console.log("This is a critical step that must not be skipped."); 75 | console.log("###############################"); 76 | } 77 | 78 | function _checkSignatures(address _safe, IMulticall3.Call3[] memory _calls, bytes memory _signatures) 79 | internal 80 | view 81 | { 82 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 83 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); 84 | bytes32 hash = _getTransactionHash(_safe, data); 85 | 86 | uint256 signatureCount = uint256(_signatures.length / 0x41); 87 | uint256 threshold = safe.getThreshold(); 88 | require(signatureCount >= threshold, "not enough signatures"); 89 | 90 | // safe requires signatures to be sorted ascending by public key 91 | _signatures = sortSignatures(_signatures, hash); 92 | 93 | safe.checkSignatures({ 94 | dataHash: hash, 95 | data: data, 96 | signatures: _signatures 97 | }); 98 | } 99 | 100 | function _executeTransaction(address _safe, IMulticall3.Call3[] memory _calls, bytes memory _signatures) 101 | internal 102 | returns (Vm.AccountAccess[] memory, SimulationPayload memory) 103 | { 104 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 105 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); 106 | bytes32 hash = _getTransactionHash(_safe, data); 107 | 108 | uint256 signatureCount = uint256(_signatures.length / 0x41); 109 | uint256 threshold = safe.getThreshold(); 110 | require(signatureCount >= threshold, "not enough signatures"); 111 | 112 | // safe requires signatures to be sorted ascending by public key 113 | _signatures = sortSignatures(_signatures, hash); 114 | 115 | logSimulationLink({ 116 | _to: _safe, 117 | _from: msg.sender, 118 | _data: abi.encodeCall( 119 | safe.execTransaction, 120 | ( 121 | address(multicall), 122 | 0, 123 | data, 124 | Enum.Operation.DelegateCall, 125 | 0, 126 | 0, 127 | 0, 128 | address(0), 129 | payable(address(0)), 130 | _signatures 131 | ) 132 | ) 133 | }); 134 | 135 | vm.startStateDiffRecording(); 136 | bool success = safe.execTransaction({ 137 | to: address(multicall), 138 | value: 0, 139 | data: data, 140 | operation: Enum.Operation.DelegateCall, 141 | safeTxGas: 0, 142 | baseGas: 0, 143 | gasPrice: 0, 144 | gasToken: address(0), 145 | refundReceiver: payable(address(0)), 146 | signatures: _signatures 147 | }); 148 | Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); 149 | require(success, "MultisigBase::_executeTransaction: Transaction failed"); 150 | require(accesses.length > 0, "MultisigBase::_executeTransaction: No state changes"); 151 | 152 | // This can be used to e.g. call out to the Tenderly API and get additional 153 | // data about the state diff before broadcasting the transaction. 154 | SimulationPayload memory simPayload = SimulationPayload({ 155 | from: msg.sender, 156 | to: address(safe), 157 | data: abi.encodeCall(safe.execTransaction, ( 158 | address(multicall), 159 | 0, 160 | data, 161 | Enum.Operation.DelegateCall, 162 | 0, 163 | 0, 164 | 0, 165 | address(0), 166 | payable(address(0)), 167 | _signatures 168 | )), 169 | stateOverrides: new SimulationStateOverride[](0) 170 | }); 171 | return (accesses, simPayload); 172 | } 173 | 174 | function toArray(IMulticall3.Call3 memory call) internal pure returns (IMulticall3.Call3[] memory) { 175 | IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1); 176 | calls[0] = call; 177 | return calls; 178 | } 179 | 180 | function prevalidatedSignatures(address[] memory _addresses) internal pure returns (bytes memory) { 181 | LibSort.sort(_addresses); 182 | bytes memory signatures; 183 | for (uint256 i; i < _addresses.length; i++) { 184 | signatures = bytes.concat(signatures, prevalidatedSignature(_addresses[i])); 185 | } 186 | return signatures; 187 | } 188 | 189 | function prevalidatedSignature(address _address) internal pure returns (bytes memory) { 190 | uint8 v = 1; 191 | bytes32 s = bytes32(0); 192 | bytes32 r = bytes32(uint256(uint160(_address))); 193 | return abi.encodePacked(r, s, v); 194 | } 195 | 196 | function sortSignatures(bytes memory _signatures, bytes32 dataHash) internal pure returns (bytes memory) { 197 | bytes memory sorted; 198 | uint256 count = uint256(_signatures.length / 0x41); 199 | uint256[] memory addressesAndIndexes = new uint256[](count); 200 | uint8 v; 201 | bytes32 r; 202 | bytes32 s; 203 | for (uint256 i; i < count; i++) { 204 | (v, r, s) = signatureSplit(_signatures, i); 205 | address owner; 206 | if (v <= 1) { 207 | owner = address(uint160(uint256(r))); 208 | } else if (v > 30) { 209 | owner = 210 | ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s); 211 | } else { 212 | owner = ecrecover(dataHash, v, r, s); 213 | } 214 | addressesAndIndexes[i] = uint256(uint256(uint160(owner)) << 0x60 | i); // address in first 160 bits, index in second 96 bits 215 | } 216 | LibSort.sort(addressesAndIndexes); 217 | for (uint256 i; i < count; i++) { 218 | uint256 index = addressesAndIndexes[i] & 0xffffffff; 219 | (v, r, s) = signatureSplit(_signatures, index); 220 | sorted = bytes.concat(sorted, abi.encodePacked(r, s, v)); 221 | } 222 | return sorted; 223 | } 224 | 225 | // see https://github.com/safe-global/safe-contracts/blob/1ed486bb148fe40c26be58d1b517cec163980027/contracts/common/SignatureDecoder.sol 226 | function signatureSplit(bytes memory signatures, uint256 pos) 227 | internal 228 | pure 229 | returns (uint8 v, bytes32 r, bytes32 s) 230 | { 231 | assembly { 232 | let signaturePos := mul(0x41, pos) 233 | r := mload(add(signatures, add(signaturePos, 0x20))) 234 | s := mload(add(signatures, add(signaturePos, 0x40))) 235 | v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /script/universal/NestedMultisigBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "./MultisigBase.sol"; 5 | 6 | import { console } from "forge-std/console.sol"; 7 | import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; 8 | 9 | import { IGnosisSafe, Enum } from "@eth-optimism-bedrock/scripts/interfaces/IGnosisSafe.sol"; 10 | 11 | /** 12 | * @title NestedMultisigBuilder 13 | * @notice Modeled from Optimism's SafeBuilder, but built for nested safes (Safes where the signers are other Safes). 14 | */ 15 | abstract contract NestedMultisigBuilder is MultisigBase { 16 | /** 17 | * ----------------------------------------------------------- 18 | * Virtual Functions 19 | * ----------------------------------------------------------- 20 | */ 21 | 22 | /** 23 | * @notice Follow up assertions to ensure that the script ran to completion 24 | */ 25 | function _postCheck(Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) internal virtual; 26 | 27 | /** 28 | * @notice Creates the calldata 29 | */ 30 | function _buildCalls() internal virtual view returns (IMulticall3.Call3[] memory); 31 | 32 | /** 33 | * @notice Returns the nested safe address to execute the final transaction from 34 | */ 35 | function _ownerSafe() internal virtual view returns (address); 36 | 37 | /** 38 | * ----------------------------------------------------------- 39 | * Implemented Functions 40 | * ----------------------------------------------------------- 41 | */ 42 | 43 | /** 44 | * Step 1 45 | * ====== 46 | * Generate a transaction approval data to sign. This method should be called by a threshold 47 | * of members of each of the multisigs involved in the nested multisig. Signers will pass 48 | * their signature to a facilitator, who will execute the approval transaction for each 49 | * multisig (see step 2). 50 | */ 51 | function sign(address _signerSafe) public { 52 | address nestedSafeAddress = _ownerSafe(); 53 | 54 | // Snapshot and restore Safe nonce after simulation, otherwise the data logged to sign 55 | // would not match the actual data we need to sign, because the simulation 56 | // would increment the nonce. 57 | uint256 originalNonce = _getNonce(IGnosisSafe(nestedSafeAddress)); 58 | uint256 originalSignerNonce = _getNonce(IGnosisSafe(_signerSafe)); 59 | 60 | IMulticall3.Call3[] memory nestedCalls = _buildCalls(); 61 | IMulticall3.Call3 memory call = _generateApproveCall(nestedSafeAddress, nestedCalls); 62 | bytes32 hash = _getTransactionHash(_signerSafe, toArray(call)); 63 | 64 | console.log("---\nIf submitting onchain, call Safe.approveHash on %s with the following hash:", _signerSafe); 65 | console.logBytes32(hash); 66 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _simulateForSigner(_signerSafe, nestedSafeAddress, nestedCalls); 67 | _postCheck(accesses, simPayload); 68 | 69 | // Restore the original nonce. 70 | vm.store(nestedSafeAddress, SAFE_NONCE_SLOT, bytes32(uint256(originalNonce))); 71 | vm.store(_signerSafe, SAFE_NONCE_SLOT, bytes32(uint256(originalSignerNonce))); 72 | 73 | _printDataToSign(_signerSafe, toArray(call)); 74 | } 75 | 76 | /** 77 | * Step 2 78 | * ====== 79 | * Execute an approval transaction. This method should be called by a facilitator 80 | * (non-signer), once for each of the multisigs involved in the nested multisig, 81 | * after collecting a threshold of signatures for each multisig (see step 1). 82 | */ 83 | function approve(address _signerSafe, bytes memory _signatures) public { 84 | address nestedSafeAddress = _ownerSafe(); 85 | IMulticall3.Call3[] memory nestedCalls = _buildCalls(); 86 | IMulticall3.Call3 memory call = _generateApproveCall(nestedSafeAddress, nestedCalls); 87 | 88 | address[] memory approvers = _getApprovers(_signerSafe, toArray(call)); 89 | _signatures = bytes.concat(_signatures, prevalidatedSignatures(approvers)); 90 | 91 | vm.startBroadcast(); 92 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _executeTransaction(_signerSafe, toArray(call), _signatures); 93 | vm.stopBroadcast(); 94 | 95 | _postCheck(accesses, simPayload); 96 | } 97 | 98 | /** 99 | * Step 3 100 | * ====== 101 | * Execute the transaction. This method should be called by a facilitator (non-signer), after 102 | * all of the approval transactions have been submitted onchain (see step 2). 103 | */ 104 | function run() public { 105 | address nestedSafeAddress = _ownerSafe(); 106 | IMulticall3.Call3[] memory nestedCalls = _buildCalls(); 107 | address[] memory approvers = _getApprovers(nestedSafeAddress, nestedCalls); 108 | bytes memory signatures = prevalidatedSignatures(approvers); 109 | 110 | vm.startBroadcast(); 111 | (Vm.AccountAccess[] memory accesses, SimulationPayload memory simPayload) = _executeTransaction(nestedSafeAddress, nestedCalls, signatures); 112 | vm.stopBroadcast(); 113 | 114 | _postCheck(accesses, simPayload); 115 | } 116 | 117 | function _generateApproveCall(address _safe, IMulticall3.Call3[] memory _calls) internal view returns (IMulticall3.Call3 memory) { 118 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 119 | bytes32 hash = _getTransactionHash(_safe, _calls); 120 | 121 | console.log("---\nNested hash:"); 122 | console.logBytes32(hash); 123 | 124 | return IMulticall3.Call3({ 125 | target: _safe, 126 | allowFailure: false, 127 | callData: abi.encodeCall(safe.approveHash, (hash)) 128 | }); 129 | } 130 | 131 | function _getApprovers(address _safe, IMulticall3.Call3[] memory _calls) internal view returns (address[] memory) { 132 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 133 | bytes32 hash = _getTransactionHash(_safe, _calls); 134 | 135 | // get a list of owners that have approved this transaction 136 | uint256 threshold = safe.getThreshold(); 137 | address[] memory owners = safe.getOwners(); 138 | address[] memory approvers = new address[](threshold); 139 | uint256 approverIndex; 140 | for (uint256 i; i < owners.length; i++) { 141 | address owner = owners[i]; 142 | uint256 approved = safe.approvedHashes(owner, hash); 143 | if (approved == 1) { 144 | approvers[approverIndex] = owner; 145 | approverIndex++; 146 | if (approverIndex == threshold) { 147 | return approvers; 148 | } 149 | } 150 | } 151 | address[] memory subset = new address[](approverIndex); 152 | for (uint256 i; i < approverIndex; i++) { 153 | subset[i] = approvers[i]; 154 | } 155 | return subset; 156 | } 157 | 158 | function _simulateForSigner(address _signerSafe, address _safe, IMulticall3.Call3[] memory _calls) 159 | internal 160 | returns (Vm.AccountAccess[] memory, SimulationPayload memory) 161 | { 162 | IGnosisSafe safe = IGnosisSafe(payable(_safe)); 163 | IGnosisSafe signerSafe = IGnosisSafe(payable(_signerSafe)); 164 | bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); 165 | bytes32 hash = _getTransactionHash(_safe, data); 166 | IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](2); 167 | 168 | // simulate an approveHash, so that signer can verify the data they are signing 169 | bytes memory approveHashData = abi.encodeCall(IMulticall3.aggregate3, (toArray( 170 | IMulticall3.Call3({ 171 | target: _safe, 172 | allowFailure: false, 173 | callData: abi.encodeCall(safe.approveHash, (hash)) 174 | }) 175 | ))); 176 | bytes memory approveHashExec = abi.encodeCall( 177 | signerSafe.execTransaction, 178 | ( 179 | address(multicall), 180 | 0, 181 | approveHashData, 182 | Enum.Operation.DelegateCall, 183 | 0, 184 | 0, 185 | 0, 186 | address(0), 187 | payable(address(0)), 188 | prevalidatedSignature(address(multicall)) 189 | ) 190 | ); 191 | calls[0] = IMulticall3.Call3({ 192 | target: _signerSafe, 193 | allowFailure: false, 194 | callData: approveHashExec 195 | }); 196 | 197 | // simulate the final state changes tx, so that signer can verify the final results 198 | bytes memory finalExec = abi.encodeCall( 199 | safe.execTransaction, 200 | ( 201 | address(multicall), 202 | 0, 203 | data, 204 | Enum.Operation.DelegateCall, 205 | 0, 206 | 0, 207 | 0, 208 | address(0), 209 | payable(address(0)), 210 | prevalidatedSignature(_signerSafe) 211 | ) 212 | ); 213 | calls[1] = IMulticall3.Call3({ 214 | target: _safe, 215 | allowFailure: false, 216 | callData: finalExec 217 | }); 218 | 219 | SimulationStateOverride[] memory overrides = new SimulationStateOverride[](2); 220 | // The state change simulation sets the multisig threshold to 1 in the 221 | // simulation to enable an approver to see what the final state change 222 | // will look like upon transaction execution. The multisig threshold 223 | // will not actually change in the transaction execution. 224 | overrides[0] = overrideSafeThreshold(_safe); 225 | // Set the signer safe threshold to 1, and set the owner to multicall. 226 | // This is a little hacky; reason is to simulate both the approve hash 227 | // and the final tx in a single Tenderly tx, using multicall. Given an 228 | // EOA cannot DELEGATECALL, multicall needs to own the signer safe. 229 | overrides[1] = overrideSafeThresholdAndOwner(_signerSafe, address(multicall)); 230 | 231 | bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls)); 232 | console.log("---\nSimulation link:"); 233 | logSimulationLink({ 234 | _to: address(multicall), 235 | _data: txData, 236 | _from: msg.sender, 237 | _overrides: overrides 238 | }); 239 | 240 | // Forge simulation of the data logged in the link. If the simulation fails 241 | // we revert to make it explicit that the simulation failed. 242 | SimulationPayload memory simPayload = SimulationPayload({ 243 | to: address(multicall), 244 | data: txData, 245 | from: msg.sender, 246 | stateOverrides: overrides 247 | }); 248 | Vm.AccountAccess[] memory accesses = simulateFromSimPayload(simPayload); 249 | return (accesses, simPayload); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /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 in the past. 85 | /// @param startTimestamp The provided start time of the contract. 86 | /// @param currentTime The current time. 87 | error StartTimeCannotBeInPast(uint256 startTimestamp, uint256 currentTime); 88 | 89 | /// @notice The error is thrown when the start timestamp is greater than the end timestamp. 90 | /// @param startTimestamp The provided start time of the contract. 91 | /// @param endTimestamp The provided end time of the contract. 92 | error StartTimeAfterEndTime(uint256 startTimestamp, uint256 endTimestamp); 93 | 94 | /// @notice The error is thrown when the cliffStart timestamp is less than the start time. 95 | /// @param cliffStartTimestamp The provided start time of the contract. 96 | /// @param startTime The start time 97 | error CliffStartTimeInvalid(uint256 cliffStartTimestamp, uint256 startTime); 98 | 99 | /// @notice The error is thrown when the cliffStart timestamp is greater than the end timestamp. 100 | /// @param cliffStartTimestamp The provided start time of the contract. 101 | /// @param endTimestamp The provided end time of the contract. 102 | error CliffStartTimeAfterEndTime(uint256 cliffStartTimestamp, uint256 endTimestamp); 103 | 104 | /// @notice The error is thrown when the vesting period is zero. 105 | error VestingPeriodIsZeroSeconds(); 106 | 107 | /// @notice The error is thrown when the number of vesting event tokens is zero. 108 | error VestingEventTokensIsZero(); 109 | 110 | /// @notice The error is thrown when vesting period is longer than the contract duration. 111 | /// @param vestingPeriodSeconds The provided vesting period in seconds. 112 | error VestingPeriodExceedsContractDuration(uint256 vestingPeriodSeconds); 113 | 114 | /// @notice The error is thrown when the vesting period does not evenly divide the contract duration. 115 | /// @param vestingPeriodSeconds The provided vesting period in seconds. 116 | /// @param startTimestamp The provided start time of the contract. 117 | /// @param endTimestamp The provided end time of the contract. 118 | error UnevenVestingPeriod(uint256 vestingPeriodSeconds, uint256 startTimestamp, uint256 endTimestamp); 119 | 120 | /// @notice The error is thrown when the contract is terminated, when it should not be. 121 | error ContractIsTerminated(); 122 | 123 | /// @notice The error is thrown when the contract is not terminated, when it should be. 124 | error ContractIsNotTerminated(); 125 | 126 | /// @notice Set initial parameters. 127 | /// @param _benefactor Address which receives tokens back in case of contract termination. 128 | /// @param _beneficiary Address which receives tokens that have vested. 129 | /// @param _benefactorOwner Address which represents the benefactor entity. 130 | /// @param _beneficiaryOwner Address which represents the beneficiary entity. 131 | /// @param _escrowOwner Address which represents both the benefactor and the beneficiary entities. 132 | /// @param _start Timestamp of the start of vesting period (or the cliff, if there is one). 133 | /// @param _end Timestamp of the end of the vesting period. 134 | /// @param _vestingPeriodSeconds Period of time between each vesting event in seconds. 135 | /// @param _initialTokens Number of OP tokens which vest at start time. 136 | /// @param _vestingEventTokens Number of OP tokens which vest upon each vesting event. 137 | constructor( 138 | address _benefactor, 139 | address _beneficiary, 140 | address _benefactorOwner, 141 | address _beneficiaryOwner, 142 | address _escrowOwner, 143 | uint256 _start, 144 | uint256 _cliffStart, 145 | uint256 _end, 146 | uint256 _vestingPeriodSeconds, 147 | uint256 _initialTokens, 148 | uint256 _vestingEventTokens 149 | ) AccessControlDefaultAdminRules(5 days, _escrowOwner) { 150 | if (_benefactor == address(0) || _beneficiary == address(0) || 151 | _beneficiaryOwner == address(0) || _benefactorOwner == address(0)) { 152 | revert AddressIsZeroAddress(); 153 | } 154 | if (_start < block.timestamp) revert StartTimeCannotBeInPast(_start, block.timestamp); 155 | if (_start >= _end) revert StartTimeAfterEndTime(_start, _end); 156 | if (_cliffStart < _start) revert CliffStartTimeInvalid(_cliffStart, _start); 157 | if (_cliffStart >= _end) revert CliffStartTimeAfterEndTime(_cliffStart, _end); 158 | if (_vestingPeriodSeconds == 0) revert VestingPeriodIsZeroSeconds(); 159 | if (_vestingEventTokens == 0) revert VestingEventTokensIsZero(); 160 | if ((_end - _start) < _vestingPeriodSeconds) { 161 | revert VestingPeriodExceedsContractDuration(_vestingPeriodSeconds); 162 | } 163 | if ((_end - _start) % _vestingPeriodSeconds != 0) { 164 | revert UnevenVestingPeriod(_vestingPeriodSeconds, _start, _end); 165 | } 166 | 167 | benefactor = _benefactor; 168 | beneficiary = _beneficiary; 169 | start = _start; 170 | cliffStart = _cliffStart; 171 | end = _end; 172 | vestingPeriod = _vestingPeriodSeconds; 173 | initialTokens = _initialTokens; 174 | vestingEventTokens = _vestingEventTokens; 175 | 176 | _grantRole(BENEFACTOR_OWNER_ROLE, _benefactorOwner); 177 | _grantRole(TERMINATOR_ROLE, _benefactorOwner); 178 | _grantRole(BENEFICIARY_OWNER_ROLE, _beneficiaryOwner); 179 | _grantRole(TERMINATOR_ROLE, _beneficiaryOwner); 180 | } 181 | 182 | /// @notice Terminates the contract if called by address with TERMINATOR_ROLE. 183 | /// @notice Releases any vested token to the beneficiary before terminating. 184 | /// @notice Emits a {ContractTerminated} event. 185 | function terminate() external onlyRole(TERMINATOR_ROLE) { 186 | if (contractTerminated) revert ContractIsTerminated(); 187 | release(); 188 | contractTerminated = true; 189 | emit ContractTerminated(); 190 | } 191 | 192 | /// @notice Resumes the contract on the original vesting schedule. 193 | /// @notice Must be called by address with DEFAULT_ADMIN_ROLE role. 194 | /// @notice Emits a {ContractResumed} event. 195 | function resume() external onlyRole(DEFAULT_ADMIN_ROLE) { 196 | if (!contractTerminated) revert ContractIsNotTerminated(); 197 | contractTerminated = false; 198 | emit ContractResumed(); 199 | } 200 | 201 | /// @notice Allow benefactor owner to update benefactor address. 202 | /// @param _newBenefactor New benefactor address. 203 | /// @notice Emits a {BenefactorUpdated} event. 204 | function updateBenefactor(address _newBenefactor) external onlyRole(BENEFACTOR_OWNER_ROLE) { 205 | if (_newBenefactor == address(0)) revert AddressIsZeroAddress(); 206 | address oldBenefactor = benefactor; 207 | if (oldBenefactor != _newBenefactor) { 208 | benefactor = _newBenefactor; 209 | emit BenefactorUpdated(oldBenefactor, _newBenefactor); 210 | } 211 | } 212 | 213 | /// @notice Allow beneficiary owner to update beneficiary address. 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/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(address indexed _systemAddress, bool indexed _success, uint256 _balanceNeeded, uint256 _balanceSent); 14 | event SentProfit(address indexed _profitWallet, bool indexed _success, uint256 _balanceSent); 15 | event ReceivedFunds(address indexed _sender, uint256 _amount); 16 | 17 | uint256 constant MAX_SYSTEM_ADDRESS_COUNT = 20; 18 | uint256 constant INITIAL_BALANCE_TRACKER_BALANCE = 2_000 ether; 19 | 20 | Proxy balanceTrackerProxy; 21 | BalanceTracker balanceTrackerImplementation; 22 | BalanceTracker balanceTracker; 23 | 24 | address payable l1StandardBridge = payable(address(1000)); 25 | address payable profitWallet = payable(address(1001)); 26 | address payable batchSender = payable(address(1002)); 27 | address payable l2OutputProposer = payable(address(1003)); 28 | uint256 batchSenderTargetBalance = 1_000 ether; 29 | uint256 l2OutputProposerTargetBalance = 100 ether; 30 | address payable[] systemAddresses = [batchSender, l2OutputProposer]; 31 | uint256[] targetBalances = [batchSenderTargetBalance, l2OutputProposerTargetBalance]; 32 | address proxyAdminOwner = address(2048); 33 | 34 | function setUp() public override { 35 | super.setUp(); 36 | 37 | balanceTrackerImplementation = new BalanceTracker( 38 | profitWallet 39 | ); 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( 48 | "BalanceTracker: PROFIT_WALLET cannot be address(0)" 49 | ); 50 | new BalanceTracker( 51 | payable(ZERO_ADDRESS) 52 | ); 53 | } 54 | 55 | 56 | function test_constructor_success() external { 57 | balanceTracker = new BalanceTracker( 58 | profitWallet 59 | ); 60 | 61 | assertEq(balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(), MAX_SYSTEM_ADDRESS_COUNT); 62 | assertEq(balanceTracker.PROFIT_WALLET(), profitWallet); 63 | } 64 | 65 | function test_initializer_fail_systemAddresses_zeroLength() external { 66 | delete systemAddresses; 67 | vm.expectRevert( 68 | "BalanceTracker: systemAddresses cannot have a length of zero" 69 | ); 70 | balanceTracker.initialize( 71 | systemAddresses, 72 | targetBalances 73 | ); 74 | } 75 | 76 | function test_initializer_fail_systemAddresses_greaterThanMaxLength() external { 77 | for (;systemAddresses.length <= balanceTracker.MAX_SYSTEM_ADDRESS_COUNT();) systemAddresses.push(payable(address(0))); 78 | 79 | vm.expectRevert( 80 | "BalanceTracker: systemAddresses cannot have a length greater than 20" 81 | ); 82 | balanceTracker.initialize( 83 | systemAddresses, 84 | targetBalances 85 | ); 86 | } 87 | 88 | function test_initializer_fail_systemAddresses_lengthNotEqualToTargetBalancesLength() external { 89 | systemAddresses.push(payable(address(0))); 90 | 91 | vm.expectRevert( 92 | "BalanceTracker: systemAddresses and targetBalances length must be equal" 93 | ); 94 | balanceTracker.initialize( 95 | systemAddresses, 96 | targetBalances 97 | ); 98 | } 99 | 100 | function test_initializer_fail_systemAddresses_containsZeroAddress() external { 101 | systemAddresses[1] = payable(address(0)); 102 | 103 | vm.expectRevert( 104 | "BalanceTracker: systemAddresses cannot contain address(0)" 105 | ); 106 | balanceTracker.initialize( 107 | systemAddresses, 108 | targetBalances 109 | ); 110 | } 111 | 112 | function test_initializer_fail_targetBalances_containsZero() external { 113 | targetBalances[1] = ZERO_VALUE; 114 | 115 | vm.expectRevert( 116 | "BalanceTracker: targetBalances cannot contain 0 target" 117 | ); 118 | balanceTracker.initialize( 119 | systemAddresses, 120 | targetBalances 121 | ); 122 | } 123 | 124 | function test_initializer_success() external { 125 | balanceTracker.initialize( 126 | systemAddresses, 127 | targetBalances 128 | ); 129 | 130 | assertEq(balanceTracker.systemAddresses(0), systemAddresses[0]); 131 | assertEq(balanceTracker.systemAddresses(1), systemAddresses[1]); 132 | assertEq(balanceTracker.targetBalances(0), targetBalances[0]); 133 | assertEq(balanceTracker.targetBalances(1), targetBalances[1]); 134 | } 135 | 136 | function test_processFees_success_cannotBeReentered() external { 137 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 138 | uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; 139 | address payable reentrancySystemAddress = payable(address(new ReenterProcessFees())); 140 | systemAddresses[0] = reentrancySystemAddress; 141 | balanceTracker.initialize( 142 | systemAddresses, 143 | targetBalances 144 | ); 145 | 146 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 147 | emit ProcessedFunds(reentrancySystemAddress, false, batchSenderTargetBalance, batchSenderTargetBalance); 148 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 149 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 150 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 151 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 152 | 153 | balanceTracker.processFees(); 154 | 155 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 156 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 157 | assertEq(batchSender.balance, ZERO_VALUE); 158 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 159 | } 160 | 161 | function test_processFees_fail_whenNotInitialized() external { 162 | vm.expectRevert( 163 | "BalanceTracker: systemAddresses cannot have a length of zero" 164 | ); 165 | 166 | balanceTracker.processFees(); 167 | } 168 | 169 | function test_processFees_success_continuesWhenSystemAddressReverts() external { 170 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 171 | uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; 172 | balanceTracker.initialize( 173 | systemAddresses, 174 | targetBalances 175 | ); 176 | vm.mockCallRevert( 177 | batchSender, 178 | bytes(""), 179 | abi.encode("revert message") 180 | ); 181 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 182 | emit ProcessedFunds(batchSender, false, batchSenderTargetBalance, batchSenderTargetBalance); 183 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 184 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 185 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 186 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 187 | 188 | balanceTracker.processFees(); 189 | 190 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 191 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 192 | assertEq(batchSender.balance, ZERO_VALUE); 193 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 194 | } 195 | 196 | function test_processFees_success_fundsSystemAddresses() external { 197 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 198 | uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - batchSenderTargetBalance - l2OutputProposerTargetBalance; 199 | balanceTracker.initialize( 200 | systemAddresses, 201 | targetBalances 202 | ); 203 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 204 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, batchSenderTargetBalance); 205 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 206 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); 207 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 208 | emit SentProfit(profitWallet, true, expectedProfitWalletBalance); 209 | 210 | balanceTracker.processFees(); 211 | 212 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 213 | assertEq(profitWallet.balance, expectedProfitWalletBalance); 214 | assertEq(batchSender.balance, batchSenderTargetBalance); 215 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 216 | } 217 | 218 | function test_processFees_success_noFunds() external { 219 | balanceTracker.initialize( 220 | systemAddresses, 221 | targetBalances 222 | ); 223 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 224 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, ZERO_VALUE); 225 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 226 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, ZERO_VALUE); 227 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 228 | emit SentProfit(profitWallet, true, ZERO_VALUE); 229 | 230 | balanceTracker.processFees(); 231 | 232 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 233 | assertEq(profitWallet.balance, ZERO_VALUE); 234 | assertEq(batchSender.balance, ZERO_VALUE); 235 | assertEq(l2OutputProposer.balance, ZERO_VALUE); 236 | } 237 | 238 | function test_processFees_success_partialFunds() external { 239 | uint256 partialBalanceTrackerBalance = INITIAL_BALANCE_TRACKER_BALANCE/3; 240 | vm.deal(address(balanceTracker), partialBalanceTrackerBalance); 241 | balanceTracker.initialize( 242 | systemAddresses, 243 | targetBalances 244 | ); 245 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 246 | emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, partialBalanceTrackerBalance); 247 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 248 | emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, ZERO_VALUE); 249 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 250 | emit SentProfit(profitWallet, true, ZERO_VALUE); 251 | 252 | balanceTracker.processFees(); 253 | 254 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 255 | assertEq(profitWallet.balance, ZERO_VALUE); 256 | assertEq(batchSender.balance, partialBalanceTrackerBalance); 257 | assertEq(l2OutputProposer.balance, ZERO_VALUE); 258 | } 259 | 260 | function test_processFees_success_skipsAddressesAtTargetBalance() external { 261 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 262 | vm.deal(batchSender, batchSenderTargetBalance); 263 | vm.deal(l2OutputProposer, l2OutputProposerTargetBalance); 264 | balanceTracker.initialize( 265 | systemAddresses, 266 | targetBalances 267 | ); 268 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 269 | emit ProcessedFunds(batchSender, false, ZERO_VALUE, ZERO_VALUE); 270 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 271 | emit ProcessedFunds(l2OutputProposer, false, ZERO_VALUE, ZERO_VALUE); 272 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 273 | emit SentProfit(profitWallet, true, INITIAL_BALANCE_TRACKER_BALANCE); 274 | 275 | balanceTracker.processFees(); 276 | 277 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 278 | assertEq(profitWallet.balance, INITIAL_BALANCE_TRACKER_BALANCE); 279 | assertEq(batchSender.balance, batchSenderTargetBalance); 280 | assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); 281 | } 282 | 283 | function test_processFees_success_maximumSystemAddresses() external { 284 | vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); 285 | delete systemAddresses; 286 | delete targetBalances; 287 | for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { 288 | systemAddresses.push(payable(address(uint160(i+100)))); 289 | targetBalances.push(l2OutputProposerTargetBalance); 290 | } 291 | balanceTracker.initialize( 292 | systemAddresses, 293 | targetBalances 294 | ); 295 | 296 | balanceTracker.processFees(); 297 | 298 | assertEq(address(balanceTracker).balance, ZERO_VALUE); 299 | for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { 300 | assertEq(systemAddresses[i].balance, l2OutputProposerTargetBalance); 301 | } 302 | assertEq(profitWallet.balance, ZERO_VALUE); 303 | } 304 | 305 | function test_receive_success() external { 306 | vm.deal(l1StandardBridge, NON_ZERO_VALUE); 307 | 308 | vm.prank(l1StandardBridge); 309 | vm.expectEmit(true, true, true, true, address(balanceTracker)); 310 | emit ReceivedFunds(l1StandardBridge, NON_ZERO_VALUE); 311 | 312 | payable(address(balanceTracker)).call{ value: NON_ZERO_VALUE }(""); 313 | 314 | assertEq(address(balanceTracker).balance, NON_ZERO_VALUE); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /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 = new TransparentUpgradeableProxy(address(feeDisburserImplementation), proxyAdminOwner, NULL_BYTES); 56 | feeDisburser = FeeDisburser(payable(address(feeDisburserProxy))); 57 | 58 | sequencerFeeVault = new SequencerFeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 59 | baseFeeVault = new BaseFeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 60 | l1FeeVault = new L1FeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 61 | 62 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 63 | vm.etch(Predeploys.BASE_FEE_VAULT, address(baseFeeVault).code); 64 | vm.etch(Predeploys.L1_FEE_VAULT, address(l1FeeVault).code); 65 | } 66 | 67 | function test_constructor_fail_optimismWallet_ZeroAddress() external { 68 | vm.expectRevert( 69 | "FeeDisburser: OptimismWallet cannot be address(0)" 70 | ); 71 | new FeeDisburser(payable(address(0)), l1Wallet, feeDisbursementInterval); 72 | } 73 | 74 | function test_constructor_fail_l1Wallet_ZeroAddress() external { 75 | vm.expectRevert( 76 | "FeeDisburser: L1Wallet cannot be address(0)" 77 | ); 78 | new FeeDisburser(optimismWallet, payable(address(0)), feeDisbursementInterval); 79 | } 80 | 81 | function test_constructor_fail_feeDisbursementInterval_lessThan24Hours() external { 82 | vm.expectRevert( 83 | "FeeDisburser: FeeDisbursementInterval cannot be less than 24 hours" 84 | ); 85 | new FeeDisburser(optimismWallet, l1Wallet, 24 hours - 1); 86 | } 87 | 88 | function test_constructor_success() external { 89 | feeDisburserImplementation = new FeeDisburser(optimismWallet, l1Wallet, feeDisbursementInterval); 90 | assertEq(feeDisburserImplementation.OPTIMISM_WALLET(), optimismWallet); 91 | assertEq(feeDisburserImplementation.L1_WALLET(), l1Wallet); 92 | } 93 | 94 | function test_disburseFees_fail_feeDisbursementInterval_Zero() external { 95 | // Setup so that the first disburse fees actually does a disbursal and doesn't return early 96 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, minimumWithdrawalAmount*2); 97 | vm.mockCall(Predeploys.L2_STANDARD_BRIDGE, 98 | abi.encodeWithSignature( 99 | "bridgeETHTo(address,uint256,bytes)", 100 | l1Wallet, 101 | WITHDRAWAL_MIN_GAS, 102 | NULL_BYTES 103 | ), 104 | NULL_BYTES 105 | ); 106 | 107 | feeDisburser.disburseFees(); 108 | vm.expectRevert( 109 | "FeeDisburser: Disbursement interval not reached" 110 | ); 111 | feeDisburser.disburseFees(); 112 | } 113 | 114 | function test_disburseFees_fail_feeVaultWithdrawalToL1() external { 115 | sequencerFeeVault = new SequencerFeeVault(payable(address(feeDisburser)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L1); 116 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 117 | 118 | vm.expectRevert( 119 | "FeeDisburser: FeeVault must withdraw to L2" 120 | ); 121 | feeDisburser.disburseFees(); 122 | } 123 | 124 | function test_disburseFees_fail_feeVaultWithdrawalToAnotherAddress() external { 125 | sequencerFeeVault = new SequencerFeeVault(admin, minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 126 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 127 | 128 | vm.expectRevert( 129 | "FeeDisburser: FeeVault must withdraw to FeeDisburser contract" 130 | ); 131 | feeDisburser.disburseFees(); 132 | } 133 | 134 | function test_disburseFees_fail_sendToOptimismFails() external { 135 | // Define a new feeDisburser for which the OP Wallet always reverts when receiving funds 136 | OptimismWalletRevert optimismWalletRevert = new OptimismWalletRevert(); 137 | FeeDisburser feeDisburser2 = new FeeDisburser(payable(address(optimismWalletRevert)), l1Wallet, feeDisbursementInterval); 138 | 139 | // Have the fee vaults point to the new fee disburser contract 140 | sequencerFeeVault = new SequencerFeeVault(payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 141 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(sequencerFeeVault).code); 142 | baseFeeVault = new BaseFeeVault(payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 143 | vm.etch(Predeploys.BASE_FEE_VAULT, address(baseFeeVault).code); 144 | l1FeeVault = new L1FeeVault(payable(address(feeDisburser2)), minimumWithdrawalAmount, FeeVault.WithdrawalNetwork.L2); 145 | vm.etch(Predeploys.L1_FEE_VAULT, address(l1FeeVault).code); 146 | 147 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, minimumWithdrawalAmount); 148 | 149 | vm.expectRevert( 150 | "FeeDisburser: Failed to send funds to Optimism" 151 | ); 152 | feeDisburser2.disburseFees(); 153 | } 154 | 155 | function test_disburseFees_fail_minimumWithdrawalReversion() external { 156 | FeeVaultRevert feeVaultRevert = new FeeVaultRevert(address(feeDisburser)); 157 | vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(feeVaultRevert).code); 158 | 159 | vm.expectRevert( 160 | "revert message" 161 | ); 162 | feeDisburser.disburseFees(); 163 | } 164 | 165 | function test_disburseFees_fail_withdrawalReversion() external { 166 | vm.mockCall(Predeploys.SEQUENCER_FEE_WALLET, MINIMUM_WITHDRAWAL_AMOUNT_SIGNATURE, abi.encode(ZERO_VALUE)); 167 | 168 | vm.expectRevert( 169 | "FeeVault: withdrawal amount must be greater than minimum withdrawal amount" 170 | ); 171 | feeDisburser.disburseFees(); 172 | } 173 | 174 | function test_disburseFees_success_noFees() external { 175 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 176 | emit NoFeesCollected(); 177 | feeDisburser.disburseFees(); 178 | 179 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, ZERO_VALUE); 180 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, ZERO_VALUE); 181 | } 182 | 183 | function test_disburseFees_success_netRevenueMax() external { 184 | // 15% of minimumWithdrawalAmount * 2 > 2.5 % of minimumWithdrawalAmount * 11 185 | uint256 sequencerFeeVaultBalance = minimumWithdrawalAmount; 186 | uint256 baseFeeVaultBalance = minimumWithdrawalAmount; 187 | uint256 l1FeeVaultBalance = minimumWithdrawalAmount * 9; 188 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 189 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 190 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 191 | 192 | uint256 netFeeVaultBalance = sequencerFeeVaultBalance + baseFeeVaultBalance; 193 | uint256 totalFeeVaultBalance = netFeeVaultBalance + l1FeeVaultBalance; 194 | uint256 expectedOptimismWalletBalance = netFeeVaultBalance * optimismNetRevenueShareBasisPoints / BASIS_POINTS_SCALE; 195 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 196 | 197 | vm.mockCall(Predeploys.L2_STANDARD_BRIDGE, 198 | abi.encodeWithSignature( 199 | "bridgeETHTo(address,uint256,bytes)", 200 | l1Wallet, 201 | WITHDRAWAL_MIN_GAS, 202 | NULL_BYTES 203 | ), 204 | NULL_BYTES 205 | ); 206 | 207 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 208 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 209 | feeDisburser.disburseFees(); 210 | 211 | assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); 212 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 213 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 214 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 215 | } 216 | 217 | function test_disburseFees_success_grossRevenueMax() external { 218 | // 15% of minimumWithdrawalAmount * 2 > 2.5 % of minimumWithdrawalAmount * 13 219 | uint256 sequencerFeeVaultBalance = minimumWithdrawalAmount; 220 | uint256 baseFeeVaultBalance = minimumWithdrawalAmount; 221 | uint256 l1FeeVaultBalance = minimumWithdrawalAmount * 11; 222 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 223 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 224 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 225 | 226 | uint256 totalFeeVaultBalance = sequencerFeeVaultBalance + baseFeeVaultBalance + l1FeeVaultBalance; 227 | uint256 expectedOptimismWalletBalance = totalFeeVaultBalance * optimismGrossRevenueShareBasisPoints / BASIS_POINTS_SCALE; 228 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 229 | 230 | vm.mockCall(Predeploys.L2_STANDARD_BRIDGE, 231 | abi.encodeWithSignature( 232 | "bridgeETHTo(address,uint256,bytes)", 233 | l1Wallet, 234 | WITHDRAWAL_MIN_GAS, 235 | NULL_BYTES 236 | ), 237 | NULL_BYTES 238 | ); 239 | 240 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 241 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 242 | feeDisburser.disburseFees(); 243 | 244 | assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); 245 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 246 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 247 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 248 | } 249 | 250 | function test_fuzz_success_disburseFees( 251 | uint256 sequencerFeeVaultBalance, 252 | uint256 baseFeeVaultBalance, 253 | uint256 l1FeeVaultBalance 254 | ) external { 255 | vm.assume(sequencerFeeVaultBalance < 10**36); 256 | vm.assume(baseFeeVaultBalance < 10**36); 257 | vm.assume(l1FeeVaultBalance < 10**36); 258 | 259 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFeeVaultBalance); 260 | vm.deal(Predeploys.BASE_FEE_VAULT, baseFeeVaultBalance); 261 | vm.deal(Predeploys.L1_FEE_VAULT, l1FeeVaultBalance); 262 | 263 | uint256 netFeeVaultBalance = sequencerFeeVaultBalance >= minimumWithdrawalAmount ? sequencerFeeVaultBalance : 0; 264 | netFeeVaultBalance += baseFeeVaultBalance >= minimumWithdrawalAmount ? baseFeeVaultBalance : 0; 265 | uint256 totalFeeVaultBalance = netFeeVaultBalance + (l1FeeVaultBalance >= minimumWithdrawalAmount ? l1FeeVaultBalance : 0); 266 | 267 | uint256 optimismNetRevenue = netFeeVaultBalance * optimismNetRevenueShareBasisPoints / BASIS_POINTS_SCALE; 268 | uint256 optimismGrossRevenue = totalFeeVaultBalance * optimismGrossRevenueShareBasisPoints / BASIS_POINTS_SCALE; 269 | uint256 expectedOptimismWalletBalance = Math.max(optimismNetRevenue, optimismGrossRevenue); 270 | 271 | uint256 expectedBridgeWithdrawalBalance = totalFeeVaultBalance - expectedOptimismWalletBalance; 272 | 273 | vm.mockCall(Predeploys.L2_STANDARD_BRIDGE, 274 | abi.encodeWithSignature( 275 | "bridgeETHTo(address,uint256,bytes)", 276 | l1Wallet, 277 | WITHDRAWAL_MIN_GAS, 278 | NULL_BYTES 279 | ), 280 | NULL_BYTES 281 | ); 282 | 283 | vm.expectEmit(true, true, true, true, address(feeDisburser)); 284 | if (totalFeeVaultBalance == 0) { 285 | emit NoFeesCollected(); 286 | } else { 287 | emit FeesDisbursed(block.timestamp, expectedOptimismWalletBalance, totalFeeVaultBalance); 288 | } 289 | 290 | feeDisburser.disburseFees(); 291 | 292 | assertEq(feeDisburser.netFeeRevenue(), ZERO_VALUE); 293 | assertEq(feeDisburser.OPTIMISM_WALLET().balance, expectedOptimismWalletBalance); 294 | assertEq(Predeploys.L2_STANDARD_BRIDGE.balance, expectedBridgeWithdrawalBalance); 295 | } 296 | 297 | function test_receive_fail_unauthorizedCaller() external { 298 | vm.expectRevert("FeeDisburser: Only FeeVaults can send ETH to FeeDisburser"); 299 | vm.prank(alice); 300 | payable(address(feeDisburser)).call{ value: NON_ZERO_VALUE }(""); 301 | } 302 | 303 | function test_receive_success() external { 304 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, NON_ZERO_VALUE); 305 | 306 | vm.prank(Predeploys.SEQUENCER_FEE_WALLET); 307 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 308 | 309 | assertEq(feeDisburser.netFeeRevenue(), NON_ZERO_VALUE); 310 | assertEq(address(feeDisburser).balance, NON_ZERO_VALUE); 311 | } 312 | 313 | function test_receive_success_fromMultipleFeeVaults() external { 314 | vm.deal(Predeploys.SEQUENCER_FEE_WALLET, NON_ZERO_VALUE); 315 | vm.deal(Predeploys.BASE_FEE_VAULT, NON_ZERO_VALUE); 316 | vm.deal(Predeploys.L1_FEE_VAULT, NON_ZERO_VALUE); 317 | uint256 expectedNetFeeRevenue = NON_ZERO_VALUE * 2; 318 | uint256 expectedTotalValue = NON_ZERO_VALUE * 3; 319 | 320 | vm.prank(Predeploys.SEQUENCER_FEE_WALLET); 321 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 322 | 323 | vm.prank(Predeploys.BASE_FEE_VAULT); 324 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 325 | 326 | assertEq(feeDisburser.netFeeRevenue(), expectedNetFeeRevenue); 327 | assertEq(address(feeDisburser).balance, expectedNetFeeRevenue); 328 | 329 | vm.prank(Predeploys.L1_FEE_VAULT); 330 | Address.sendValue(payable(address(feeDisburser)), NON_ZERO_VALUE); 331 | 332 | assertEq(feeDisburser.netFeeRevenue(), expectedNetFeeRevenue); 333 | assertEq(address(feeDisburser).balance, expectedTotalValue); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /bindings/FeeDisburser.go: -------------------------------------------------------------------------------- 1 | // Code generated - DO NOT EDIT. 2 | // This file is a generated binding and any manual changes will be lost. 3 | 4 | package bindings 5 | 6 | import ( 7 | "errors" 8 | "math/big" 9 | "strings" 10 | 11 | ethereum "github.com/ethereum/go-ethereum" 12 | "github.com/ethereum/go-ethereum/accounts/abi" 13 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 14 | "github.com/ethereum/go-ethereum/common" 15 | "github.com/ethereum/go-ethereum/core/types" 16 | "github.com/ethereum/go-ethereum/event" 17 | ) 18 | 19 | // Reference imports to suppress errors if they are not otherwise used. 20 | var ( 21 | _ = errors.New 22 | _ = big.NewInt 23 | _ = strings.NewReader 24 | _ = ethereum.NotFound 25 | _ = bind.Bind 26 | _ = common.Big1 27 | _ = types.BloomLookup 28 | _ = event.NewSubscription 29 | _ = abi.ConvertType 30 | ) 31 | 32 | // FeeDisburserMetaData contains all meta data concerning the FeeDisburser contract. 33 | var FeeDisburserMetaData = &bind.MetaData{ 34 | ABI: "[{\"inputs\":[{\"internalType\":\"addresspayable\",\"name\":\"_optimismWallet\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_l1Wallet\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_feeDisbursementInterval\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_disbursementTime\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_paidToOptimism\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_totalFeesDisbursed\",\"type\":\"uint256\"}],\"name\":\"FeesDisbursed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"FeesReceived\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"NoFeesCollected\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"BASIS_POINT_SCALE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"FEE_DISBURSEMENT_INTERVAL\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"L1_WALLET\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPTIMISM_WALLET\",\"outputs\":[{\"internalType\":\"addresspayable\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"WITHDRAWAL_MIN_GAS\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"disburseFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"lastDisbursementTime\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"netFeeRevenue\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", 35 | } 36 | 37 | // FeeDisburserABI is the input ABI used to generate the binding from. 38 | // Deprecated: Use FeeDisburserMetaData.ABI instead. 39 | var FeeDisburserABI = FeeDisburserMetaData.ABI 40 | 41 | // FeeDisburser is an auto generated Go binding around an Ethereum contract. 42 | type FeeDisburser struct { 43 | FeeDisburserCaller // Read-only binding to the contract 44 | FeeDisburserTransactor // Write-only binding to the contract 45 | FeeDisburserFilterer // Log filterer for contract events 46 | } 47 | 48 | // FeeDisburserCaller is an auto generated read-only Go binding around an Ethereum contract. 49 | type FeeDisburserCaller struct { 50 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 51 | } 52 | 53 | // FeeDisburserTransactor is an auto generated write-only Go binding around an Ethereum contract. 54 | type FeeDisburserTransactor struct { 55 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 56 | } 57 | 58 | // FeeDisburserFilterer is an auto generated log filtering Go binding around an Ethereum contract events. 59 | type FeeDisburserFilterer struct { 60 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 61 | } 62 | 63 | // FeeDisburserSession is an auto generated Go binding around an Ethereum contract, 64 | // with pre-set call and transact options. 65 | type FeeDisburserSession struct { 66 | Contract *FeeDisburser // Generic contract binding to set the session for 67 | CallOpts bind.CallOpts // Call options to use throughout this session 68 | TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session 69 | } 70 | 71 | // FeeDisburserCallerSession is an auto generated read-only Go binding around an Ethereum contract, 72 | // with pre-set call options. 73 | type FeeDisburserCallerSession struct { 74 | Contract *FeeDisburserCaller // Generic contract caller binding to set the session for 75 | CallOpts bind.CallOpts // Call options to use throughout this session 76 | } 77 | 78 | // FeeDisburserTransactorSession is an auto generated write-only Go binding around an Ethereum contract, 79 | // with pre-set transact options. 80 | type FeeDisburserTransactorSession struct { 81 | Contract *FeeDisburserTransactor // Generic contract transactor binding to set the session for 82 | TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session 83 | } 84 | 85 | // FeeDisburserRaw is an auto generated low-level Go binding around an Ethereum contract. 86 | type FeeDisburserRaw struct { 87 | Contract *FeeDisburser // Generic contract binding to access the raw methods on 88 | } 89 | 90 | // FeeDisburserCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. 91 | type FeeDisburserCallerRaw struct { 92 | Contract *FeeDisburserCaller // Generic read-only contract binding to access the raw methods on 93 | } 94 | 95 | // FeeDisburserTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. 96 | type FeeDisburserTransactorRaw struct { 97 | Contract *FeeDisburserTransactor // Generic write-only contract binding to access the raw methods on 98 | } 99 | 100 | // NewFeeDisburser creates a new instance of FeeDisburser, bound to a specific deployed contract. 101 | func NewFeeDisburser(address common.Address, backend bind.ContractBackend) (*FeeDisburser, error) { 102 | contract, err := bindFeeDisburser(address, backend, backend, backend) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return &FeeDisburser{FeeDisburserCaller: FeeDisburserCaller{contract: contract}, FeeDisburserTransactor: FeeDisburserTransactor{contract: contract}, FeeDisburserFilterer: FeeDisburserFilterer{contract: contract}}, nil 107 | } 108 | 109 | // NewFeeDisburserCaller creates a new read-only instance of FeeDisburser, bound to a specific deployed contract. 110 | func NewFeeDisburserCaller(address common.Address, caller bind.ContractCaller) (*FeeDisburserCaller, error) { 111 | contract, err := bindFeeDisburser(address, caller, nil, nil) 112 | if err != nil { 113 | return nil, err 114 | } 115 | return &FeeDisburserCaller{contract: contract}, nil 116 | } 117 | 118 | // NewFeeDisburserTransactor creates a new write-only instance of FeeDisburser, bound to a specific deployed contract. 119 | func NewFeeDisburserTransactor(address common.Address, transactor bind.ContractTransactor) (*FeeDisburserTransactor, error) { 120 | contract, err := bindFeeDisburser(address, nil, transactor, nil) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return &FeeDisburserTransactor{contract: contract}, nil 125 | } 126 | 127 | // NewFeeDisburserFilterer creates a new log filterer instance of FeeDisburser, bound to a specific deployed contract. 128 | func NewFeeDisburserFilterer(address common.Address, filterer bind.ContractFilterer) (*FeeDisburserFilterer, error) { 129 | contract, err := bindFeeDisburser(address, nil, nil, filterer) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return &FeeDisburserFilterer{contract: contract}, nil 134 | } 135 | 136 | // bindFeeDisburser binds a generic wrapper to an already deployed contract. 137 | func bindFeeDisburser(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { 138 | parsed, err := FeeDisburserMetaData.GetAbi() 139 | if err != nil { 140 | return nil, err 141 | } 142 | return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil 143 | } 144 | 145 | // Call invokes the (constant) contract method with params as input values and 146 | // sets the output to result. The result type might be a single field for simple 147 | // returns, a slice of interfaces for anonymous returns and a struct for named 148 | // returns. 149 | func (_FeeDisburser *FeeDisburserRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { 150 | return _FeeDisburser.Contract.FeeDisburserCaller.contract.Call(opts, result, method, params...) 151 | } 152 | 153 | // Transfer initiates a plain transaction to move funds to the contract, calling 154 | // its default method if one is available. 155 | func (_FeeDisburser *FeeDisburserRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { 156 | return _FeeDisburser.Contract.FeeDisburserTransactor.contract.Transfer(opts) 157 | } 158 | 159 | // Transact invokes the (paid) contract method with params as input values. 160 | func (_FeeDisburser *FeeDisburserRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { 161 | return _FeeDisburser.Contract.FeeDisburserTransactor.contract.Transact(opts, method, params...) 162 | } 163 | 164 | // Call invokes the (constant) contract method with params as input values and 165 | // sets the output to result. The result type might be a single field for simple 166 | // returns, a slice of interfaces for anonymous returns and a struct for named 167 | // returns. 168 | func (_FeeDisburser *FeeDisburserCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { 169 | return _FeeDisburser.Contract.contract.Call(opts, result, method, params...) 170 | } 171 | 172 | // Transfer initiates a plain transaction to move funds to the contract, calling 173 | // its default method if one is available. 174 | func (_FeeDisburser *FeeDisburserTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { 175 | return _FeeDisburser.Contract.contract.Transfer(opts) 176 | } 177 | 178 | // Transact invokes the (paid) contract method with params as input values. 179 | func (_FeeDisburser *FeeDisburserTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { 180 | return _FeeDisburser.Contract.contract.Transact(opts, method, params...) 181 | } 182 | 183 | // BASISPOINTSCALE is a free data retrieval call binding the contract method 0x5b201d83. 184 | // 185 | // Solidity: function BASIS_POINT_SCALE() view returns(uint32) 186 | func (_FeeDisburser *FeeDisburserCaller) BASISPOINTSCALE(opts *bind.CallOpts) (uint32, error) { 187 | var out []interface{} 188 | err := _FeeDisburser.contract.Call(opts, &out, "BASIS_POINT_SCALE") 189 | 190 | if err != nil { 191 | return *new(uint32), err 192 | } 193 | 194 | out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) 195 | 196 | return out0, err 197 | 198 | } 199 | 200 | // BASISPOINTSCALE is a free data retrieval call binding the contract method 0x5b201d83. 201 | // 202 | // Solidity: function BASIS_POINT_SCALE() view returns(uint32) 203 | func (_FeeDisburser *FeeDisburserSession) BASISPOINTSCALE() (uint32, error) { 204 | return _FeeDisburser.Contract.BASISPOINTSCALE(&_FeeDisburser.CallOpts) 205 | } 206 | 207 | // BASISPOINTSCALE is a free data retrieval call binding the contract method 0x5b201d83. 208 | // 209 | // Solidity: function BASIS_POINT_SCALE() view returns(uint32) 210 | func (_FeeDisburser *FeeDisburserCallerSession) BASISPOINTSCALE() (uint32, error) { 211 | return _FeeDisburser.Contract.BASISPOINTSCALE(&_FeeDisburser.CallOpts) 212 | } 213 | 214 | // FEEDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x54664de5. 215 | // 216 | // Solidity: function FEE_DISBURSEMENT_INTERVAL() view returns(uint256) 217 | func (_FeeDisburser *FeeDisburserCaller) FEEDISBURSEMENTINTERVAL(opts *bind.CallOpts) (*big.Int, error) { 218 | var out []interface{} 219 | err := _FeeDisburser.contract.Call(opts, &out, "FEE_DISBURSEMENT_INTERVAL") 220 | 221 | if err != nil { 222 | return *new(*big.Int), err 223 | } 224 | 225 | out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) 226 | 227 | return out0, err 228 | 229 | } 230 | 231 | // FEEDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x54664de5. 232 | // 233 | // Solidity: function FEE_DISBURSEMENT_INTERVAL() view returns(uint256) 234 | func (_FeeDisburser *FeeDisburserSession) FEEDISBURSEMENTINTERVAL() (*big.Int, error) { 235 | return _FeeDisburser.Contract.FEEDISBURSEMENTINTERVAL(&_FeeDisburser.CallOpts) 236 | } 237 | 238 | // FEEDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x54664de5. 239 | // 240 | // Solidity: function FEE_DISBURSEMENT_INTERVAL() view returns(uint256) 241 | func (_FeeDisburser *FeeDisburserCallerSession) FEEDISBURSEMENTINTERVAL() (*big.Int, error) { 242 | return _FeeDisburser.Contract.FEEDISBURSEMENTINTERVAL(&_FeeDisburser.CallOpts) 243 | } 244 | 245 | // L1WALLET is a free data retrieval call binding the contract method 0x36f1a6e5. 246 | // 247 | // Solidity: function L1_WALLET() view returns(address) 248 | func (_FeeDisburser *FeeDisburserCaller) L1WALLET(opts *bind.CallOpts) (common.Address, error) { 249 | var out []interface{} 250 | err := _FeeDisburser.contract.Call(opts, &out, "L1_WALLET") 251 | 252 | if err != nil { 253 | return *new(common.Address), err 254 | } 255 | 256 | out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) 257 | 258 | return out0, err 259 | 260 | } 261 | 262 | // L1WALLET is a free data retrieval call binding the contract method 0x36f1a6e5. 263 | // 264 | // Solidity: function L1_WALLET() view returns(address) 265 | func (_FeeDisburser *FeeDisburserSession) L1WALLET() (common.Address, error) { 266 | return _FeeDisburser.Contract.L1WALLET(&_FeeDisburser.CallOpts) 267 | } 268 | 269 | // L1WALLET is a free data retrieval call binding the contract method 0x36f1a6e5. 270 | // 271 | // Solidity: function L1_WALLET() view returns(address) 272 | func (_FeeDisburser *FeeDisburserCallerSession) L1WALLET() (common.Address, error) { 273 | return _FeeDisburser.Contract.L1WALLET(&_FeeDisburser.CallOpts) 274 | } 275 | 276 | // OPTIMISMGROSSREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x235d506d. 277 | // 278 | // Solidity: function OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 279 | func (_FeeDisburser *FeeDisburserCaller) OPTIMISMGROSSREVENUESHAREBASISPOINTS(opts *bind.CallOpts) (*big.Int, error) { 280 | var out []interface{} 281 | err := _FeeDisburser.contract.Call(opts, &out, "OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS") 282 | 283 | if err != nil { 284 | return *new(*big.Int), err 285 | } 286 | 287 | out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) 288 | 289 | return out0, err 290 | 291 | } 292 | 293 | // OPTIMISMGROSSREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x235d506d. 294 | // 295 | // Solidity: function OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 296 | func (_FeeDisburser *FeeDisburserSession) OPTIMISMGROSSREVENUESHAREBASISPOINTS() (*big.Int, error) { 297 | return _FeeDisburser.Contract.OPTIMISMGROSSREVENUESHAREBASISPOINTS(&_FeeDisburser.CallOpts) 298 | } 299 | 300 | // OPTIMISMGROSSREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x235d506d. 301 | // 302 | // Solidity: function OPTIMISM_GROSS_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 303 | func (_FeeDisburser *FeeDisburserCallerSession) OPTIMISMGROSSREVENUESHAREBASISPOINTS() (*big.Int, error) { 304 | return _FeeDisburser.Contract.OPTIMISMGROSSREVENUESHAREBASISPOINTS(&_FeeDisburser.CallOpts) 305 | } 306 | 307 | // OPTIMISMNETREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x93819a3f. 308 | // 309 | // Solidity: function OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 310 | func (_FeeDisburser *FeeDisburserCaller) OPTIMISMNETREVENUESHAREBASISPOINTS(opts *bind.CallOpts) (*big.Int, error) { 311 | var out []interface{} 312 | err := _FeeDisburser.contract.Call(opts, &out, "OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS") 313 | 314 | if err != nil { 315 | return *new(*big.Int), err 316 | } 317 | 318 | out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) 319 | 320 | return out0, err 321 | 322 | } 323 | 324 | // OPTIMISMNETREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x93819a3f. 325 | // 326 | // Solidity: function OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 327 | func (_FeeDisburser *FeeDisburserSession) OPTIMISMNETREVENUESHAREBASISPOINTS() (*big.Int, error) { 328 | return _FeeDisburser.Contract.OPTIMISMNETREVENUESHAREBASISPOINTS(&_FeeDisburser.CallOpts) 329 | } 330 | 331 | // OPTIMISMNETREVENUESHAREBASISPOINTS is a free data retrieval call binding the contract method 0x93819a3f. 332 | // 333 | // Solidity: function OPTIMISM_NET_REVENUE_SHARE_BASIS_POINTS() view returns(uint256) 334 | func (_FeeDisburser *FeeDisburserCallerSession) OPTIMISMNETREVENUESHAREBASISPOINTS() (*big.Int, error) { 335 | return _FeeDisburser.Contract.OPTIMISMNETREVENUESHAREBASISPOINTS(&_FeeDisburser.CallOpts) 336 | } 337 | 338 | // OPTIMISMWALLET is a free data retrieval call binding the contract method 0x0c8cd070. 339 | // 340 | // Solidity: function OPTIMISM_WALLET() view returns(address) 341 | func (_FeeDisburser *FeeDisburserCaller) OPTIMISMWALLET(opts *bind.CallOpts) (common.Address, error) { 342 | var out []interface{} 343 | err := _FeeDisburser.contract.Call(opts, &out, "OPTIMISM_WALLET") 344 | 345 | if err != nil { 346 | return *new(common.Address), err 347 | } 348 | 349 | out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) 350 | 351 | return out0, err 352 | 353 | } 354 | 355 | // OPTIMISMWALLET is a free data retrieval call binding the contract method 0x0c8cd070. 356 | // 357 | // Solidity: function OPTIMISM_WALLET() view returns(address) 358 | func (_FeeDisburser *FeeDisburserSession) OPTIMISMWALLET() (common.Address, error) { 359 | return _FeeDisburser.Contract.OPTIMISMWALLET(&_FeeDisburser.CallOpts) 360 | } 361 | 362 | // OPTIMISMWALLET is a free data retrieval call binding the contract method 0x0c8cd070. 363 | // 364 | // Solidity: function OPTIMISM_WALLET() view returns(address) 365 | func (_FeeDisburser *FeeDisburserCallerSession) OPTIMISMWALLET() (common.Address, error) { 366 | return _FeeDisburser.Contract.OPTIMISMWALLET(&_FeeDisburser.CallOpts) 367 | } 368 | 369 | // WITHDRAWALMINGAS is a free data retrieval call binding the contract method 0xad41d09c. 370 | // 371 | // Solidity: function WITHDRAWAL_MIN_GAS() view returns(uint32) 372 | func (_FeeDisburser *FeeDisburserCaller) WITHDRAWALMINGAS(opts *bind.CallOpts) (uint32, error) { 373 | var out []interface{} 374 | err := _FeeDisburser.contract.Call(opts, &out, "WITHDRAWAL_MIN_GAS") 375 | 376 | if err != nil { 377 | return *new(uint32), err 378 | } 379 | 380 | out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) 381 | 382 | return out0, err 383 | 384 | } 385 | 386 | // WITHDRAWALMINGAS is a free data retrieval call binding the contract method 0xad41d09c. 387 | // 388 | // Solidity: function WITHDRAWAL_MIN_GAS() view returns(uint32) 389 | func (_FeeDisburser *FeeDisburserSession) WITHDRAWALMINGAS() (uint32, error) { 390 | return _FeeDisburser.Contract.WITHDRAWALMINGAS(&_FeeDisburser.CallOpts) 391 | } 392 | 393 | // WITHDRAWALMINGAS is a free data retrieval call binding the contract method 0xad41d09c. 394 | // 395 | // Solidity: function WITHDRAWAL_MIN_GAS() view returns(uint32) 396 | func (_FeeDisburser *FeeDisburserCallerSession) WITHDRAWALMINGAS() (uint32, error) { 397 | return _FeeDisburser.Contract.WITHDRAWALMINGAS(&_FeeDisburser.CallOpts) 398 | } 399 | 400 | // LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. 401 | // 402 | // Solidity: function lastDisbursementTime() view returns(uint256) 403 | func (_FeeDisburser *FeeDisburserCaller) LastDisbursementTime(opts *bind.CallOpts) (*big.Int, error) { 404 | var out []interface{} 405 | err := _FeeDisburser.contract.Call(opts, &out, "lastDisbursementTime") 406 | 407 | if err != nil { 408 | return *new(*big.Int), err 409 | } 410 | 411 | out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) 412 | 413 | return out0, err 414 | 415 | } 416 | 417 | // LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. 418 | // 419 | // Solidity: function lastDisbursementTime() view returns(uint256) 420 | func (_FeeDisburser *FeeDisburserSession) LastDisbursementTime() (*big.Int, error) { 421 | return _FeeDisburser.Contract.LastDisbursementTime(&_FeeDisburser.CallOpts) 422 | } 423 | 424 | // LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. 425 | // 426 | // Solidity: function lastDisbursementTime() view returns(uint256) 427 | func (_FeeDisburser *FeeDisburserCallerSession) LastDisbursementTime() (*big.Int, error) { 428 | return _FeeDisburser.Contract.LastDisbursementTime(&_FeeDisburser.CallOpts) 429 | } 430 | 431 | // NetFeeRevenue is a free data retrieval call binding the contract method 0x447eb5ac. 432 | // 433 | // Solidity: function netFeeRevenue() view returns(uint256) 434 | func (_FeeDisburser *FeeDisburserCaller) NetFeeRevenue(opts *bind.CallOpts) (*big.Int, error) { 435 | var out []interface{} 436 | err := _FeeDisburser.contract.Call(opts, &out, "netFeeRevenue") 437 | 438 | if err != nil { 439 | return *new(*big.Int), err 440 | } 441 | 442 | out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) 443 | 444 | return out0, err 445 | 446 | } 447 | 448 | // NetFeeRevenue is a free data retrieval call binding the contract method 0x447eb5ac. 449 | // 450 | // Solidity: function netFeeRevenue() view returns(uint256) 451 | func (_FeeDisburser *FeeDisburserSession) NetFeeRevenue() (*big.Int, error) { 452 | return _FeeDisburser.Contract.NetFeeRevenue(&_FeeDisburser.CallOpts) 453 | } 454 | 455 | // NetFeeRevenue is a free data retrieval call binding the contract method 0x447eb5ac. 456 | // 457 | // Solidity: function netFeeRevenue() view returns(uint256) 458 | func (_FeeDisburser *FeeDisburserCallerSession) NetFeeRevenue() (*big.Int, error) { 459 | return _FeeDisburser.Contract.NetFeeRevenue(&_FeeDisburser.CallOpts) 460 | } 461 | 462 | // DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. 463 | // 464 | // Solidity: function disburseFees() returns() 465 | func (_FeeDisburser *FeeDisburserTransactor) DisburseFees(opts *bind.TransactOpts) (*types.Transaction, error) { 466 | return _FeeDisburser.contract.Transact(opts, "disburseFees") 467 | } 468 | 469 | // DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. 470 | // 471 | // Solidity: function disburseFees() returns() 472 | func (_FeeDisburser *FeeDisburserSession) DisburseFees() (*types.Transaction, error) { 473 | return _FeeDisburser.Contract.DisburseFees(&_FeeDisburser.TransactOpts) 474 | } 475 | 476 | // DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. 477 | // 478 | // Solidity: function disburseFees() returns() 479 | func (_FeeDisburser *FeeDisburserTransactorSession) DisburseFees() (*types.Transaction, error) { 480 | return _FeeDisburser.Contract.DisburseFees(&_FeeDisburser.TransactOpts) 481 | } 482 | 483 | // Receive is a paid mutator transaction binding the contract receive function. 484 | // 485 | // Solidity: receive() payable returns() 486 | func (_FeeDisburser *FeeDisburserTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { 487 | return _FeeDisburser.contract.RawTransact(opts, nil) // calldata is disallowed for receive function 488 | } 489 | 490 | // Receive is a paid mutator transaction binding the contract receive function. 491 | // 492 | // Solidity: receive() payable returns() 493 | func (_FeeDisburser *FeeDisburserSession) Receive() (*types.Transaction, error) { 494 | return _FeeDisburser.Contract.Receive(&_FeeDisburser.TransactOpts) 495 | } 496 | 497 | // Receive is a paid mutator transaction binding the contract receive function. 498 | // 499 | // Solidity: receive() payable returns() 500 | func (_FeeDisburser *FeeDisburserTransactorSession) Receive() (*types.Transaction, error) { 501 | return _FeeDisburser.Contract.Receive(&_FeeDisburser.TransactOpts) 502 | } 503 | 504 | // FeeDisburserFeesDisbursedIterator is returned from FilterFeesDisbursed and is used to iterate over the raw logs and unpacked data for FeesDisbursed events raised by the FeeDisburser contract. 505 | type FeeDisburserFeesDisbursedIterator struct { 506 | Event *FeeDisburserFeesDisbursed // Event containing the contract specifics and raw log 507 | 508 | contract *bind.BoundContract // Generic contract to use for unpacking event data 509 | event string // Event name to use for unpacking event data 510 | 511 | logs chan types.Log // Log channel receiving the found contract events 512 | sub ethereum.Subscription // Subscription for errors, completion and termination 513 | done bool // Whether the subscription completed delivering logs 514 | fail error // Occurred error to stop iteration 515 | } 516 | 517 | // Next advances the iterator to the subsequent event, returning whether there 518 | // are any more events found. In case of a retrieval or parsing error, false is 519 | // returned and Error() can be queried for the exact failure. 520 | func (it *FeeDisburserFeesDisbursedIterator) Next() bool { 521 | // If the iterator failed, stop iterating 522 | if it.fail != nil { 523 | return false 524 | } 525 | // If the iterator completed, deliver directly whatever's available 526 | if it.done { 527 | select { 528 | case log := <-it.logs: 529 | it.Event = new(FeeDisburserFeesDisbursed) 530 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 531 | it.fail = err 532 | return false 533 | } 534 | it.Event.Raw = log 535 | return true 536 | 537 | default: 538 | return false 539 | } 540 | } 541 | // Iterator still in progress, wait for either a data or an error event 542 | select { 543 | case log := <-it.logs: 544 | it.Event = new(FeeDisburserFeesDisbursed) 545 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 546 | it.fail = err 547 | return false 548 | } 549 | it.Event.Raw = log 550 | return true 551 | 552 | case err := <-it.sub.Err(): 553 | it.done = true 554 | it.fail = err 555 | return it.Next() 556 | } 557 | } 558 | 559 | // Error returns any retrieval or parsing error occurred during filtering. 560 | func (it *FeeDisburserFeesDisbursedIterator) Error() error { 561 | return it.fail 562 | } 563 | 564 | // Close terminates the iteration process, releasing any pending underlying 565 | // resources. 566 | func (it *FeeDisburserFeesDisbursedIterator) Close() error { 567 | it.sub.Unsubscribe() 568 | return nil 569 | } 570 | 571 | // FeeDisburserFeesDisbursed represents a FeesDisbursed event raised by the FeeDisburser contract. 572 | type FeeDisburserFeesDisbursed struct { 573 | DisbursementTime *big.Int 574 | PaidToOptimism *big.Int 575 | TotalFeesDisbursed *big.Int 576 | Raw types.Log // Blockchain specific contextual infos 577 | } 578 | 579 | // FilterFeesDisbursed is a free log retrieval operation binding the contract event 0xe155e054cfe69655d6d2f8bbfb856aa8cdf49ecbea6557901533364539caad94. 580 | // 581 | // Solidity: event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed) 582 | func (_FeeDisburser *FeeDisburserFilterer) FilterFeesDisbursed(opts *bind.FilterOpts) (*FeeDisburserFeesDisbursedIterator, error) { 583 | 584 | logs, sub, err := _FeeDisburser.contract.FilterLogs(opts, "FeesDisbursed") 585 | if err != nil { 586 | return nil, err 587 | } 588 | return &FeeDisburserFeesDisbursedIterator{contract: _FeeDisburser.contract, event: "FeesDisbursed", logs: logs, sub: sub}, nil 589 | } 590 | 591 | // WatchFeesDisbursed is a free log subscription operation binding the contract event 0xe155e054cfe69655d6d2f8bbfb856aa8cdf49ecbea6557901533364539caad94. 592 | // 593 | // Solidity: event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed) 594 | func (_FeeDisburser *FeeDisburserFilterer) WatchFeesDisbursed(opts *bind.WatchOpts, sink chan<- *FeeDisburserFeesDisbursed) (event.Subscription, error) { 595 | 596 | logs, sub, err := _FeeDisburser.contract.WatchLogs(opts, "FeesDisbursed") 597 | if err != nil { 598 | return nil, err 599 | } 600 | return event.NewSubscription(func(quit <-chan struct{}) error { 601 | defer sub.Unsubscribe() 602 | for { 603 | select { 604 | case log := <-logs: 605 | // New log arrived, parse the event and forward to the user 606 | event := new(FeeDisburserFeesDisbursed) 607 | if err := _FeeDisburser.contract.UnpackLog(event, "FeesDisbursed", log); err != nil { 608 | return err 609 | } 610 | event.Raw = log 611 | 612 | select { 613 | case sink <- event: 614 | case err := <-sub.Err(): 615 | return err 616 | case <-quit: 617 | return nil 618 | } 619 | case err := <-sub.Err(): 620 | return err 621 | case <-quit: 622 | return nil 623 | } 624 | } 625 | }), nil 626 | } 627 | 628 | // ParseFeesDisbursed is a log parse operation binding the contract event 0xe155e054cfe69655d6d2f8bbfb856aa8cdf49ecbea6557901533364539caad94. 629 | // 630 | // Solidity: event FeesDisbursed(uint256 _disbursementTime, uint256 _paidToOptimism, uint256 _totalFeesDisbursed) 631 | func (_FeeDisburser *FeeDisburserFilterer) ParseFeesDisbursed(log types.Log) (*FeeDisburserFeesDisbursed, error) { 632 | event := new(FeeDisburserFeesDisbursed) 633 | if err := _FeeDisburser.contract.UnpackLog(event, "FeesDisbursed", log); err != nil { 634 | return nil, err 635 | } 636 | event.Raw = log 637 | return event, nil 638 | } 639 | 640 | // FeeDisburserFeesReceivedIterator is returned from FilterFeesReceived and is used to iterate over the raw logs and unpacked data for FeesReceived events raised by the FeeDisburser contract. 641 | type FeeDisburserFeesReceivedIterator struct { 642 | Event *FeeDisburserFeesReceived // Event containing the contract specifics and raw log 643 | 644 | contract *bind.BoundContract // Generic contract to use for unpacking event data 645 | event string // Event name to use for unpacking event data 646 | 647 | logs chan types.Log // Log channel receiving the found contract events 648 | sub ethereum.Subscription // Subscription for errors, completion and termination 649 | done bool // Whether the subscription completed delivering logs 650 | fail error // Occurred error to stop iteration 651 | } 652 | 653 | // Next advances the iterator to the subsequent event, returning whether there 654 | // are any more events found. In case of a retrieval or parsing error, false is 655 | // returned and Error() can be queried for the exact failure. 656 | func (it *FeeDisburserFeesReceivedIterator) Next() bool { 657 | // If the iterator failed, stop iterating 658 | if it.fail != nil { 659 | return false 660 | } 661 | // If the iterator completed, deliver directly whatever's available 662 | if it.done { 663 | select { 664 | case log := <-it.logs: 665 | it.Event = new(FeeDisburserFeesReceived) 666 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 667 | it.fail = err 668 | return false 669 | } 670 | it.Event.Raw = log 671 | return true 672 | 673 | default: 674 | return false 675 | } 676 | } 677 | // Iterator still in progress, wait for either a data or an error event 678 | select { 679 | case log := <-it.logs: 680 | it.Event = new(FeeDisburserFeesReceived) 681 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 682 | it.fail = err 683 | return false 684 | } 685 | it.Event.Raw = log 686 | return true 687 | 688 | case err := <-it.sub.Err(): 689 | it.done = true 690 | it.fail = err 691 | return it.Next() 692 | } 693 | } 694 | 695 | // Error returns any retrieval or parsing error occurred during filtering. 696 | func (it *FeeDisburserFeesReceivedIterator) Error() error { 697 | return it.fail 698 | } 699 | 700 | // Close terminates the iteration process, releasing any pending underlying 701 | // resources. 702 | func (it *FeeDisburserFeesReceivedIterator) Close() error { 703 | it.sub.Unsubscribe() 704 | return nil 705 | } 706 | 707 | // FeeDisburserFeesReceived represents a FeesReceived event raised by the FeeDisburser contract. 708 | type FeeDisburserFeesReceived struct { 709 | Sender common.Address 710 | Amount *big.Int 711 | Raw types.Log // Blockchain specific contextual infos 712 | } 713 | 714 | // FilterFeesReceived is a free log retrieval operation binding the contract event 0x2ccfc58c2cef4ee590b5f16be0548cc54afc12e1c66a67b362b7d640fd16bb2d. 715 | // 716 | // Solidity: event FeesReceived(address indexed _sender, uint256 _amount) 717 | func (_FeeDisburser *FeeDisburserFilterer) FilterFeesReceived(opts *bind.FilterOpts, _sender []common.Address) (*FeeDisburserFeesReceivedIterator, error) { 718 | 719 | var _senderRule []interface{} 720 | for _, _senderItem := range _sender { 721 | _senderRule = append(_senderRule, _senderItem) 722 | } 723 | 724 | logs, sub, err := _FeeDisburser.contract.FilterLogs(opts, "FeesReceived", _senderRule) 725 | if err != nil { 726 | return nil, err 727 | } 728 | return &FeeDisburserFeesReceivedIterator{contract: _FeeDisburser.contract, event: "FeesReceived", logs: logs, sub: sub}, nil 729 | } 730 | 731 | // WatchFeesReceived is a free log subscription operation binding the contract event 0x2ccfc58c2cef4ee590b5f16be0548cc54afc12e1c66a67b362b7d640fd16bb2d. 732 | // 733 | // Solidity: event FeesReceived(address indexed _sender, uint256 _amount) 734 | func (_FeeDisburser *FeeDisburserFilterer) WatchFeesReceived(opts *bind.WatchOpts, sink chan<- *FeeDisburserFeesReceived, _sender []common.Address) (event.Subscription, error) { 735 | 736 | var _senderRule []interface{} 737 | for _, _senderItem := range _sender { 738 | _senderRule = append(_senderRule, _senderItem) 739 | } 740 | 741 | logs, sub, err := _FeeDisburser.contract.WatchLogs(opts, "FeesReceived", _senderRule) 742 | if err != nil { 743 | return nil, err 744 | } 745 | return event.NewSubscription(func(quit <-chan struct{}) error { 746 | defer sub.Unsubscribe() 747 | for { 748 | select { 749 | case log := <-logs: 750 | // New log arrived, parse the event and forward to the user 751 | event := new(FeeDisburserFeesReceived) 752 | if err := _FeeDisburser.contract.UnpackLog(event, "FeesReceived", log); err != nil { 753 | return err 754 | } 755 | event.Raw = log 756 | 757 | select { 758 | case sink <- event: 759 | case err := <-sub.Err(): 760 | return err 761 | case <-quit: 762 | return nil 763 | } 764 | case err := <-sub.Err(): 765 | return err 766 | case <-quit: 767 | return nil 768 | } 769 | } 770 | }), nil 771 | } 772 | 773 | // ParseFeesReceived is a log parse operation binding the contract event 0x2ccfc58c2cef4ee590b5f16be0548cc54afc12e1c66a67b362b7d640fd16bb2d. 774 | // 775 | // Solidity: event FeesReceived(address indexed _sender, uint256 _amount) 776 | func (_FeeDisburser *FeeDisburserFilterer) ParseFeesReceived(log types.Log) (*FeeDisburserFeesReceived, error) { 777 | event := new(FeeDisburserFeesReceived) 778 | if err := _FeeDisburser.contract.UnpackLog(event, "FeesReceived", log); err != nil { 779 | return nil, err 780 | } 781 | event.Raw = log 782 | return event, nil 783 | } 784 | 785 | // FeeDisburserNoFeesCollectedIterator is returned from FilterNoFeesCollected and is used to iterate over the raw logs and unpacked data for NoFeesCollected events raised by the FeeDisburser contract. 786 | type FeeDisburserNoFeesCollectedIterator struct { 787 | Event *FeeDisburserNoFeesCollected // Event containing the contract specifics and raw log 788 | 789 | contract *bind.BoundContract // Generic contract to use for unpacking event data 790 | event string // Event name to use for unpacking event data 791 | 792 | logs chan types.Log // Log channel receiving the found contract events 793 | sub ethereum.Subscription // Subscription for errors, completion and termination 794 | done bool // Whether the subscription completed delivering logs 795 | fail error // Occurred error to stop iteration 796 | } 797 | 798 | // Next advances the iterator to the subsequent event, returning whether there 799 | // are any more events found. In case of a retrieval or parsing error, false is 800 | // returned and Error() can be queried for the exact failure. 801 | func (it *FeeDisburserNoFeesCollectedIterator) Next() bool { 802 | // If the iterator failed, stop iterating 803 | if it.fail != nil { 804 | return false 805 | } 806 | // If the iterator completed, deliver directly whatever's available 807 | if it.done { 808 | select { 809 | case log := <-it.logs: 810 | it.Event = new(FeeDisburserNoFeesCollected) 811 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 812 | it.fail = err 813 | return false 814 | } 815 | it.Event.Raw = log 816 | return true 817 | 818 | default: 819 | return false 820 | } 821 | } 822 | // Iterator still in progress, wait for either a data or an error event 823 | select { 824 | case log := <-it.logs: 825 | it.Event = new(FeeDisburserNoFeesCollected) 826 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 827 | it.fail = err 828 | return false 829 | } 830 | it.Event.Raw = log 831 | return true 832 | 833 | case err := <-it.sub.Err(): 834 | it.done = true 835 | it.fail = err 836 | return it.Next() 837 | } 838 | } 839 | 840 | // Error returns any retrieval or parsing error occurred during filtering. 841 | func (it *FeeDisburserNoFeesCollectedIterator) Error() error { 842 | return it.fail 843 | } 844 | 845 | // Close terminates the iteration process, releasing any pending underlying 846 | // resources. 847 | func (it *FeeDisburserNoFeesCollectedIterator) Close() error { 848 | it.sub.Unsubscribe() 849 | return nil 850 | } 851 | 852 | // FeeDisburserNoFeesCollected represents a NoFeesCollected event raised by the FeeDisburser contract. 853 | type FeeDisburserNoFeesCollected struct { 854 | Raw types.Log // Blockchain specific contextual infos 855 | } 856 | 857 | // FilterNoFeesCollected is a free log retrieval operation binding the contract event 0x8c887b1215d5e6b119c1c1008fe1d0919b4c438301d5a0357362a13fb56f6a40. 858 | // 859 | // Solidity: event NoFeesCollected() 860 | func (_FeeDisburser *FeeDisburserFilterer) FilterNoFeesCollected(opts *bind.FilterOpts) (*FeeDisburserNoFeesCollectedIterator, error) { 861 | 862 | logs, sub, err := _FeeDisburser.contract.FilterLogs(opts, "NoFeesCollected") 863 | if err != nil { 864 | return nil, err 865 | } 866 | return &FeeDisburserNoFeesCollectedIterator{contract: _FeeDisburser.contract, event: "NoFeesCollected", logs: logs, sub: sub}, nil 867 | } 868 | 869 | // WatchNoFeesCollected is a free log subscription operation binding the contract event 0x8c887b1215d5e6b119c1c1008fe1d0919b4c438301d5a0357362a13fb56f6a40. 870 | // 871 | // Solidity: event NoFeesCollected() 872 | func (_FeeDisburser *FeeDisburserFilterer) WatchNoFeesCollected(opts *bind.WatchOpts, sink chan<- *FeeDisburserNoFeesCollected) (event.Subscription, error) { 873 | 874 | logs, sub, err := _FeeDisburser.contract.WatchLogs(opts, "NoFeesCollected") 875 | if err != nil { 876 | return nil, err 877 | } 878 | return event.NewSubscription(func(quit <-chan struct{}) error { 879 | defer sub.Unsubscribe() 880 | for { 881 | select { 882 | case log := <-logs: 883 | // New log arrived, parse the event and forward to the user 884 | event := new(FeeDisburserNoFeesCollected) 885 | if err := _FeeDisburser.contract.UnpackLog(event, "NoFeesCollected", log); err != nil { 886 | return err 887 | } 888 | event.Raw = log 889 | 890 | select { 891 | case sink <- event: 892 | case err := <-sub.Err(): 893 | return err 894 | case <-quit: 895 | return nil 896 | } 897 | case err := <-sub.Err(): 898 | return err 899 | case <-quit: 900 | return nil 901 | } 902 | } 903 | }), nil 904 | } 905 | 906 | // ParseNoFeesCollected is a log parse operation binding the contract event 0x8c887b1215d5e6b119c1c1008fe1d0919b4c438301d5a0357362a13fb56f6a40. 907 | // 908 | // Solidity: event NoFeesCollected() 909 | func (_FeeDisburser *FeeDisburserFilterer) ParseNoFeesCollected(log types.Log) (*FeeDisburserNoFeesCollected, error) { 910 | event := new(FeeDisburserNoFeesCollected) 911 | if err := _FeeDisburser.contract.UnpackLog(event, "NoFeesCollected", log); err != nil { 912 | return nil, err 913 | } 914 | event.Raw = log 915 | return event, nil 916 | } 917 | --------------------------------------------------------------------------------