LivoMasterFeeHandler

0x7766e3a6a8c98a76308cfb4040e330c3308f7c73

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
8,052 bytes
ABI entries
32
6 read · 7 write
License
none

Contract information

Address
0x7766e3a6a8c98a76308cfb4040e330c3308f7c73
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x894a3072f0…b1919c2c7d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (6)

getClaimable(address[], address)uint256[]
getDirectReceivers(address)address[]
getRecipients(address)address[], uint256[]
isDirectReceiver(address, address)bool
owner()address
pendingOwner()address

Events (7)

CreatorClaimedCreatorFeesDepositedDirectReceiverRegisteredDirectReceiverRemovedOwnershipTransferStartedOwnershipTransferredSharesUpdated

ABI

[
  {
    "inputs": [],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyRegistered",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFeeShares",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShares",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotRegistered",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooManyDirectReceivers",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooManyFeeReceivers",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeesDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "DirectReceiverRegistered",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "DirectReceiverRemoved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      },
      {
        "indexed": false,
        "internalType": "uint256[]",
        "name": "sharesBps",
        "type": "uint256[]"
      }
    ],
    "name": "SharesUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "tokens",
        "type": "address[]"
      }
    ],
    "name": "claim",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "depositFees",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "tokens",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "getClaimable",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getDirectReceivers",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getRecipients",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "isDirectReceiver",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "account",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "shares",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "directFeesEnabled",
            "type": "bool"
          }
        ],
        "internalType": "struct ILivoFactory.FeeShare[]",
        "name": "feeShares",
        "type": "tuple[]"
      }
    ],
    "name": "registerToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "account",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "shares",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "directFeesEnabled",
            "type": "bool"
          }
        ],
        "internalType": "struct ILivoFactory.FeeShare[]",
        "name": "feeShares",
        "type": "tuple[]"
      }
    ],
    "name": "setShares",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {ILivoMasterFeeHandler} from "src/interfaces/ILivoMasterFeeHandler.sol";
