Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
GasbackTest:testConvertGasback() (gas: 73039)
GasbackTest:testConvertGasback(uint256,uint256) (runs: 256, μ: 434549, ~: 294047)
GasbackTest:testConvertGasbackBaseFeeVault() (gas: 27070)
GasbackTest:testConvertGasbackMaxBaseFee() (gas: 44525)
GasbackTest:testConvertGasbackMinVaultBalance() (gas: 26953)
GasbackTest:testConvertGasbackWithAccruedToAccruedRecipient() (gas: 69305)
GasbackTest:test__codesize() (gas: 9846)
FeeVaultSplitterTest:testFuzz_balances_after_multiple_payments(uint8,uint256[9]) (runs: 256, μ: 4605383, ~: 2803798)
FeeVaultSplitterTest:testFuzz_balances_after_payment(uint8,uint256) (runs: 256, μ: 2752114, ~: 1760765)
FeeVaultSplitterTest:test__codesize() (gas: 30291)
FeeVaultSplitterTest:test_balances_after_payment() (gas: 261991)
FeeVaultSplitterTest:test_distribute_clamps_end_to_payees_length() (gas: 245124)
FeeVaultSplitterTest:test_distribute_invariants_with_failed_payee() (gas: 1148434)
FeeVaultSplitterTest:test_distribute_noop_start_gte_end() (gas: 25396)
FeeVaultSplitterTest:test_externalPayees_length_matches_payee_count() (gas: 1034697)
FeeVaultSplitterTest:test_failed_payee_accounting_invariants() (gas: 1156750)
FeeVaultSplitterTest:test_multiple_payments_accounting_is_cumulative() (gas: 308742)
FeeVaultSplitterTest:test_read_public_variables() (gas: 59354)
FeeVaultSplitterTest:test_receive_allows_small_payment() (gas: 64848)
FeeVaultSplitterTest:test_receive_emits_payment_received() (gas: 1251992)
FeeVaultSplitterTest:test_receive_reverts_on_reentrant_payee() (gas: 1285807)
FeeVaultSplitterTest:test_receive_skips_failed_payee_emits_failure() (gas: 1152566)
FeeVaultSplitterTest:test_release_after_dust_payment() (gas: 131877)
FeeVaultSplitterTest:test_revert_deploy_duplicate_payee() (gas: 181846)
FeeVaultSplitterTest:test_revert_deploy_empty_payees() (gas: 39046)
FeeVaultSplitterTest:test_revert_deploy_length_mismatch_more_payees() (gas: 46640)
FeeVaultSplitterTest:test_revert_deploy_length_mismatch_more_shares() (gas: 43716)
FeeVaultSplitterTest:test_revert_deploy_zero_address_payee() (gas: 131946)
FeeVaultSplitterTest:test_revert_deploy_zero_shares() (gas: 134065)
FeeVaultSplitterTest:test_revert_release_account_has_no_shares() (gas: 11672)
FeeVaultSplitterTest:test_revert_release_account_not_due_payment() (gas: 20633)
FeeVaultSplitterTest:test_revert_release_failed_to_send_value() (gas: 1043983)
FeeVaultSplitterTest:test_revert_release_insufficient_balance() (gas: 36608)
GasbackTest:testConvertGasback() (gas: 56997)
GasbackTest:testConvertGasback(uint256,uint256) (runs: 256, μ: 414710, ~: 262761)
GasbackTest:testConvertGasbackBaseFeeVault() (gas: 29386)
GasbackTest:testConvertGasbackMaxBaseFee() (gas: 24715)
GasbackTest:testSetBaseFeeVaultShareNumeratorRevertsWhenValueAboveDenominator() (gas: 13001)
GasbackTest:testSetBaseFeeVaultShareNumeratorRevertsWhenValueBelowGasbackRatio() (gas: 13602)
GasbackTest:testSetGasbackRatioNumeratorRevertsWhenValueAboveBaseFeeVaultShare() (gas: 13418)
GasbackTest:testSetGasbackRatioNumeratorRevertsWhenValueAboveDenominator() (gas: 9778)
GasbackTest:testWithdrawAccruedRevertsWhenCallerUnauthorized() (gas: 84792)
GasbackTest:testWithdrawLeavesAccruedWhenBufferCovers() (gas: 117411)
GasbackTest:testWithdrawReconcilesAccruedDownToBalance() (gas: 117693)
GasbackTest:test__codesize() (gas: 14273)
SoladyTest:test__codesize() (gas: 4099)
TestPlus:test__codesize() (gas: 393)
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ wake-coverage.cov
create2

