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 nonReentrant {
_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);
}
}
}
}
115 changes: 19 additions & 96 deletions src/Gasback.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ contract Gasback {
// recipient of the base fee vault, it can be configured to auto-pull
// funds from the base fee vault when it runs out of ETH.
address baseFeeVault;
// The minimum balance of the base fee vault.
uint256 minVaultBalance;
// The amount of ETH accrued by taking a cut from the gas burned.
uint256 accrued;
// The recipient of the accrued ETH.
address accruedRecipient;
// A mapping of addresses authorized to withdraw the accrued ETH.
mapping(address => bool) accuralWithdrawers;
}

/// @dev Returns a pointer to the storage struct.
Expand All @@ -56,11 +48,9 @@ contract Gasback {

constructor() payable {
GasbackStorage storage $ = _getGasbackStorage();
$.gasbackRatioNumerator = 0.8 ether;
$.gasbackRatioNumerator = 0.6 ether;
$.gasbackMaxBaseFee = type(uint256).max;
$.baseFeeVault = 0x4200000000000000000000000000000000000019;
$.minVaultBalance = 0.42 ether;
$.accruedRecipient = 0x4200000000000000000000000000000000000019;
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
Expand All @@ -82,68 +72,6 @@ contract Gasback {
return _getGasbackStorage().baseFeeVault;
}

/// @dev The minimum balance of the base fee vault that allows a pull withdrawal.
function minVaultBalance() public view virtual returns (uint256) {
return _getGasbackStorage().minVaultBalance;
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* ACCURAL FUNCTIONS */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @dev Returns the amount of ETH accrued.
function accrued() public view virtual returns (uint256) {
return _getGasbackStorage().accrued;
}

/// @dev Withdraws from the accrued amount.
function withdrawAccrued(address to, uint256 amount) public virtual returns (bool) {
require(_getGasbackStorage().accuralWithdrawers[msg.sender]);
// Checked math prevents underflow.
_getGasbackStorage().accrued -= amount;
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gas(), to, amount, 0x00, 0x00, 0x00, 0x00)) { revert(0x00, 0x00) }
}
return true;
}

/// @dev Returns whether `addr` is authorized to call `withdrawAccrued`.
function isAuthorizedAccuralWithdrawer(address addr) public view virtual returns (bool) {
return _getGasbackStorage().accuralWithdrawers[addr];
}

/// @dev Set whether `addr` is authorized to call `withdrawAccrued`.
function setAccuralWithdrawer(address addr, bool authorized)
public
onlySystemOrThis
returns (bool)
{
_getGasbackStorage().accuralWithdrawers[addr] = authorized;
return true;
}

/// @dev Withdraws from the accrued amount to the accrued recipient.
function withdrawAccruedToAccruedRecipient(uint256 amount) public virtual returns (bool) {
// Checked math prevents underflow.
_getGasbackStorage().accrued -= amount;

address accruedRecipient = _getGasbackStorage().accruedRecipient;
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gas(), accruedRecipient, amount, 0x00, 0x00, 0x00, 0x00)) {
revert(0x00, 0x00)
}
}
return true;
}

/// @dev Sets the accrued recipient.
function setAccruedRecipient(address value) public onlySystemOrThis returns (bool) {
_getGasbackStorage().accruedRecipient = value;
return true;
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* ADMIN FUNCTIONS */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/
Expand Down Expand Up @@ -176,24 +104,32 @@ contract Gasback {
return true;
}

/// @dev Sets the minimum balance of the base fee vault.
function setMinVaultBalance(uint256 value) public onlySystemOrThis returns (bool) {
_getGasbackStorage().minVaultBalance = value;
return true;
}

/// @dev A noop function.
function noop() public payable returns (bool) {
return true;
}

/// @dev Pulls from the base fee vault and reverts unless this contract has enough ETH after.
function triggerBaseFeeVaultWithdraw(uint256 expectedSelfBalanceAfter) external onlySelf {
(bool success,) =
_getGasbackStorage().baseFeeVault.call(abi.encodeWithSignature("withdraw()"));
require(success);
require(address(this).balance >= expectedSelfBalanceAfter);
}

/// @dev Guards the function such that it can only be called either by
/// the system contract, or by the contract itself (as an EIP-7702 delegated EOA).
modifier onlySystemOrThis() {
require(msg.sender == _SYSTEM_ADDRESS || msg.sender == address(this));
_;
}

/// @dev Guards the function such that it can only be called by the contract itself.
modifier onlySelf() {
require(msg.sender == address(this));
_;
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* GASBACK */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/
Expand All @@ -220,20 +156,12 @@ contract Gasback {

uint256 selfBalance = address(this).balance;
// If the contract has insufficient ETH, try to pull from the base fee vault.
if (ethToGive > selfBalance) {
address vault = $.baseFeeVault;
uint256 minBalance = $.minVaultBalance;
if (ethToGive > selfBalance && block.basefee <= $.gasbackMaxBaseFee) {
/// @solidity memory-safe-assembly
assembly {
if extcodesize(vault) {
// If the vault has sufficient ETH, pull from it.
if gt(balance(vault), add(sub(ethToGive, selfBalance), minBalance)) {
mstore(0x00, 0x3ccfd60b) // `withdraw()`.
pop(call(gas(), vault, 0, 0x1c, 0x04, 0x00, 0x00))
// Return ETH to vault to ensure that it has `minBalance`.
pop(call(gas(), vault, minBalance, 0x00, 0x00, 0x00, 0x00))
}
}
mstore(0x00, 0xc70746b1) // `triggerBaseFeeVaultWithdraw(uint256)`.
mstore(0x20, ethToGive)
pop(call(gas(), address(), 0, 0x1c, 0x24, 0x00, 0x00))
}
}

Expand All @@ -243,11 +171,6 @@ contract Gasback {
ethToGive = 0;
gasToBurn = 0;
}

unchecked {
$.accrued += ethFromGas - ethToGive;
}

/// @solidity memory-safe-assembly
assembly {
if gasToBurn {
Expand Down
Loading
Loading