import {ILivoFactory} from "src/interfaces/ILivoFactory.sol";
import {ILivoToken} from "src/interfaces/ILivoToken.sol";
import {TokenFeeConfigLib} from "src/libraries/TokenFeeConfigLib.sol";
import {Ownable, Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol";
import {ReentrancyGuardTransient} from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";

/// @title LivoMasterFeeHandler
/// @notice Unified singleton fee handler for all Livo tokens. Supports single and multi-receiver
///         configs with optional synchronous ETH forwarding (direct fees) per receiver.
///
///         All ETH enters through `depositFees` — there is no `receive()` fallback and no excess
///         ETH can accumulate. Every wei is attributed to a specific token at arrival.
///
///         Two recipient classes coexist:
///           - **direct** recipients have their slice forwarded synchronously on every `depositFees`,
///             with the `.call` gas-capped at `DIRECT_FORWARD_GAS` to bound griefing on swappers.
///             Failed forwards (including out-of-gas inside the receiver) fall back to per-account
///             pending claims so a hostile receiver cannot DoS swap or graduation hot paths.
///           - **claimable** recipients accumulate ETH via a per-token cumulative accumulator
///             (`ethPerBps`) that scales O(1) per deposit regardless of recipient count.
///
///         The direct-receiver set is mutable via `setShares` (admin or token-owner gated).
contract LivoMasterFeeHandler is ILivoMasterFeeHandler, Ownable2Step, ReentrancyGuardTransient {
    using TokenFeeConfigLib for TokenFeeConfigLib.Config;

    uint256 internal constant BPS_TOTAL = 10_000;
    uint256 internal constant PRECISION = 1e18;
    /// @notice Hard cap on direct receivers per token, enforced on every `_setSharesInternal` call.
    ///         Bounds per-deposit gas in `_depositSplit` (one external `.call` per direct receiver).
    uint256 internal constant MAX_DIRECT_RECEIVERS = 4;
    /// @notice Hard cap on total fee receivers per token (direct + claimable). Bounds the O(n²)
    ///         duplicate-check in `_requireNoDuplicates` and the linear loops in `_setSharesInternal`.
    uint256 internal constant MAX_FEE_RECEIVERS = 32;
    /// @notice Gas forwarded to a direct receiver's `.call` in `_depositSingle` / `_depositSplit`.
    ///         Caps griefing on swappers if a malicious receiver burns gas on `receive`. Failures
    ///         (including out-of-gas inside the receiver) fall back to per-account pending claims,
    ///         so legitimate receivers needing more gas can still recover via `claim`.
    uint256 internal constant DIRECT_FORWARD_GAS = 100_000;

    mapping(address token => TokenFeeConfigLib.Config) internal _configs;

    /// @notice BPS share for every recipient (direct and claimable alike). `isDirectReceiver`
    ///         distinguishes the two classes.
    mapping(address token => mapping(address account => uint256)) internal _sharesBpsOf;

    /// @notice True iff `account` is currently a direct receiver for `token`.
    mapping(address token => mapping(address account => bool)) public isDirectReceiver;

    /// @notice Snapshot of `_configs[token].ethPerBps` at the time of the account's last claim
    ///         or share update. Used in the claimable accumulator formula.
    mapping(address token => mapping(address account => uint256)) internal _claimedPerBps;

    /// @notice Residual claimable ETH for an account: carried over from share updates
    ///         (claimable recipients) or from failed direct forwards (direct recipients).
    mapping(address token => mapping(address account => uint256)) internal _pendingClaims;

    constructor() Ownable(msg.sender) {}

    ////////////////////////////// EXTERNAL FUNCTIONS ///////////////////////////////////

    /// @notice Deposits ETH fees for `token`. For direct receivers the slice is forwarded
    ///         synchronously; for claimable recipients the accumulator is advanced.
    /// @dev `CreatorFeesDeposited` is emitted before any forward attempt for non-zero deposits;
    ///      zero-value calls are no-ops and emit nothing. There is intentionally no explicit
    ///      registration check on this swap-hot path: registered configs always contain at least one
    ///      recipient, while an unregistered positive-value deposit reaches `_depositSingle` and
    ///      reverts with Solidity's array-out-of-bounds panic when reading `claimableRecipients[0]`.
    ///      The transient `nonReentrant` guard is shared with `setShares` and `claim` — any nested
    ///      call from a direct-receiver hook into those functions reverts, which prevents iteration
    ///      corruption in `_depositSplit`.
    function depositFees(address token) external payable nonReentrant {
        TokenFeeConfigLib.Config storage cfg = _configs[token];

        if (msg.value == 0) return;
        emit CreatorFeesDeposited(token, msg.value);

        if (!cfg.isSplit) {
            _depositSingle(token, cfg);
        } else {
            _depositSplit(token, cfg);
        }
    }

    /// @notice One-shot registration of fee-receiver config for a freshly-deployed token.
    ///         Callable only by the token itself. Factories should call the token's `registerFees`,
    ///         which then self-registers here.
    function registerToken(ILivoFactory.FeeShare[] calldata feeShares) external {
        address token = msg.sender;
        require(token.code.length > 0, Unauthorized());
        require(ILivoToken(token).feeHandler() == address(this), Unauthorized());

        TokenFeeConfigLib.Config storage cfg = _configs[token];
        require(!cfg.isRegistered(), AlreadyRegistered());
        _setSharesInternal(token, cfg, feeShares, false);
    }

    /// @notice Replaces the fee-receiver config for `token`. Callable only by the admin or the
    ///         token's current non-zero owner. V2 tokens and V4 tokens with `renounceOwnership = true`
    ///         have `owner() == address(0)`, so only the admin can update their shares.
    /// @dev `nonReentrant` shares the transient guard with `depositFees` and `claim`; this
    ///      prevents a malicious direct receiver from reentering `setShares` mid-deposit and
    ///      corrupting `_depositSplit`'s iteration over `cfg.directReceivers`.
    function setShares(address token, ILivoFactory.FeeShare[] calldata feeShares) external nonReentrant {
        TokenFeeConfigLib.Config storage cfg = _configs[token];
        require(cfg.isRegistered(), NotRegistered());

        address tokenOwner = ILivoToken(token).owner();
        require(msg.sender == owner() || (msg.sender == tokenOwner), Unauthorized());

        // Snapshotting of accumulated claimable ETH happens inside `_setSharesInternal`'s
        // claimable wipe loop, so no ETH is lost on transitions.
        _setSharesInternal(token, cfg, feeShares, true);
    }

    /// @notice Claims accumulated ETH fees for `msg.sender` across the given tokens.
    function claim(address[] calldata tokens) external nonReentrant {
        uint256 total;
        uint256 nTokens = tokens.length;

        for (uint256 i = 0; i < nTokens; i++) {
            address token = tokens[i];
            TokenFeeConfigLib.Config storage cfg = _configs[token];
            if (!cfg.isRegistered()) continue;

            uint256 claimable = _getAndClearClaimable(token, cfg, msg.sender);
            if (claimable == 0) continue;

            total += claimable;
            emit CreatorClaimed(token, msg.sender, claimable);
        }

        if (total == 0) return;
        _transferEth(msg.sender, total);
    }

    ////////////////////////////// VIEW FUNCTIONS ///////////////////////////////////

    /// @notice Returns the pending claimable ETH for `account` across the given tokens.
    function getClaimable(address[] calldata tokens, address account) external view returns (uint256[] memory amounts) {
        uint256 nTokens = tokens.length;
        amounts = new uint256[](nTokens);

        for (uint256 i = 0; i < nTokens; i++) {
            address token = tokens[i];
            TokenFeeConfigLib.Config storage cfg = _configs[token];
            if (!cfg.isRegistered()) continue;
            amounts[i] = _claimableView(token, cfg, account);
        }
    }

    /// @notice Returns all current recipients and their BPS shares for `token`.
    function getRecipients(address token) external view returns (address[] memory, uint256[] memory) {
        TokenFeeConfigLib.Config storage cfg = _configs[token];
        uint256 directLen = cfg.directReceivers.length;
        uint256 claimableLen = cfg.claimableRecipients.length;
        uint256 totalLen = directLen + claimableLen;

        address[] memory addrs = new address[](totalLen);
        uint256[] memory bps = new uint256[](totalLen);

        for (uint256 i = 0; i < directLen; i++) {
            address dr = cfg.directReceivers[i];
            addrs[i] = dr;
            bps[i] = _sharesBpsOf[token][dr];
        }
        for (uint256 i = 0; i < claimableLen; i++) {
            address cr = cfg.claimableRecipients[i];
            addrs[directLen + i] = cr;
            bps[directLen + i] = _sharesBpsOf[token][cr];
        }

        return (addrs, bps);
    }

    /// @notice Returns the current direct-receiver addresses for `token`.
    function getDirectReceivers(address token) external view returns (address[] memory) {
        return _configs[token].directReceivers;
    }

    ///////////////////////// INTERNAL //////////////////////////

    /// @dev Single-receiver deposit path. Either forwards directly or credits pending claims.
    ///      Branches on `totalDirectBps` (warm slot — packed with `isSplit` already read in
    ///      `depositFees`) instead of `directReceivers.length` to avoid a cold SLOAD.
    function _depositSingle(address token, TokenFeeConfigLib.Config storage cfg) internal {
        if (cfg.totalDirectBps > 0) {
            address receiver = cfg.directReceivers[0];
            (bool ok,) = receiver.call{value: msg.value, gas: DIRECT_FORWARD_GAS}("");
            if (ok) {
                emit CreatorClaimed(token, receiver, msg.value);
                return;
            }
            _pendingClaims[token][receiver] += msg.value;
        } else {
            _pendingClaims[token][cfg.claimableRecipients[0]] += msg.value;
        }
    }

    /// @dev Multi-receiver deposit path. Forwards direct slices and accumulates the rest.
    function _depositSplit(address token, TokenFeeConfigLib.Config storage cfg) internal {
        uint256 directAmountTotal;

        uint256 directLen = cfg.directReceivers.length;
        for (uint256 i = 0; i < directLen; i++) {
            address dr = cfg.directReceivers[i];
            uint256 directAmount = (msg.value * _sharesBpsOf[token][dr]) / BPS_TOTAL;
            directAmountTotal += directAmount;
            if (directAmount == 0) continue;

            (bool ok,) = dr.call{value: directAmount, gas: DIRECT_FORWARD_GAS}("");
            if (ok) {
                emit CreatorClaimed(token, dr, directAmount);
            } else {
                _pendingClaims[token][dr] += directAmount;
            }
        }

        uint256 claimableBpsTot = cfg.claimableBpsTotal();
        if (claimableBpsTot > 0) {
            uint256 toAccumulate = msg.value - directAmountTotal;
            cfg.ethPerBps += (toAccumulate * PRECISION) / claimableBpsTot;
        }
    }

    /// @dev Returns and clears all claimable ETH for `account` on `token`.
    function _getAndClearClaimable(address token, TokenFeeConfigLib.Config storage cfg, address account)
        internal
        returns (uint256 claimable)
    {
        if (isDirectReceiver[token][account]) {
            claimable = _pendingClaims[token][account];
            _pendingClaims[token][account] = 0;
        } else {
            claimable = _accruedClaimableFor(token, cfg, account) + _pendingClaims[token][account];
            _claimedPerBps[token][account] = cfg.ethPerBps;
            _pendingClaims[token][account] = 0;
        }
    }

    /// @dev View counterpart of `_getAndClearClaimable` — no state mutation.
    function _claimableView(address token, TokenFeeConfigLib.Config storage cfg, address account)
        internal
        view
        returns (uint256)
    {
        if (isDirectReceiver[token][account]) {
            return _pendingClaims[token][account];
        }
        return _accruedClaimableFor(token, cfg, account) + _pendingClaims[token][account];
    }

    /// @dev Accumulator-based claimable for a claimable (non-direct) account.
    function _accruedClaimableFor(address token, TokenFeeConfigLib.Config storage cfg, address account)
        internal
        view
        returns (uint256)
    {
        return (cfg.ethPerBps - _claimedPerBps[token][account]) * _sharesBpsOf[token][account] / PRECISION;
    }

    /// @dev Rebuilds the per-token config from `feeShares`. When `isUpdate = true`, wipes previous
    ///      per-account state and emits diff-style direct-set events before the final `SharesUpdated`.
    function _setSharesInternal(
        address token,
        TokenFeeConfigLib.Config storage cfg,
        ILivoFactory.FeeShare[] calldata feeShares,
        bool isUpdate
    ) internal {
        uint256 len = feeShares.length;
        require(len > 0, InvalidFeeShares());
        require(len <= MAX_FEE_RECEIVERS, TooManyFeeReceivers());
        _requireNoDuplicates(feeShares);

        address[] memory oldDirects;

        if (isUpdate) {
            // Capture old direct set for diff events before wiping.
            oldDirects = cfg.directReceivers;

            // Wipe claimable per-account state, snapshotting accumulator-based accrual into pending
            // first so removals keep their residue (and re-registered recipients don't double-earn).
            // `cfg.ethPerBps` is cached once outside the loop to avoid re-reading the storage slot.
            uint256 cachedEthPerBps = cfg.ethPerBps;
            uint256 oldClaimableLen = cfg.claimableRecipients.length;
            for (uint256 i = 0; i < oldClaimableLen; i++) {
                address r = cfg.claimableRecipients[i];
                _pendingClaims[token][r] += (cachedEthPerBps - _claimedPerBps[token][r]) * _sharesBpsOf[token][r]
                    / PRECISION;
                delete _sharesBpsOf[token][r];
                // _claimedPerBps[token][r] is stale but harmless: sharesBps==0 zeroes the accumulator term.
            }
            delete cfg.claimableRecipients;

            // Wipe direct per-account state.
            uint256 oldDirectLen = oldDirects.length;
            for (uint256 i = 0; i < oldDirectLen; i++) {
                address r = oldDirects[i];
                delete _sharesBpsOf[token][r];
                delete isDirectReceiver[token][r];
            }
            delete cfg.directReceivers;
        }

        // Build new config (extracted to reduce stack depth).
        (address[] memory newRecipients, uint256[] memory newShares) = _populateNewShares(token, cfg, feeShares, len);

        cfg.isSplit = len > 1;

        // Diff-style direct-set events (only on updates).
        if (isUpdate) {
            uint256 oldDirectLen = oldDirects.length;

            // Removals: old direct whose new isDirectReceiver entry is false.
            for (uint256 i = 0; i < oldDirectLen; i++) {
                if (!isDirectReceiver[token][oldDirects[i]]) {
                    emit DirectReceiverRemoved(token, oldDirects[i]);
                }
            }

            // Additions: new direct that wasn't in the old set.
            for (uint256 i = 0; i < len; i++) {
                if (!feeShares[i].directFeesEnabled) continue;
                address acc = feeShares[i].account;
                bool wasDirectBefore;
                for (uint256 j = 0; j < oldDirectLen; j++) {
                    if (oldDirects[j] == acc) {
                        wasDirectBefore = true;
                        break;
                    }
                }
                if (!wasDirectBefore) {
                    emit DirectReceiverRegistered(token, acc);
                }
            }
        } else {
            // At init: emit DirectReceiverRegistered for every direct entry.
            for (uint256 i = 0; i < len; i++) {
                if (feeShares[i].directFeesEnabled) {
                    emit DirectReceiverRegistered(token, feeShares[i].account);
                }
            }
        }

        emit SharesUpdated(token, newRecipients, newShares);
    }

    /// @dev Populates per-account mappings and config arrays from `feeShares`. Separated from
    ///      `_setSharesInternal` to avoid a stack-too-deep error in that function.
    function _populateNewShares(
        address token,
        TokenFeeConfigLib.Config storage cfg,
        ILivoFactory.FeeShare[] calldata feeShares,
        uint256 len
    ) private returns (address[] memory recipients, uint256[] memory shares) {
        recipients = new address[](len);
        shares = new uint256[](len);
        uint256 total;
        uint256 directSum;
        // Cache `cfg.ethPerBps` once: read N times by the loop otherwise. Also lets us skip the
        // SSTORE entirely when zero (which is always the case at `registerToken`, and avoids a
        // wasted cold 0→0 write per claimable on init).
        uint256 cachedEthPerBps = cfg.ethPerBps;

        for (uint256 i = 0; i < len; i++) {
            address acc = feeShares[i].account;
            uint256 sh = feeShares[i].shares;
            require(acc != address(0), InvalidFeeShares());
            require(sh > 0, InvalidShares());

            recipients[i] = acc;
            shares[i] = sh;
            total += sh;

            _sharesBpsOf[token][acc] = sh;

            if (feeShares[i].directFeesEnabled) {
                isDirectReceiver[token][acc] = true;
                cfg.directReceivers.push(acc);
                directSum += sh;
            } else {
                cfg.claimableRecipients.push(acc);
                if (cachedEthPerBps != 0) {
                    _claimedPerBps[token][acc] = cachedEthPerBps;
                }
            }
        }
        require(total == BPS_TOTAL, InvalidShares());
        require(cfg.directReceivers.length <= MAX_DIRECT_RECEIVERS, TooManyDirectReceivers());
        // Safe cast: `directSum <= total == BPS_TOTAL == 10_000`, fits in uint16.
        cfg.totalDirectBps = uint16(directSum);
    }

    /// @dev Reverts if any two `feeShares` entries share the same `account`.
    function _requireNoDuplicates(ILivoFactory.FeeShare[] calldata feeShares) internal pure {
        uint256 len = feeShares.length;
        for (uint256 i = 0; i < len; i++) {
            for (uint256 j = i + 1; j < len; j++) {
                require(feeShares[i].account != feeShares[j].account, InvalidFeeShares());
            }
        }
    }

    /// @dev Transfers ETH to `recipient`, reverting on failure.
    function _transferEth(address recipient, uint256 amount) internal {
        if (amount == 0) return;
        (bool success,) = recipient.call{value: amount}("");
        require(success, EthTransferFailed());
    }
}
Chain explorer5972msChain node96ms