# Coverage report
report
report

dependencies/
5 changes: 3 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# The Default Profile
[profile.default]
solc_version = "0.8.30"
solc_version = "0.8.34"
evm_version = "prague"
auto_detect_solc = false
optimizer = true
Expand All @@ -15,7 +15,8 @@ always_use_create_2_factory = true
remappings = [
"murky=lib/murky",
"dn404/=lib/dn404/src",
"solady=lib/solady/src"
"solady=lib/solady/src",
"@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-4.9.5/"
]

[fmt]
Expand Down
6 changes: 6 additions & 0 deletions soldeer.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[[dependencies]]
name = "@openzeppelin-contracts"
version = "4.9.5"
url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/4_9_5_22-01-2024_13:13:56_contracts.zip"
checksum = "23d102f257e57f95e4a4a55f981f8f7781f3a68c36fa77e80640812480334b27"
integrity = "27c0919f5274f868b39a294a81d73dd061ef2518d08148a454bc16095088380e"
2 changes: 2 additions & 0 deletions soldeer.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[dependencies]
"@openzeppelin-contracts" = "4.9.5"
92 changes: 92 additions & 0 deletions src/FeeVaultSplitter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

import {PaymentSplitter} from "@openzeppelin/contracts/finance/PaymentSplitter.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title FeeVaultSplitter
* @dev This contract, implements OpenZeppelin's PaymentSplitter, supports splitting Ether payments among a group of accounts.
*
* FeeVaultSplitter follows a _push payment_ model. Incoming Ether triggers an attempt to release funds to all payees.
*/
contract FeeVaultSplitter is PaymentSplitter, ReentrancyGuard {
event PaymentFailed(address to, uint256 amount, bytes reason);

address[] public externalPayees;

/**
* @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at
* the matching position in the `shares` array.
*
* All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no
* duplicates in `payees`.
*/
constructor(address[] memory payees_, uint256[] memory shares_)
payable
PaymentSplitter(payees_, shares_)
{
for (uint256 i = 0; i < payees_.length; i++) {
externalPayees.push(payees_[i]);
}
}

/**
* @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully
* reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the
* reliability of the events, and not the actual splitting of Ether.
*
* To learn more about this see the Solidity documentation for
* https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback
* functions].
*
* SECURITY / DoS NOTE (push-payment model): this function attempts to release to every payee in
* `externalPayees` in a single call. Its gas cost therefore scales with the payee count, and a payee whose
* `receive`/fallback consumes a large amount of gas (rather than cheaply reverting, which is caught and skipped)
* can push this call out of gas and make it revert. Because the OP base fee vault's `withdraw()` sends fees to
* this contract (triggering `receive`), such a revert would block that withdrawal and strand base fees in the
* vault until resolved. To bound this risk: keep the payee set small and trusted (it is fixed at deployment).
* If a deposit's auto-distribution is ever blocked, funds are not lost — anyone can call {distribute} with a
* bounded `[start, end)` slice to release payees in chunks and recover.
*/
receive() external payable override(PaymentSplitter) nonReentrant {
emit PaymentReceived(_msgSender(), msg.value);

_distribute(0, externalPayees.length);
}

/**
* @dev Attempts to release payments for a slice of payees, skipping zero-due payees and emitting failures instead of
* reverting on send failures.
*/
function distribute(uint256 start, uint256 end) public {
_distribute(start, end);
}

/**
* @dev Attempt to pay a slice of payees without reverting the whole call.
* Skips zero-due accounts and emits failures for accounts that revert on receive.
*/
function _distribute(uint256 start, uint256 end) private {
uint256 payeesLength = externalPayees.length;
if (end > payeesLength) {
end = payeesLength;
}
if (start >= end) {
return;
}

for (uint256 i = start; i < end; i++) {
address payable account = payable(externalPayees[i]);
uint256 payment = releasable(account);
if (payment == 0) {
continue;
}

try this.release(account) {}
catch (bytes memory reason) {
emit PaymentFailed(account, payment, reason);
}
}
}
}
Loading
Loading