From b37b714c5421ac4a7625e6729544e03bcfb0174c Mon Sep 17 00:00:00 2001 From: Aashish Paliwal Date: Fri, 20 Jun 2025 15:48:49 +0530 Subject: [PATCH 1/2] feat(interfaces): add IPayments interface defining structure and external methods --- src/interfaces/IPayments.sol | 266 +++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 src/interfaces/IPayments.sol diff --git a/src/interfaces/IPayments.sol b/src/interfaces/IPayments.sol new file mode 100644 index 00000000..46b9814c --- /dev/null +++ b/src/interfaces/IPayments.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title IPayments +/// @notice Interface for the Payments contract, defining all external functions and relevant structs. +interface IPayments { + // -------- Structs -------- + + /// @notice Account data for each user and token. + struct Account { + uint256 funds; + uint256 lockupCurrent; + uint256 lockupRate; + uint256 lockupLastSettledAt; + } + + /// @notice Struct returned by getRail, representing a rail's public state (excluding internal-only fields). + struct RailView { + address token; + address from; + address to; + address operator; + address arbiter; + uint256 paymentRate; + uint256 lockupPeriod; + uint256 lockupFixed; + uint256 settledUpTo; + uint256 endEpoch; + uint256 commissionRateBps; + } + + /// @notice Approval and usage stats for operators managing rails on behalf of clients. + struct OperatorApproval { + bool isApproved; + uint256 rateAllowance; + uint256 lockupAllowance; + uint256 rateUsage; + uint256 lockupUsage; + uint256 maxLockupPeriod; + } + + /// @notice Define a struct for rails by payee information + struct RailInfo { + uint256 railId; + bool isTerminated; + uint256 endEpoch; + } + + /// @notice Settlement state for a rail. + struct SettlementState { + uint256 totalSettledAmount; + uint256 totalNetPayeeAmount; + uint256 totalPaymentFee; + uint256 totalOperatorCommission; + uint256 processedEpoch; + string note; + } + + // -------- Events -------- + + /// @notice Emitted when tokens are deposited using permit (EIP-2612). + event DepositWithPermit( + address indexed token, + address indexed from, + address indexed to, + uint256 amount + ); + + // -------- Functions -------- + // Marking public functions as external as solidity doesn't allow external functions to be marked as public in interfaces. + + /// @notice Initializes the Payments contract (for upgradeable proxies). + function initialize() external; + + /// @notice Gets the current state of the target rail or reverts if the rail isn't active. + /// @param railId the ID of the rail. + function getRail(uint256 railId) external view returns (RailView memory); + + /// @notice Updates the approval status and allowances for an operator on behalf of the message sender. + /// @param token The ERC20 token address for which the approval is being set. + /// @param operator The address of the operator whose approval is being modified. + /// @param approved Whether the operator is approved (true) or not (false) to create new rails> + /// @param rateAllowance The maximum payment rate the operator can set across all rails created by the operator on behalf of the message sender. If this is less than the current payment rate, the operator will only be able to reduce rates until they fall below the target. + /// @param lockupAllowance The maximum amount of funds the operator can lock up on behalf of the message sender towards future payments. If this exceeds the current total amount of funds locked towards future payments, the operator will only be able to reduce future lockup. + /// @param maxLockupPeriod The maximum number of epochs (blocks) the operator can lock funds for. If this is less than the current lockup period for a rail, the operator will only be able to reduce the lockup period. + function setOperatorApproval( + address token, + address operator, + bool approved, + uint256 rateAllowance, + uint256 lockupAllowance, + uint256 maxLockupPeriod + ) external; + + /// @notice Terminates a payment rail, preventing further payments after the rail's lockup period. After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced. + /// @param railId The ID of the rail to terminate. + function terminateRail(uint256 railId) external; + + /// @notice Deposits tokens from the message sender's account into `to`'s account. + /// @param token The ERC20 token address to deposit. + /// @param to The address whose account will be credited. + /// @param amount The amount of tokens to deposit. + function deposit( + address token, + address to, + uint256 amount + ) external payable; + + /** + * @notice Deposits tokens using permit (EIP-2612) approval in a single transaction. + * @param token The ERC20 token address to deposit. + * @param to The address whose account will be credited. + * @param amount The amount of tokens to deposit. + * @param deadline Permit deadline (timestamp). + * @param v,r,s Permit signature. + */ + function depositWithPermit( + address token, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @notice Withdraws tokens from the caller's account to the caller's account, up to the amount of currently available tokens (the tokens not currently locked in rails). + /// @param token The ERC20 token address to withdraw. + /// @param amount The amount of tokens to withdraw. + function withdraw(address token, uint256 amount) external; + + /// @notice Withdraws tokens (`token`) from the caller's account to `to`, up to the amount of currently available tokens (the tokens not currently locked in rails). + /// @param token The ERC20 token address to withdraw. + /// @param to The address to receive the withdrawn tokens. + /// @param amount The amount of tokens to withdraw. + function withdrawTo(address token, address to, uint256 amount) external; + + /// @notice Create a new rail from `from` to `to`, operated by the caller. + /// @param token The ERC20 token address for payments on this rail. + /// @param from The client address (payer) for this rail. + /// @param to The recipient address for payments on this rail. + /// @param arbiter Optional address of an arbiter contract (can be address(0) for no arbitration). + /// @param commissionRateBps Optional operator commission in basis points (0-10000). + /// @return The ID of the newly created rail. + function createRail( + address token, + address from, + address to, + address arbiter, + uint256 commissionRateBps + ) external returns (uint256); + + /// @notice Modifies the fixed lockup and lockup period of a rail. + /// - If the rail has already been terminated, the lockup period may not be altered and the fixed lockup may only be reduced. + /// - If the rail is active, the lockup may only be modified if the payer's account is fully funded and will remain fully funded after the operation. + /// @param railId The ID of the rail to modify. + /// @param period The new lockup period (in epochs/blocks). + /// @param lockupFixed The new fixed lockup amount. + function modifyRailLockup( + uint256 railId, + uint256 period, + uint256 lockupFixed + ) external; + + /// @notice Modifies the payment rate and optionally makes a one-time payment. + /// - If the rail has already been terminated, one-time payments can be made and the rate may always be decreased (but never increased) regardless of the status of the payer's account. + /// - If the payer's account isn't fully funded and the rail is active (not terminated), the rail's payment rate may not be changed at all (increased or decreased). + /// - Regardless of the payer's account status, one-time payments will always go through provided that the rail has sufficient fixed lockup to cover the payment. + /// @param railId The ID of the rail to modify. + /// @param newRate The new payment rate (per epoch). This new rate applies starting the next epoch after the current one. + /// @param oneTimePayment Optional one-time payment amount to transfer immediately, taken out of the rail's fixed lockup. + function modifyRailPayment( + uint256 railId, + uint256 newRate, + uint256 oneTimePayment + ) external; + + /// @notice Settles payments for a terminated rail without arbitration. This may only be called by the payee and after the terminated rail's max settlement epoch has passed. It's an escape-hatch to unblock payments in an otherwise stuck rail (e.g., due to a buggy arbiter contract) and it always pays in full. + /// @param railId The ID of the rail to settle. + /// @return totalSettledAmount The total amount settled and transferred. + /// @return totalNetPayeeAmount The net amount credited to the payee after fees. + /// @return totalPaymentFee The fee retained by the payment contract. + /// @return totalOperatorCommission The commission credited to the operator. + /// @return finalSettledEpoch The epoch up to which settlement was actually completed. + /// @return note Additional information about the settlement. + function settleTerminatedRailWithoutArbitration( + uint256 railId + ) + external + returns ( + uint256 totalSettledAmount, + uint256 totalNetPayeeAmount, + uint256 totalPaymentFee, + uint256 totalOperatorCommission, + uint256 finalSettledEpoch, + string memory note + ); + + /// @notice Settles payments for a rail up to the specified epoch. Settlement may fail to reach the target epoch if either the client lacks the funds to pay up to the current epoch or the arbiter refuses to settle the entire requested range. + /// @param railId The ID of the rail to settle. + /// @param untilEpoch The epoch up to which to settle (must not exceed current block number). + /// @return totalSettledAmount The total amount settled and transferred. + /// @return totalNetPayeeAmount The net amount credited to the payee after fees. + /// @return totalPaymentFee The fee retained by the payment contract. + /// @return totalOperatorCommission The commission credited to the operator. + /// @return finalSettledEpoch The epoch up to which settlement was actually completed. + /// @return note Additional information about the settlement (especially from arbitration). + function settleRail( + uint256 railId, + uint256 untilEpoch + ) + external + returns ( + uint256 totalSettledAmount, + uint256 totalNetPayeeAmount, + uint256 totalPaymentFee, + uint256 totalOperatorCommission, + uint256 finalSettledEpoch, + string memory note + ); + + /// @notice Allows the contract owner to withdraw accumulated payment fees. + /// @param token The ERC20 token address of the fees to withdraw. + /// @param to The address to send the withdrawn fees to. + /// @param amount The amount of fees to withdraw. + function withdrawFees( + address token, + address to, + uint256 amount + ) external; + + /// @notice Returns information about all accumulated fees + /// @return tokens Array of token addresses that have accumulated fees + /// @return amounts Array of fee amounts corresponding to each token + /// @return count Total number of tokens with accumulated fees + function getAllAccumulatedFees() + external + view + returns ( + address[] memory tokens, + uint256[] memory amounts, + uint256 count + ); + + /** + * @notice Gets all rails where the given address is the payer for a specific token. + * @param payer The address of the payer to get rails for. + * @param token The token address to filter rails by. + * @return Array of RailInfo structs containing rail IDs and termination status. + */ + function getRailsForPayerAndToken( + address payer, + address token + ) external view returns (RailInfo[] memory); + + /** + * @notice Gets all rails where the given address is the payee for a specific token. + * @param payee The address of the payee to get rails for. + * @param token The token address to filter rails by. + * @return Array of RailInfo structs containing rail IDs and termination status. + */ + function getRailsForPayeeAndToken( + address payee, + address token + ) external view returns (RailInfo[] memory); +} From 9c7627fbc9bbc023c8c27686601f792c15752fb9 Mon Sep 17 00:00:00 2001 From: Aashish Paliwal Date: Tue, 24 Jun 2025 11:38:28 +0530 Subject: [PATCH 2/2] refactor(payments): prepare contract for compilation without via-ir - refactor internal settlement logic to use SettlementResult and SettlementState structs for cleaner return handling and avoiding stack-too-deep error. - replace depositWithPermit() multi-argument signature with a PermitParams struct. - move arbitration logic out of _settleSegment() into dedicated _runArbitration() helper. - split modifyRailPayment logic into internal _modifyRailPayment. - minor grouping, naming, and internal structure improvements. --- foundry.toml | 1 - src/Payments.sol | 458 +++++++++++++++++++++++------------------------ 2 files changed, 229 insertions(+), 230 deletions(-) diff --git a/foundry.toml b/foundry.toml index f2029968..527e409f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,6 @@ out = 'out' libs = ['lib'] cache_path = 'cache' solc = "0.8.25" -via_ir = true # For dependencies remappings = [ diff --git a/src/Payments.sol b/src/Payments.sol index b291fa97..17865f36 100644 --- a/src/Payments.sol +++ b/src/Payments.sol @@ -141,6 +141,24 @@ contract Payments is string note; } + struct SettlementResult { + uint256 totalSettledAmount; + uint256 totalNetPayeeAmount; + uint256 totalPaymentFee; + uint256 totalOperatorCommission; + string note; + } + + struct PermitParams { + address token; + address to; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + // Events event DepositWithPermit(address indexed token, address indexed account, uint256 amount); @@ -418,37 +436,33 @@ contract Payments is /** * @notice Deposits tokens using permit (EIP-2612) approval in a single transaction. - * @param token The ERC20 token address to deposit. - * @param to The address whose account will be credited (must be the permit signer). - * @param amount The amount of tokens to deposit. - * @param deadline Permit deadline (timestamp). - * @param v,r,s Permit signature. + * @param params The struct containing parameters for the deposit with permit. + * - `token`: The ERC20 token address to deposit. + * - `to`: The address whose account will be credited (must be the permit signer). + * - `amount`: The amount of tokens to deposit. + * - `deadline`: Permit deadline (timestamp). + * - `v`, `r`, `s`: Permit signature components. */ - function depositWithPermit( - address token, - address to, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) + function depositWithPermit(PermitParams calldata params) external nonReentrant - validateNonZeroAddress(to, "to") - settleAccountLockupBeforeAndAfter(token, to, false) + validateNonZeroAddress(params.to, "to") + settleAccountLockupBeforeAndAfter(params.token, params.to, false) { // Revert if token is address(0) as permit is not supported for native tokens - require(token != address(0), "depositWithPermit: native token not supported"); + require( + params.token != address(0), + "depositWithPermit: native token not supported" + ); // Use 'to' as the owner in permit call (the address that signed the permit) - IERC20Permit(token).permit(to, address(this), amount, deadline, v, r, s); + IERC20Permit(params.token).permit(params.to, address(this), params.amount, params.deadline, params.v, params.r, params.s); - Account storage account = accounts[token][to]; - IERC20(token).safeTransferFrom(to, address(this), amount); - account.funds += amount; + Account storage account = accounts[params.token][params.to]; + IERC20(params.token).safeTransferFrom(params.to, address(this), params.amount); + account.funds += params.amount; - emit DepositWithPermit(token, to, amount); + emit DepositWithPermit(params.token, params.to, params.amount); } @@ -700,6 +714,14 @@ contract Payments is onlyRailOperator(railId) settleAccountLockupBeforeAndAfterForRail(railId, false, oneTimePayment) { + _modifyRailPayment(railId, newRate, oneTimePayment); + } + + function _modifyRailPayment( + uint256 railId, + uint256 newRate, + uint256 oneTimePayment + ) internal { Rail storage rail = rails[railId]; Account storage payer = accounts[rail.token][rail.from]; Account storage payee = accounts[rail.token][rail.to]; @@ -924,12 +946,13 @@ contract Payments is /// @notice Settles payments for a terminated rail without arbitration. This may only be called by the payee and after the terminated rail's max settlement epoch has passed. It's an escape-hatch to unblock payments in an otherwise stuck rail (e.g., due to a buggy arbiter contract) and it always pays in full. /// @param railId The ID of the rail to settle. - /// @return totalSettledAmount The total amount settled and transferred. - /// @return totalNetPayeeAmount The net amount credited to the payee after fees. - /// @return totalPaymentFee The fee retained by the payment contract. - /// @return totalOperatorCommission The commission credited to the operator. - /// @return finalSettledEpoch The epoch up to which settlement was actually completed. - /// @return note Additional information about the settlement. + /// @return SettlementState struct containing details of the settlement, including: + /// - totalSettledAmount: The total amount settled and transferred. + /// - totalNetPayeeAmount: The net amount credited to the payee after fees. + /// - totalPaymentFee: The fee retained by the payment contract. + /// - totalOperatorCommission: The commission credited to the operator. + /// - finalSettledEpoch: The epoch up to which settlement was actually completed. + /// - note: Additional information about the settlement. function settleTerminatedRailWithoutArbitration( uint256 railId ) @@ -939,14 +962,7 @@ contract Payments is validateRailTerminated(railId) onlyRailClient(railId) settleAccountLockupBeforeAndAfterForRail(railId, false, 0) - returns ( - uint256 totalSettledAmount, - uint256 totalNetPayeeAmount, - uint256 totalPaymentFee, - uint256 totalOperatorCommission, - uint256 finalSettledEpoch, - string memory note - ) + returns (SettlementState memory) { // Verify the current epoch is greater than the max settlement epoch uint256 maxSettleEpoch = maxSettlementEpochForTerminatedRail( @@ -960,15 +976,18 @@ contract Payments is return settleRailInternal(railId, maxSettleEpoch, true); } - /// @notice Settles payments for a rail up to the specified epoch. Settlement may fail to reach the target epoch if either the client lacks the funds to pay up to the current epoch or the arbiter refuses to settle the entire requested range. + /// @notice Settles payments for a rail up to the specified epoch. + /// Settlement may fail to reach the target epoch if either the client lacks the funds to pay up to the current epoch + /// or the arbiter refuses to settle the entire requested range. /// @param railId The ID of the rail to settle. /// @param untilEpoch The epoch up to which to settle (must not exceed current block number). - /// @return totalSettledAmount The total amount settled and transferred. - /// @return totalNetPayeeAmount The net amount credited to the payee after fees. - /// @return totalPaymentFee The fee retained by the payment contract. - /// @return totalOperatorCommission The commission credited to the operator. - /// @return finalSettledEpoch The epoch up to which settlement was actually completed. - /// @return note Additional information about the settlement (especially from arbitration). + /// @return SettlementState struct containing details of the settlement, including: + /// - totalSettledAmount: The total amount settled and transferred. + /// - totalNetPayeeAmount: The net amount credited to the payee after fees. + /// - totalPaymentFee: The fee retained by the payment contract. + /// - totalOperatorCommission: The commission credited to the operator. + /// - finalSettledEpoch: The epoch up to which settlement was actually completed. + /// - note: Additional information about the settlement (especially from arbitration). function settleRail( uint256 railId, uint256 untilEpoch @@ -978,14 +997,7 @@ contract Payments is validateRailActive(railId) onlyRailParticipant(railId) settleAccountLockupBeforeAndAfterForRail(railId, false, 0) - returns ( - uint256 totalSettledAmount, - uint256 totalNetPayeeAmount, - uint256 totalPaymentFee, - uint256 totalOperatorCommission, - uint256 finalSettledEpoch, - string memory note - ) + returns (SettlementState memory) { return settleRailInternal(railId, untilEpoch, false); } @@ -996,14 +1008,7 @@ contract Payments is bool skipArbitration ) internal - returns ( - uint256 totalSettledAmount, - uint256 totalNetPayeeAmount, - uint256 totalPaymentFee, - uint256 totalOperatorCommission, - uint256 finalSettledEpoch, - string memory note - ) + returns (SettlementState memory) { require( untilEpoch <= block.number, @@ -1016,14 +1021,14 @@ contract Payments is // Handle terminated and fully settled rails that are still not finalised if (isRailTerminated(rail) && rail.settledUpTo >= rail.endEpoch) { finalizeTerminatedRail(rail, payer); - return ( - 0, - 0, - 0, - 0, - rail.settledUpTo, - "rail fully settled and finalized" - ); + return SettlementState ({ + totalSettledAmount: 0, + totalNetPayeeAmount: 0, + totalPaymentFee: 0, + totalOperatorCommission: 0, + processedEpoch: rail.settledUpTo, + note: "rail fully settled and finalized" + }); } // Calculate the maximum settlement epoch based on account lockup @@ -1037,28 +1042,24 @@ contract Payments is uint256 startEpoch = rail.settledUpTo; // Nothing to settle (already settled or zero-duration) if (startEpoch >= maxSettlementEpoch) { - return ( - 0, - 0, - 0, - 0, - startEpoch, - string.concat( + return SettlementState ({ + totalSettledAmount: 0, + totalNetPayeeAmount: 0, + totalPaymentFee: 0, + totalOperatorCommission: 0, + processedEpoch: startEpoch, + note: string.concat( "already settled up to epoch ", Strings.toString(maxSettlementEpoch) ) - ); + }); } + SettlementState memory state; + // Process settlement depending on whether rate changes exist if (rail.rateChangeQueue.isEmpty()) { - ( - uint256 amount, - uint256 netPayeeAmount, - uint256 paymentFee, - uint256 operatorCommission, - string memory segmentNote - ) = _settleSegment( + SettlementResult memory result = _settleSegment( railId, startEpoch, maxSettlementEpoch, @@ -1068,29 +1069,27 @@ contract Payments is require(rail.settledUpTo > startEpoch, "No progress in settlement"); + state = SettlementState({ + totalSettledAmount: result.totalSettledAmount, + totalNetPayeeAmount: result.totalNetPayeeAmount, + totalPaymentFee: result.totalPaymentFee, + totalOperatorCommission: result.totalOperatorCommission, + processedEpoch: rail.settledUpTo, + note: result.note + }); + return checkAndFinalizeTerminatedRail( rail, payer, - amount, - netPayeeAmount, - paymentFee, - operatorCommission, - rail.settledUpTo, - segmentNote, + state, string.concat( - segmentNote, + state.note, "terminated rail fully settled and finalized." ) ); } else { - ( - uint256 settledAmount, - uint256 netPayeeAmount, - uint256 paymentFee, - uint256 operatorCommission, - string memory settledNote - ) = _settleWithRateChanges( + SettlementResult memory result = _settleWithRateChanges( railId, rail.paymentRate, startEpoch, @@ -1098,18 +1097,22 @@ contract Payments is skipArbitration ); + state = SettlementState({ + totalSettledAmount: result.totalSettledAmount, + totalNetPayeeAmount: result.totalNetPayeeAmount, + totalPaymentFee: result.totalPaymentFee, + totalOperatorCommission: result.totalOperatorCommission, + processedEpoch: rail.settledUpTo, + note: result.note + }); + return checkAndFinalizeTerminatedRail( rail, payer, - settledAmount, - netPayeeAmount, - paymentFee, - operatorCommission, - rail.settledUpTo, - settledNote, + state, string.concat( - settledNote, + state.note, "terminated rail fully settled and finalized." ) ); @@ -1119,16 +1122,11 @@ contract Payments is function checkAndFinalizeTerminatedRail( Rail storage rail, Account storage payer, - uint256 totalSettledAmount, - uint256 totalNetPayeeAmount, - uint256 totalPaymentFee, - uint256 totalOperatorCommission, - uint256 finalEpoch, - string memory regularNote, + SettlementState memory state, string memory finalizedNote ) internal - returns (uint256, uint256, uint256, uint256, uint256, string memory) + returns (SettlementState memory) { // Check if rail is a terminated rail that's now fully settled if ( @@ -1136,24 +1134,10 @@ contract Payments is rail.settledUpTo >= maxSettlementEpochForTerminatedRail(rail) ) { finalizeTerminatedRail(rail, payer); - return ( - totalSettledAmount, - totalNetPayeeAmount, - totalPaymentFee, - totalOperatorCommission, - finalEpoch, - finalizedNote - ); + state.note = finalizedNote; } - return ( - totalSettledAmount, - totalNetPayeeAmount, - totalPaymentFee, - totalOperatorCommission, - finalEpoch, - regularNote - ); + return state; } function finalizeTerminatedRail( @@ -1186,13 +1170,7 @@ contract Payments is bool skipArbitration ) internal - returns ( - uint256 totalSettledAmount, - uint256 totalNetPayeeAmount, - uint256 totalPaymentFee, - uint256 totalOperatorCommission, - string memory note - ) + returns (SettlementResult memory) { Rail storage rail = rails[railId]; RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue; @@ -1233,13 +1211,7 @@ contract Payments is } // Settle the current segment with potentially arbitrated outcomes - ( - uint256 segmentSettledAmount, - uint256 segmentNetPayeeAmount, - uint256 segmentPaymentFee, - uint256 segmentOperatorCommission, - string memory arbitrationNote - ) = _settleSegment( + SettlementResult memory settlementResult = _settleSegment( railId, state.processedEpoch, segmentEndBoundary, @@ -1249,35 +1221,35 @@ contract Payments is // If arbiter returned no progress, exit early without updating state if (rail.settledUpTo <= state.processedEpoch) { - return ( - state.totalSettledAmount, - state.totalNetPayeeAmount, - state.totalPaymentFee, - state.totalOperatorCommission, - arbitrationNote - ); + return SettlementResult({ + totalSettledAmount: state.totalSettledAmount, + totalNetPayeeAmount: state.totalNetPayeeAmount, + totalPaymentFee: state.totalPaymentFee, + totalOperatorCommission: state.totalOperatorCommission, + note: settlementResult.note + }); } // Add the settled amounts to our running totals - state.totalSettledAmount += segmentSettledAmount; - state.totalNetPayeeAmount += segmentNetPayeeAmount; - state.totalPaymentFee += segmentPaymentFee; - state.totalOperatorCommission += segmentOperatorCommission; + state.totalSettledAmount += settlementResult.totalSettledAmount; + state.totalNetPayeeAmount += settlementResult.totalNetPayeeAmount; + state.totalPaymentFee += settlementResult.totalPaymentFee; + state.totalOperatorCommission += settlementResult.totalOperatorCommission; // If arbiter partially settled the segment, exit early if (rail.settledUpTo < segmentEndBoundary) { - return ( - state.totalSettledAmount, - state.totalNetPayeeAmount, - state.totalPaymentFee, - state.totalOperatorCommission, - arbitrationNote - ); + return SettlementResult ({ + totalSettledAmount: state.totalSettledAmount, + totalNetPayeeAmount: state.totalNetPayeeAmount, + totalPaymentFee: state.totalPaymentFee, + totalOperatorCommission: state.totalOperatorCommission, + note: settlementResult.note + }); } // Successfully settled full segment, update tracking values state.processedEpoch = rail.settledUpTo; - state.note = arbitrationNote; + state.note = settlementResult.note; // Remove the processed rate change from the queue if (!rateQueue.isEmpty()) { @@ -1286,13 +1258,13 @@ contract Payments is } // We've successfully settled up to the target epoch - return ( - state.totalSettledAmount, - state.totalNetPayeeAmount, - state.totalPaymentFee, - state.totalOperatorCommission, - state.note - ); + return SettlementResult ({ + totalSettledAmount: state.totalSettledAmount, + totalNetPayeeAmount: state.totalNetPayeeAmount, + totalPaymentFee: state.totalPaymentFee, + totalOperatorCommission: state.totalOperatorCommission, + note: state.note + }); } function _getNextSegmentBoundary( @@ -1327,65 +1299,39 @@ contract Payments is uint256 epochEnd, uint256 rate, bool skipArbitration - ) - internal - returns ( - uint256 totalSettledAmount, - uint256 netPayeeAmount, - uint256 paymentFee, - uint256 operatorCommission, - string memory note - ) - { + ) internal returns (SettlementResult memory) { Rail storage rail = rails[railId]; Account storage payer = accounts[rail.token][rail.from]; Account storage payee = accounts[rail.token][rail.to]; if (rate == 0) { rail.settledUpTo = epochEnd; - return (0, 0, 0, 0, "Zero rate payment rail"); - } - - // Calculate the default settlement values (without arbitration) - uint256 duration = epochEnd - epochStart; - uint256 settledAmount = rate * duration; - uint256 settledUntilEpoch = epochEnd; - note = ""; - - // If this rail has an arbiter and we're not skipping arbitration, let it decide on the final settlement amount - if (rail.arbiter != address(0) && !skipArbitration) { - IArbiter arbiter = IArbiter(rail.arbiter); - IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment( - railId, - settledAmount, - epochStart, - epochEnd, - rate - ); - - // Ensure arbiter doesn't settle beyond our segment's end boundary - require( - result.settleUpto <= epochEnd, - "arbiter settled beyond segment end" - ); - require( - result.settleUpto >= epochStart, - "arbiter settled before segment start" - ); - - settledUntilEpoch = result.settleUpto; - settledAmount = result.modifiedAmount; - note = result.note; - // Ensure arbiter doesn't allow more payment than the maximum possible - // for the epochs they're confirming - uint256 maxAllowedAmount = rate * (settledUntilEpoch - epochStart); - require( - result.modifiedAmount <= maxAllowedAmount, - "arbiter modified amount exceeds maximum for settled duration" - ); + return + SettlementResult({ + totalSettledAmount: 0, + totalNetPayeeAmount: 0, + totalPaymentFee: 0, + totalOperatorCommission: 0, + note: "Zero rate payment rail" + }); } + uint256 settledAmount = rate * (epochEnd - epochStart); + + // Arbitration logic moved to helper + (uint256 settledUntilEpoch, uint256 modifiedAmount, string memory note) = _runArbitration( + rail, + railId, + settledAmount, + epochStart, + epochEnd, + rate, + skipArbitration + ); + + settledAmount = modifiedAmount; + // Verify payer has sufficient funds for the settlement require( payer.funds >= settledAmount, @@ -1402,12 +1348,16 @@ contract Payments is payer.funds -= settledAmount; // Calculate fees, pay operator commission and track platform fees - (netPayeeAmount, paymentFee, operatorCommission) = calculateAndPayFees( - settledAmount, - rail.token, - rail.operator, - rail.commissionRateBps - ); + ( + uint256 netPayeeAmount, + uint256 paymentFee, + uint256 operatorCommission + ) = calculateAndPayFees( + settledAmount, + rail.token, + rail.operator, + rail.commissionRateBps + ); // Credit payee payee.funds += netPayeeAmount; @@ -1424,13 +1374,63 @@ contract Payments is "failed to settle: invariant violation: insufficient funds to cover lockup after settlement" ); - return ( - settledAmount, - netPayeeAmount, - paymentFee, - operatorCommission, - note - ); + return + SettlementResult({ + totalSettledAmount: settledAmount, + totalNetPayeeAmount: netPayeeAmount, + totalPaymentFee: paymentFee, + totalOperatorCommission: operatorCommission, + note: note + }); + } + + function _runArbitration( + Rail storage rail, + uint256 railId, + uint256 settledAmount, + uint256 epochStart, + uint256 epochEnd, + uint256 rate, + bool skipArbitration + ) internal returns (uint256 settledUntilEpoch, uint256 modifiedAmount, string memory note) { + // If the rail has an arbiter and we're not skipping arbitration, let it decide on the final settlement amount + if (rail.arbiter != address(0) && !skipArbitration) { + IArbiter arbiter = IArbiter(rail.arbiter); + IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment( + railId, + settledAmount, + epochStart, + epochEnd, + rate + ); + + // Ensure arbiter doesn't settle beyond our segment's end boundary + require( + result.settleUpto <= epochEnd, + "arbiter settled beyond segment end" + ); + require( + result.settleUpto >= epochStart, + "arbiter settled before segment start" + ); + + settledUntilEpoch = result.settleUpto; + modifiedAmount = result.modifiedAmount; + note = result.note; + + // Ensure arbiter doesn't allow more payment than the maximum possible + // for the epochs they're confirming + uint256 maxAllowedAmount = rate * (settledUntilEpoch - epochStart); + require( + modifiedAmount <= maxAllowedAmount, + "arbiter modified amount exceeds maximum for settled duration" + ); + } else { + // No arbitration, use default values + settledUntilEpoch = epochEnd; + modifiedAmount = settledAmount; + note = ""; + } } function isAccountLockupFullySettled(