Contract Name:
FeeSplitter
Contract Source Code:
<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
interface IAshStaking {
function notifyReward() external payable;
}
/// @title FeeSplitter (Hardened)
/// @notice Receives ETH trading fees from AshHook and splits them:
/// 50% to treasury (pull-payment pattern)
/// 50% to AshStaking (distributed to ASH stakers)
///
/// Set as the hook's treasury address via hook.setTreasury(thisAddress).
/// Auto-splits on receive. Manual split() and retrySplit() available.
/// Treasury uses pull-payment to eliminate reentrancy risk.
contract FeeSplitter is Ownable, Pausable {
IAshStaking public stakingContract;
address public treasury;
uint256 public ownerBps = 5000; // 50%
uint256 public stakerBps = 5000; // 50%
uint256 public constant BASIS = 10000;
// Pull-payment: treasury ETH accumulates here
uint256 public treasuryBalance;
// Failure tracking
uint256 public failureCount;
uint256 public lastFailureTimestamp;
bytes public lastFailureReason;
event Split(uint256 toTreasury, uint256 toStakers);
event TreasuryWithdrawn(address indexed to, uint256 amount);
event StakingContractUpdated(address indexed oldStaking, address indexed newStaking);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
event SplitRatioUpdated(uint256 ownerBps, uint256 stakerBps);
event AutoSplitFailed(uint256 amount, bytes reason);
event StakerRewardFailed(uint256 amount, bytes reason);
error InvalidSplit();
error NothingToSplit();
error TransferFailed();
error NothingToWithdraw();
constructor(address _treasury, address _stakingContract) Ownable(msg.sender) {
require(_treasury != address(0), "Invalid treasury");
require(_stakingContract != address(0), "Invalid staking");
treasury = _treasury;
stakingContract = IAshStaking(_stakingContract);
}
/// @notice Distribute all pending ETH according to the split ratio.
/// Treasury portion goes to pull-payment balance.
/// Staker portion sent to AshStaking via notifyReward().
function split() public whenNotPaused {
// Calculate splittable balance (exclude treasury's pending withdrawal)
uint256 balance = address(this).balance - treasuryBalance;
if (balance == 0) revert NothingToSplit();
uint256 toTreasury = (balance * ownerBps) / BASIS;
uint256 toStakers = balance - toTreasury;
// Pull-payment: accumulate treasury portion
if (toTreasury > 0) {
treasuryBalance += toTreasury;
}
// Push staker rewards to staking contract
if (toStakers > 0) {
try stakingContract.notifyReward{value: toStakers}() {
// success
} catch (bytes memory reason) {
// If staking contract reverts (e.g., MinimumRewardRequired),
// buffer the staker portion in treasury for now
treasuryBalance += toStakers;
failureCount++;
lastFailureTimestamp = block.timestamp;
lastFailureReason = reason;
emit StakerRewardFailed(toStakers, reason);
}
}
emit Split(toTreasury, toStakers);
}
/// @notice Treasury withdraws accumulated ETH (pull-payment).
function treasuryWithdraw() external {
if (treasuryBalance == 0) revert NothingToWithdraw();
uint256 amount = treasuryBalance;
treasuryBalance = 0;
(bool ok,) = treasury.call{value: amount}("");
if (!ok) revert TransferFailed();
emit TreasuryWithdrawn(treasury, amount);
}
/// @notice Retry a failed split (admin recovery).
function retrySplit() external onlyOwner {
split();
}
/// @notice Accept ETH from hook. Does NOT auto-split because the hook
/// limits gas to 50k which isn't enough for split().
/// Call split() separately (anyone can call it).
receive() external payable {}
// ── Views ──
function getSplitStatus() external view returns (bool failed, uint256 timestamp, uint256 attempts) {
failed = failureCount > 0;
timestamp = lastFailureTimestamp;
attempts = failureCount;
}
function pendingSplit() external view returns (uint256) {
return address(this).balance - treasuryBalance;
}
// ── Admin ──
function setStakingContract(address _staking) external onlyOwner {
require(_staking != address(0), "Invalid address");
emit StakingContractUpdated(address(stakingContract), _staking);
stakingContract = IAshStaking(_staking);
}
function setTreasury(address _treasury) external onlyOwner {
require(_treasury != address(0), "Invalid address");
emit TreasuryUpdated(treasury, _treasury);
treasury = _treasury;
}
function setSplitRatio(uint256 _ownerBps, uint256 _stakerBps) external onlyOwner {
if (_ownerBps + _stakerBps != BASIS) revert InvalidSplit();
ownerBps = _ownerBps;
stakerBps = _stakerBps;
emit SplitRatioUpdated(_ownerBps, _stakerBps);
}
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (utils/Pausable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account.
*
* This module is used through inheritance. It will make available the
* modifiers `whenNotPaused` and `whenPaused`, which can be applied to
* the functions of your contract. Note that they will not be pausable by
* simply including this module, only once the modifiers are put in place.
*/
abstract contract Pausable is Context {
bool private _paused;
/**
* @dev Emitted when the pause is triggered by `account`.
*/
event Paused(address account);
/**
* @dev Emitted when the pause is lifted by `account`.
*/
event Unpaused(address account);
/**
* @dev The operation failed because the contract is paused.
*/
error EnforcedPause();
/**
* @dev The operation failed because the contract is not paused.
*/
error ExpectedPause();
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*
* Requirements:
*
* - The contract must not be paused.
*/
modifier whenNotPaused() {
_requireNotPaused();
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*
* Requirements:
*
* - The contract must be paused.
*/
modifier whenPaused() {
_requirePaused();
_;
}
/**
* @dev Returns true if the contract is paused, and false otherwise.
*/
function paused() public view virtual returns (bool) {
return _paused;
}
/**
* @dev Throws if the contract is paused.
*/
function _requireNotPaused() internal view virtual {
if (paused()) {
revert EnforcedPause();
}
}
/**
* @dev Throws if the contract is not paused.
*/
function _requirePaused() internal view virtual {
if (!paused()) {
revert ExpectedPause();
}
}
/**
* @dev Triggers stopped state.
*
* Requirements:
*
* - The contract must not be paused.
*/
function _pause() internal virtual whenNotPaused {
_paused = true;
emit Paused(_msgSender());
}
/**
* @dev Returns to normal state.
*
* Requirements:
*
* - The contract must be paused.
*/
function _unpause() internal virtual whenPaused {
_paused = false;
emit Unpaused(_msgSender());
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}