Repository: FilOzone/payments
Pull request #1: Payments contract first draft
Author: login: aarshkshah1992
Created: 2025-02-07T12:42:24Z | Merged: 2025-03-04T08:29:38Z

This is the first draft for the payments contract MVP. It has the following features: 

- ERC-20 deposits
- ERC-20 withdrawals
- rail creation
- operator approvals
- operator termination
- rail payment modification
- rail lockup modification
- lazy rail settlement
- proxy pattern for contract upgrade
- arbitration
- rate change queuing
- avoiding debt creation during lockup or rate modification


All rail modifications and creation respect the operator approval.

The following features which will be added in a subsequent PR:

1) Escrow
2) Payment Fees


Diff:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..350f8c1f
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
+name: Makefile CI
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    branches: ["main"]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install Foundry
+        uses: foundry-rs/foundry-toolchain@v1
+        with:
+          version: nightly
+
+      - name: Install Dependencies
+        run: |
+          forge install
+
+      - name: Add foundry to the PATH
+        run: |
+          echo "$HOME/.config/.foundry/bin" >> "$GITHUB_PATH"
+
+      - name: Run build
+        run: |
+          make build
+
+      - name: Run tests
+        run: |
+          make test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..85198aaa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# Compiler files
+cache/
+out/
+
+# Ignores development broadcast logs
+!/broadcast
+/broadcast/*/31337/
+/broadcast/**/dry-run/
+
+# Docs
+docs/
+
+# Dotenv file
+.env
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..9296efd5
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "lib/forge-std"]
+	path = lib/forge-std
+	url = https://github.com/foundry-rs/forge-std
+[submodule "lib/openzeppelin-contracts"]
+	path = lib/openzeppelin-contracts
+	url = https://github.com/OpenZeppelin/openzeppelin-contracts
+[submodule "lib/openzeppelin-contracts-upgradeable"]
+	path = lib/openzeppelin-contracts-upgradeable
+	url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..44bc4879
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. 
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..1ac7abdf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+# Makefile for Payment Contracts
+
+# Default target
+.PHONY: default
+default: build test
+
+# All target including installation
+.PHONY: all
+all: install build test
+
+# Install dependencies
+.PHONY: install
+install:
+	forge install
+
+# Build target
+.PHONY: build
+build:
+	forge build
+
+# Test target
+.PHONY: test
+test:
+	forge test -vv
diff --git a/README.md b/README.md
index 4db6d0f9..e0481aa1 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1 @@
-# payments
-
-WIP
\ No newline at end of file
+### TODO README
diff --git a/foundry.toml b/foundry.toml
new file mode 100644
index 00000000..2d206a4a
--- /dev/null
+++ b/foundry.toml
@@ -0,0 +1,15 @@
+[profile.default]
+src = 'src'
+test = 'test'
+script = 'script'
+out = 'out'
+libs = ['lib']
+cache_path = 'cache'
+solc = "0.8.23"
+
+# For dependencies
+remappings = [
+    '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
+    '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/',
+    'forge-std/=lib/forge-std/src/',
+]
diff --git a/lib/forge-std b/lib/forge-std
new file mode 160000
index 00000000..3b20d60d
--- /dev/null
+++ b/lib/forge-std
@@ -0,0 +1 @@
+Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
new file mode 160000
index 00000000..441dc141
--- /dev/null
+++ b/lib/openzeppelin-contracts
@@ -0,0 +1 @@
+Subproject commit 441dc141ac99622de7e535fa75dfc74af939019c
diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable
new file mode 160000
index 00000000..266b24b1
--- /dev/null
+++ b/lib/openzeppelin-contracts-upgradeable
@@ -0,0 +1 @@
+Subproject commit 266b24b1338f88281040cab1e805f96795d59d3e
diff --git a/src/ERC1967Proxy.sol b/src/ERC1967Proxy.sol
new file mode 100644
index 00000000..bd5b3ed4
--- /dev/null
+++ b/src/ERC1967Proxy.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+contract PaymentsERC1967Proxy is ERC1967Proxy {
+    constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {}
+}
diff --git a/src/Payments.sol b/src/Payments.sol
new file mode 100644
index 00000000..41a125f0
--- /dev/null
+++ b/src/Payments.sol
@@ -0,0 +1,707 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+        bool isLocked; // Indicates if the rail is currently being modified
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(!rails[railId].isLocked, "Modification already in progress");
+        rails[railId].isLocked = true;
+        _;
+        rails[railId].isLocked = false;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Implement Me
+    // https://github.com/FilOzone/payments/pull/1/files#r1974415624
+    function terminateOperator(address operator) external {}
+
+    function deposit(
+        address token,
+        address to,
+        uint256 amount
+    ) external nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external nonReentrant {
+        withdrawTo(token, msg.sender, amount);
+    }
+
+    function withdrawTo(
+        address token,
+        address to,
+        uint256 amount
+    ) public nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "recipient address cannot be zero");
+
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(to, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        require(approval.isApproved, "operator not approved");
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            // handle multiple rate changes by the operator in a single epoch
+            // by queueuing the previous rate only once
+            if (
+                rail.rateChangeQueue.isEmpty() ||
+                rail.rateChangeQueue.peek().untilEpoch != block.number
+            ) {
+                rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+            }
+        }
+
+        require(
+            rail.lockupFixed >= oneTimePayment,
+            "one time payment cannot be greater than rail lockupFixed"
+        );
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(
+            payer.lockupRate >= oldRate,
+            "payer lockup rate cannot be less than old rate"
+        );
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(
+            payer.lockupCurrent >=
+                ((oldRate * rail.lockupPeriod) + oneTimePayment),
+            "failed to modify rail payment: insufficient current lockup"
+        );
+        payer.lockupCurrent =
+            payer.lockupCurrent -
+            (oldRate * rail.lockupPeriod) +
+            (newRate * rail.lockupPeriod) -
+            oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer lockup cannot exceed funds"
+        );
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(
+                payer.funds >= oneTimePayment,
+                "insufficient funds for one-time payment"
+            );
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldTotalLockup -
+            newTotalLockup;
+
+        require(
+            newRate <= oldRate + approval.rateAllowance,
+            "new rate exceeds operator rate allowance"
+        );
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+    }
+
+    function settleRail(
+        uint256 railId,
+        uint256 untilEpoch
+    )
+        public
+        validateRailActive(railId)
+        nonReentrant
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch,
+            string memory note
+        )
+    {
+        require(
+            untilEpoch <= block.number,
+            "failed to settle: cannot settle future epochs"
+        );
+
+        Rail storage rail = rails[railId];
+
+        // Update the payer's lockup to account for elapsed time
+        Account storage payer = accounts[rail.token][rail.from];
+        settleAccountLockup(payer);
+
+        uint256 maxSettlementEpoch = min(
+            untilEpoch,
+            payer.lockupLastSettledAt + rail.lockupPeriod
+        );
+
+        uint256 startEpoch = rail.settledUpTo;
+
+        // nothing to settle (already settled or zero-duration)
+        if (startEpoch >= maxSettlementEpoch) {
+            return (0, startEpoch, "already settled upto requested epoch");
+        }
+
+        // for zero rate rails with empty queue, just advance the settlement epoch
+        // without transferring funds
+        uint256 currentRate = rail.paymentRate;
+        if (currentRate == 0 && rail.rateChangeQueue.isEmpty()) {
+            rail.settledUpTo = maxSettlementEpoch;
+            return (0, maxSettlementEpoch, "zero rate payment rail");
+        }
+
+        // Process settlement depending on whether rate changes exist
+        if (rail.rateChangeQueue.isEmpty()) {
+            (uint256 amount, string memory segmentNote) = _settleSegment(
+                railId,
+                startEpoch,
+                maxSettlementEpoch,
+                currentRate
+            );
+
+            require(rail.settledUpTo > startEpoch, "No progress in settlement");
+            return (amount, rail.settledUpTo, segmentNote);
+        } else {
+            return
+                _settleWithRateChanges(
+                    railId,
+                    currentRate,
+                    startEpoch,
+                    maxSettlementEpoch
+                );
+        }
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    )
+        internal
+        returns (uint256 totalSettled, uint256 finalEpoch, string memory note)
+    {
+        Rail storage rail = rails[railId];
+        RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue;
+
+        totalSettled = 0;
+        uint256 processedEpoch = startEpoch;
+        note = "";
+
+        // Process each segment until we reach the target epoch or hit an early exit condition
+        while (processedEpoch < targetEpoch) {
+            // Default boundary is the target we want to reach
+            uint256 segmentEndBoundary = targetEpoch;
+            uint256 segmentRate;
+
+            // If we have rate changes in the queue, use the rate from the next change
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextRateChange = rateQueue
+                    .peek();
+
+                // Validate rate change queue consistency
+                require(
+                    nextRateChange.untilEpoch >= processedEpoch,
+                    "rate queue is in an invalid state"
+                );
+
+                // Boundary is the minimum of our target or the next rate change epoch
+                segmentEndBoundary = min(
+                    targetEpoch,
+                    nextRateChange.untilEpoch
+                );
+                segmentRate = nextRateChange.rate;
+            } else {
+                // If queue is empty, use the current rail rate
+                segmentRate = currentRate;
+
+                // if current rate is zero, there's nothing left to do and we've finished settlement
+                if (segmentRate == 0) {
+                    rail.settledUpTo = targetEpoch;
+                    return (
+                        totalSettled,
+                        rail.settledUpTo,
+                        "Zero rate payment rail"
+                    );
+                }
+            }
+
+            // Settle the current segment with potentially arbitrated outcomes
+            (
+                uint256 segmentAmount,
+                string memory arbitrationNote
+            ) = _settleSegment(
+                    railId,
+                    processedEpoch,
+                    segmentEndBoundary,
+                    segmentRate
+                );
+
+            // If arbiter returned no progress, exit early without updating state
+            if (rail.settledUpTo <= processedEpoch) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Add the settled amount to our running total
+            totalSettled += segmentAmount;
+
+            // If arbiter partially settled the segment, exit early
+            if (rail.settledUpTo < segmentEndBoundary) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Successfully settled full segment, update tracking values
+            processedEpoch = rail.settledUpTo;
+            note = arbitrationNote;
+
+            // Remove the processed rate change from the queue
+            if (!rateQueue.isEmpty()) {
+                rateQueue.dequeue();
+            }
+        }
+
+        // We've successfully settled up to the target epoch
+        return (totalSettled, rail.settledUpTo, note);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, string memory note) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        // Calculate the default settlement values (without arbitration)
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        uint256 settledUntilEpoch = epochEnd;
+        note = "";
+
+        // If this rail has an arbiter, let it decide on the final settlement amount
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            // 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"
+            );
+        }
+
+        // Verify payer has sufficient funds for the settlement
+        require(
+            payer.funds >= settledAmount,
+            "failed to settle: insufficient funds to cover settlement"
+        );
+
+        // Verify payer has sufficient lockup for the settlement
+        require(
+            payer.lockupCurrent >= settledAmount,
+            "failed to settle: insufficient lockup to cover settlement"
+        );
+
+        // Transfer funds from payer to payee
+        payer.funds -= settledAmount;
+        payee.funds += settledAmount;
+
+        // Reduce the lockup by the settled amount
+        payer.lockupCurrent -= settledAmount;
+
+        // Update the rail's settled epoch
+        rail.settledUpTo = settledUntilEpoch;
+
+        // Invariant check: lockup should never exceed funds
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "failed to settle: insufficient funds to cover lockup after settlement"
+        );
+
+        return (settledAmount, note);
+    }
+
+    function min(uint256 a, uint256 b) public pure returns (uint256) {
+        return a < b ? a : b;
+    }
+
+    function settleAccountLockup(
+        Account storage account
+    ) internal returns (bool fullySettled, uint256 settledUpto) {
+        uint256 currentEpoch = block.number;
+        uint256 elapsedTime = currentEpoch - account.lockupLastSettledAt;
+
+        if (elapsedTime <= 0) {
+            return (true, account.lockupLastSettledAt);
+        }
+
+        if (account.lockupRate == 0) {
+            account.lockupLastSettledAt = currentEpoch;
+            return (true, currentEpoch);
+        }
+
+        uint256 additionalLockup = account.lockupRate * elapsedTime;
+
+        if (account.funds >= account.lockupCurrent + additionalLockup) {
+            // If sufficient, apply full lockup
+            account.lockupCurrent += additionalLockup;
+            account.lockupLastSettledAt = currentEpoch;
+            return (true, currentEpoch);
+        } else {
+            // If insufficient, calculate the fractional epoch where funds became insufficient
+            uint256 availableFunds = account.funds > account.lockupCurrent
+                ? account.funds - account.lockupCurrent
+                : 0;
+
+            if (availableFunds == 0) {
+                return (false, account.lockupLastSettledAt);
+            }
+
+            // Round down to the nearest whole epoch
+            uint256 fractionalEpochs = availableFunds / account.lockupRate;
+            settledUpto = account.lockupLastSettledAt + fractionalEpochs;
+
+            // Apply lockup up to this point
+            account.lockupCurrent += account.lockupRate * fractionalEpochs;
+            account.lockupLastSettledAt = settledUpto;
+            return (false, settledUpto);
+        }
+    }
+}
diff --git a/src/RateChangeQueue.sol b/src/RateChangeQueue.sol
new file mode 100644
index 00000000..4b8f9527
--- /dev/null
+++ b/src/RateChangeQueue.sol
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+library RateChangeQueue {
+    struct RateChange {
+        uint256 rate;
+        uint256 untilEpoch;
+    }
+
+    struct Queue {
+        // Map from index to RateChange
+        mapping(uint256 => RateChange) changes;
+        uint256 head;
+        uint256 tail;
+    }
+
+    function enqueue(Queue storage queue, uint256 rate, uint256 untilEpoch) internal {
+        queue.changes[queue.tail] = RateChange(rate, untilEpoch);
+        queue.tail++;
+    }
+
+    function dequeue(Queue storage queue) internal returns (RateChange memory) {
+        require(queue.head < queue.tail, "Queue is empty");
+        RateChange memory change = queue.changes[queue.head];
+        delete queue.changes[queue.head];
+        queue.head++;
+        return change;
+    }
+
+    function peek(Queue storage queue) internal view returns (RateChange memory) {
+        require(queue.head < queue.tail, "Queue is empty");
+        return queue.changes[queue.head];
+    }
+
+    function isEmpty(Queue storage queue) internal view returns (bool) {
+        return queue.head >= queue.tail;
+    }
+
+    function clear(Queue storage queue) internal {
+        while (!isEmpty(queue)) {
+            dequeue(queue);
+        }
+        queue.head = 0;
+        queue.tail = 0;
+    }
+
+    function size(Queue storage queue) internal view returns (uint256) {
+        return queue.tail - queue.head;
+    }
+}
diff --git a/test/ERC1967Proxy.t.sol b/test/ERC1967Proxy.t.sol
new file mode 100644
index 00000000..4c24a7e5
--- /dev/null
+++ b/test/ERC1967Proxy.t.sol
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {Payments} from "../src/Payments.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+contract ERC1967ProxyTest is Test {
+    Payments public implementation;
+    Payments public proxy;
+    address owner = address(0x123);
+
+    function setUp() public {
+        // Set owner for testing
+        vm.startPrank(owner);
+        // Deploy implementation contract
+        implementation = new Payments();
+
+        // Deploy proxy pointing to implementation
+        bytes memory initData = abi.encodeWithSelector(
+            Payments.initialize.selector
+        );
+
+        ERC1967Proxy proxyContract = new ERC1967Proxy(
+            address(implementation),
+            initData
+        );
+
+        // Get Payments interface on proxy address
+        proxy = Payments(address(proxyContract));
+    }
+
+    function testInitialSetup() public view {
+        assertEq(proxy.owner(), owner);
+    }
+
+    function assertImplementationEquals(address checkImpl) public view {
+        bytes32 implementationSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        assertEq(
+            address(
+                uint160(uint256(vm.load(address(proxy), implementationSlot)))
+            ),
+            address(checkImpl)
+        );
+    }
+
+    function testUpgradeImplementation() public {
+        assertImplementationEquals(address(implementation));
+
+        // Deploy new implementation
+        Payments newImplementation = new Payments();
+
+        // Upgrade proxy to new implementation
+        proxy.upgradeToAndCall(address(newImplementation), "");
+
+        // Verify upgrade was successful
+        assertImplementationEquals(address(newImplementation));
+        assertEq(proxy.owner(), owner); // Owner is preserved
+    }
+
+    function test_RevertWhen_UpgradeFromNonOwner() public {
+        Payments newImplementation = new Payments();
+
+        vm.stopPrank();
+        vm.startPrank(address(0xdead));
+
+        vm.expectRevert(
+            abi.encodeWithSignature(
+                "OwnableUnauthorizedAccount(address)",
+                address(0xdead)
+            )
+        );
+        proxy.upgradeToAndCall(address(newImplementation), "");
+        assertEq(proxy.owner(), owner); // Owner is preserved
+    }
+
+    function testOwnershipTransfer() public {
+        vm.stopPrank();
+        vm.startPrank(owner);
+        // Verify initial owner
+        assertEq(proxy.owner(), owner);
+
+        address newOwner = address(0x456);
+
+        // Transfer ownership
+        proxy.transferOwnership(newOwner);
+
+        // Verify ownership changed
+        assertEq(proxy.owner(), newOwner);
+    }
+
+    function test_RevertWhen_TransferFromNonOwner() public {
+        // Switch to non-owner account
+        vm.stopPrank();
+        vm.startPrank(address(0xdead));
+
+        address newOwner = address(0x456);
+
+        // Attempt transfer should fail
+        vm.expectRevert(
+            abi.encodeWithSignature(
+                "OwnableUnauthorizedAccount(address)",
+                address(0xdead)
+            )
+        );
+        proxy.transferOwnership(newOwner);
+
+        // Verify owner unchanged
+        assertEq(proxy.owner(), owner);
+    }
+}


Reviews:
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T01:14:35Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T02:22:50Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T08:50:07Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T20:21:12Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T19:20:12Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T19:45:51Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T19:46:31Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T19:46:54Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T19:59:59Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T20:00:46Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T20:02:20Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T20:21:44Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-18T22:11:21Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T03:44:36Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T06:25:23Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T06:38:58Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:07:39Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:08:30Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:09:17Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:14:31Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:15:38Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:16:21Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:18:27Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:21:13Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:21:27Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:23:28Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:33:54Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:34:17Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:34:21Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T08:41:08Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:21:44Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:24:37Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:26:58Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:29:18Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:31:13Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:38:51Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:41:51Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:45:05Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:46:19Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T16:50:26Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:03:25Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:10:37Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:14:10Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:14:52Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:16:25Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:17:23Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:18:15Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:21:10Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:23:23Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:24:14Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:25:27Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:28:39Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:29:59Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:33:27Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:34:36Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:35:25Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:37:05Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:38:51Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:39:32Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:43:45Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:45:17Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:49:39Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:50:52Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:51:26Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:51:33Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:52:23Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:54:15Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:57:28Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T17:57:40Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T18:34:44Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T18:35:19Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T18:46:14Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T20:32:20Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:32:38Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:51:18Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:53:39Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:57:34Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T09:26:37Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:55:22Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:56:22Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:56:48Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:57:01Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:57:25Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:58:18Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:58:36Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:58:44Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:59:01Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:59:07Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:59:13Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:59:18Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T11:59:28Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T23:12:39Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T01:50:28Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T01:56:50Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T01:59:35Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:00:37Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:10:18Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:10:27Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:10:34Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:10:45Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:11:49Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:11:54Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:18:53Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:27:07Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T02:29:15Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T03:00:40Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T03:29:15Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T03:30:15Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T03:31:32Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:09:14Z
user: login: Stebalien
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:08:06Z
user: login: Stebalien
body: LGTM. There are some optimizations we can make, but they aren't required for correctness.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T01:21:00Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:21:58Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:23:50Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:24:42Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:25:54Z
user: login: aarshkshah1992
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:29:21Z

Review comments:
diff_hunk: @@ -1,3 +1,66 @@
-# payments
+## Foundry
path: README.md
user: login: Stebalien
body: Revert this change?
created_at: 2025-02-13T01:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,418 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+
+    struct Account {
+        address owner;           // allowed to operate on the account
+        uint256 funds;          // amount of funds in the account
+        uint256 lockupBase;     // locked funds (always non-negative)
+        uint256 lockupRate;     // rate at which funds are locked (always non-negative)
+        bool hasLockupStarted;
+        uint256 lockupStart;    // epoch at which the lockup rate begins to apply
+        uint256 lockupInsufficientSince;  // epoch when account stopped having enough locked funds
+    }
+
+    struct Rail {
+        bool isRateSet;
+        address token;          // token being used for payment
+        address from;           // payer address
+        address to;            // payee address
+        address operator;      // operator address (typically the market contract)
+        address arbiter;       // optional arbiter address for payment validation
+        uint256 paymentRate;   // rate at which this rail pays the payee
+        uint256 lockupPeriod;  // time into the future up-to-which funds will always be locked
+        uint256 lockupFixed;   // fixed amount of locked funds
+        uint256 lastSettledAt; // epoch at which the rail was last settled
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer; // optional arbitrer address approved for payment validation by the client
+        uint256 maxRate;    // max rate at which operator can establish payments
+        uint256 maxBase;    // amount operator is allowed to spend outside of rate
+        uint256 rate_used;
+        uint256 base_used;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailExists(uint256 railId) {
+        require(rails[railId].from != address(0), "Rail does not exist");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "Rail does not exist");
+        require(accounts[rail.token][rail.from].owner != address(0), "From account does not exist");
+        require(accounts[rail.token][rail.to].owner != address(0), "To account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "Only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].owner;
+        require(owner != address(0), "Account does not exist");
+        require(owner == msg.sender, "Not account owner");
+        _;
+    }
+
+    /// @notice Approves or modifies approval for an operator to create and manage payment rails
+    /// @dev This sets approval limits for new rails and rail modifications going forward.
+    /// When reducing approvals, existing rails continue operating under their original
+    /// terms (i.e. existing rails are grandfathered).
+    /// However, any modifications to existing rails must fit within these new approval limits.
+    /// @dev Approval tracking works as follows:
+    /// - New rails check against current maxRate/maxBase minus rate_used/base_used
+    /// - Rail modifications (e.g. rate increases) also check against these current limits
+    /// - Existing unmodified rails continue with their original terms
+    /// This allows users to reduce exposure while honoring existing commitments
+    /// @param token The ERC20 token address this approval is for
+    /// @param operator The address being approved to create/modify rails
+    /// @param arbiter Optional address that can validate payments (0x0 for none)
+    /// @param maxRate Maximum rate at which the sum of all rails operated by this operator can pay out.
+    /// Payments made via rail payment rates count against this limit. Unused rate does not accumulate.
+    /// @param maxBase Maximum amount operator can spend outside of rate-based payments. This covers:
+    /// 1) Lockup amounts (sum of rail.rate * rail.lockup_period + rail.lockup_fixed for all operator rails).
+    /// Lockup modifications count against but do not modify base.
+    /// 2) One-time payments made via ModifyRailPayment's 'once' parameter.
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 maxRate,
+        uint256 maxBase
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "Token address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.maxRate = maxRate;
+        approval.maxBase = maxBase;
+        approval.isApproved = true;
+    }
+
+    // TODO: Debt handling
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] storage railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail owner");
+
+            settleRail(railIds[i]);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupBase -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.maxRate = 0;
+            approval.maxBase = 0;
+            approval.rate_used = 0;
+            approval.base_used = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: Debt payment ?
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "Token address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(amount > 0, "Amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.owner == address(0)) {
+            account.owner = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+    }
+
+    /// @notice Allows an account owner to withdraw funds from their account.
+    /// @dev This function settles the account's lockup before calculating available funds.
+    /// @dev Withdrawal is only possible if the account has sufficient unlocked funds after accounting
+    /// for lockup.
+    /// @dev The available balance for withdrawal is calculated as:
+    ///      max(0, account.funds - account.lockupBase)
+    /// @dev If the requested amount exceeds available unlocked funds, the transaction will revert
+    /// @param token The address of the ERC20 token to withdraw
+    /// @param amount The amount of tokens to withdraw
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        applyAccumulatedRateLockup(acct);
+
+        uint256 available = acct.funds > acct.lockupBase
+            ? acct.funds - acct.lockupBase
+            : 0;
+
+        require(amount <= available, "Insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    /// @notice Creates a new payment rail between a payer and payee, initiated by an approved operator.
+    /// @dev This function checks that:
+    /// 1. The token, from, to, and operator addresses are all valid (non-zero).
+    /// 2. Both the payer (from) and payee (to) have existing accounts for the given token.
+    /// 3. The payer account has a non-zero balance.
+    /// 4. The operator has been pre-approved by the payer to create rails on their behalf.
+    /// 5. If the operator approval specifies an arbiter, it must match the one passed to this function.
+    /// @param token The ERC20 token to use for payments on this rail.
+    /// @param from The payer account.
+    /// @param to The payee account.
+    /// @param operator The account creating and managing this rail, must be pre-approved by the payer.
+    /// @param arbiter An optional account that can validate payments, must match operator approval if set.
+    /// @return railId The unique ID of the newly created payment rail.
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "Token address cannot be zero");
+        require(from != address(0), "From address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.owner != address(0), "To account does not exist");
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.owner != address(0), "From account does not exist");
+        require(fromAccount.funds > 0, "From account has no funds");
+        require(toAccount.funds > 0, "To account has no funds");
+
+        OperatorApproval storage approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "Operator not approved");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "Arbiter mismatch");
+        }
+
+        uint256 railId = nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.lastSettledAt = block.number;
+
+        rails[railId] = rail;
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 fixedLockup
+        ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        applyAccumulatedRateLockup(payer);
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = fixedLockup + (rail.paymentRate * period);
+
+        require(approval.base_used >= oldLockup, "base used cannot be less than oldLockup");
+        require(approval.base_used - oldLockup + newLockup <= approval.maxBase, "Exceeds operator base approval");
+        require(payer.lockupBase >= oldLockup, "payer lockup base cannot be less than oldLockup");
+
+        // Update base used
+        approval.base_used = approval.base_used - oldLockup + newLockup;
+
+        // Update payer's lockup base
+        payer.lockupBase = payer.lockupBase - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = fixedLockup;
+
+        // Calculate and return deficit if any
+        if (payer.funds < payer.lockupBase) {
+            return payer.lockupBase - payer.funds;
+        }
+        return 0;
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 rate,
+        uint256 once
+    ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the rail before modifying payment
+        // This ensures that all past payments are accounted for before changing the rate
+        settleRail(railId);
+
+        // Calculate the change in rate
+        uint256 oldRate = rail.paymentRate;
+
+        // Check if the new rate exceeds the operator's approval
+        require(approval.rate_used - oldRate + rate <= approval.maxRate, "Exceeds operator rate approval");
+
+        // Update the operator's used rate
+        approval.rate_used = approval.rate_used - oldRate + rate;
+
+        // Update rail payment rate
+        rail.paymentRate = rate;
+
+
+        // Handle one-time payment if specified
+        if (once > 0) {
+            require(approval.base_used + once <= approval.maxBase, "Exceeds operator base approval");
+            require(payer.funds >= once, "Insufficient funds for one-time payment");
+
+            payer.funds -= once;
+            payee.funds += once;
+
+            // Update operator's used base
+            approval.base_used += once;
+        }
+
+        // Update payer's lockup rate and base
+        payer.lockupBase = payer.lockupBase - (oldRate * rail.lockupPeriod) + (rate * rail.lockupPeriod);
+        payer.lockupRate = payer.lockupRate - oldRate + rate;
+
+        // Init hasLockupStarted flag and lockupStart for the first rail with non-zero rate
+        if (rate > 0 && !payer.hasLockupStarted) {
path: src/Payments.sol
user: login: Stebalien
body: Take a look at the "Lockup can be settled..." part of: https://www.notion.so/filecoindev/Payments-Design-Iteration-17ddc41950c180468806c7e0097c2e14?pvs=4#185dc41950c1805da8d4ce567e343307

`lockupStart` is probably misnamed. Really, it means "the last time we settled lockup". I.e., it's when the lockup rate starts applying.

My point is, I don't think we need `hasLockupStarted`. We always adjust `lockupStart`.
created_at: 2025-02-13T01:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,418 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+
+    struct Account {
+        address owner;           // allowed to operate on the account
+        uint256 funds;          // amount of funds in the account
+        uint256 lockupBase;     // locked funds (always non-negative)
+        uint256 lockupRate;     // rate at which funds are locked (always non-negative)
+        bool hasLockupStarted;
+        uint256 lockupStart;    // epoch at which the lockup rate begins to apply
+        uint256 lockupInsufficientSince;  // epoch when account stopped having enough locked funds
+    }
+
+    struct Rail {
+        bool isRateSet;
+        address token;          // token being used for payment
+        address from;           // payer address
+        address to;            // payee address
+        address operator;      // operator address (typically the market contract)
+        address arbiter;       // optional arbiter address for payment validation
+        uint256 paymentRate;   // rate at which this rail pays the payee
+        uint256 lockupPeriod;  // time into the future up-to-which funds will always be locked
+        uint256 lockupFixed;   // fixed amount of locked funds
+        uint256 lastSettledAt; // epoch at which the rail was last settled
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer; // optional arbitrer address approved for payment validation by the client
+        uint256 maxRate;    // max rate at which operator can establish payments
+        uint256 maxBase;    // amount operator is allowed to spend outside of rate
+        uint256 rate_used;
+        uint256 base_used;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailExists(uint256 railId) {
+        require(rails[railId].from != address(0), "Rail does not exist");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "Rail does not exist");
+        require(accounts[rail.token][rail.from].owner != address(0), "From account does not exist");
+        require(accounts[rail.token][rail.to].owner != address(0), "To account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "Only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].owner;
+        require(owner != address(0), "Account does not exist");
+        require(owner == msg.sender, "Not account owner");
+        _;
+    }
+
+    /// @notice Approves or modifies approval for an operator to create and manage payment rails
+    /// @dev This sets approval limits for new rails and rail modifications going forward.
+    /// When reducing approvals, existing rails continue operating under their original
+    /// terms (i.e. existing rails are grandfathered).
+    /// However, any modifications to existing rails must fit within these new approval limits.
+    /// @dev Approval tracking works as follows:
+    /// - New rails check against current maxRate/maxBase minus rate_used/base_used
+    /// - Rail modifications (e.g. rate increases) also check against these current limits
+    /// - Existing unmodified rails continue with their original terms
+    /// This allows users to reduce exposure while honoring existing commitments
+    /// @param token The ERC20 token address this approval is for
+    /// @param operator The address being approved to create/modify rails
+    /// @param arbiter Optional address that can validate payments (0x0 for none)
+    /// @param maxRate Maximum rate at which the sum of all rails operated by this operator can pay out.
+    /// Payments made via rail payment rates count against this limit. Unused rate does not accumulate.
+    /// @param maxBase Maximum amount operator can spend outside of rate-based payments. This covers:
+    /// 1) Lockup amounts (sum of rail.rate * rail.lockup_period + rail.lockup_fixed for all operator rails).
+    /// Lockup modifications count against but do not modify base.
+    /// 2) One-time payments made via ModifyRailPayment's 'once' parameter.
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 maxRate,
+        uint256 maxBase
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "Token address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.maxRate = maxRate;
+        approval.maxBase = maxBase;
+        approval.isApproved = true;
+    }
+
+    // TODO: Debt handling
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] storage railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail owner");
+
+            settleRail(railIds[i]);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupBase -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.maxRate = 0;
+            approval.maxBase = 0;
+            approval.rate_used = 0;
+            approval.base_used = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: Debt payment ?
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "Token address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(amount > 0, "Amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.owner == address(0)) {
+            account.owner = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+    }
+
+    /// @notice Allows an account owner to withdraw funds from their account.
+    /// @dev This function settles the account's lockup before calculating available funds.
+    /// @dev Withdrawal is only possible if the account has sufficient unlocked funds after accounting
+    /// for lockup.
+    /// @dev The available balance for withdrawal is calculated as:
+    ///      max(0, account.funds - account.lockupBase)
+    /// @dev If the requested amount exceeds available unlocked funds, the transaction will revert
+    /// @param token The address of the ERC20 token to withdraw
+    /// @param amount The amount of tokens to withdraw
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        applyAccumulatedRateLockup(acct);
+
+        uint256 available = acct.funds > acct.lockupBase
+            ? acct.funds - acct.lockupBase
+            : 0;
+
+        require(amount <= available, "Insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    /// @notice Creates a new payment rail between a payer and payee, initiated by an approved operator.
+    /// @dev This function checks that:
+    /// 1. The token, from, to, and operator addresses are all valid (non-zero).
+    /// 2. Both the payer (from) and payee (to) have existing accounts for the given token.
+    /// 3. The payer account has a non-zero balance.
+    /// 4. The operator has been pre-approved by the payer to create rails on their behalf.
+    /// 5. If the operator approval specifies an arbiter, it must match the one passed to this function.
+    /// @param token The ERC20 token to use for payments on this rail.
+    /// @param from The payer account.
+    /// @param to The payee account.
+    /// @param operator The account creating and managing this rail, must be pre-approved by the payer.
+    /// @param arbiter An optional account that can validate payments, must match operator approval if set.
+    /// @return railId The unique ID of the newly created payment rail.
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "Token address cannot be zero");
+        require(from != address(0), "From address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.owner != address(0), "To account does not exist");
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.owner != address(0), "From account does not exist");
+        require(fromAccount.funds > 0, "From account has no funds");
+        require(toAccount.funds > 0, "To account has no funds");
+
+        OperatorApproval storage approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "Operator not approved");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "Arbiter mismatch");
+        }
+
+        uint256 railId = nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.lastSettledAt = block.number;
+
+        rails[railId] = rail;
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 fixedLockup
+        ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        applyAccumulatedRateLockup(payer);
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = fixedLockup + (rail.paymentRate * period);
+
+        require(approval.base_used >= oldLockup, "base used cannot be less than oldLockup");
+        require(approval.base_used - oldLockup + newLockup <= approval.maxBase, "Exceeds operator base approval");
+        require(payer.lockupBase >= oldLockup, "payer lockup base cannot be less than oldLockup");
+
+        // Update base used
+        approval.base_used = approval.base_used - oldLockup + newLockup;
+
+        // Update payer's lockup base
+        payer.lockupBase = payer.lockupBase - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = fixedLockup;
+
+        // Calculate and return deficit if any
+        if (payer.funds < payer.lockupBase) {
+            return payer.lockupBase - payer.funds;
+        }
+        return 0;
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 rate,
+        uint256 once
+    ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the rail before modifying payment
+        // This ensures that all past payments are accounted for before changing the rate
+        settleRail(railId);
+
+        // Calculate the change in rate
+        uint256 oldRate = rail.paymentRate;
+
+        // Check if the new rate exceeds the operator's approval
+        require(approval.rate_used - oldRate + rate <= approval.maxRate, "Exceeds operator rate approval");
+
+        // Update the operator's used rate
+        approval.rate_used = approval.rate_used - oldRate + rate;
+
+        // Update rail payment rate
+        rail.paymentRate = rate;
+
+
+        // Handle one-time payment if specified
+        if (once > 0) {
+            require(approval.base_used + once <= approval.maxBase, "Exceeds operator base approval");
+            require(payer.funds >= once, "Insufficient funds for one-time payment");
+
+            payer.funds -= once;
+            payee.funds += once;
+
+            // Update operator's used base
+            approval.base_used += once;
+        }
+
+        // Update payer's lockup rate and base
+        payer.lockupBase = payer.lockupBase - (oldRate * rail.lockupPeriod) + (rate * rail.lockupPeriod);
+        payer.lockupRate = payer.lockupRate - oldRate + rate;
+
+        // Init hasLockupStarted flag and lockupStart for the first rail with non-zero rate
+        if (rate > 0 && !payer.hasLockupStarted) {
path: src/Payments.sol
user: login: Stebalien
body: Ah, I see you've implemented `accumulateLockupRate`. You don't need this code here, you just need to call `accumulateLockupRate` before changing the rate (e.g., at the very top of this function). The first time this is called, that will accumulate nothing because the rate will be 0, but it will initialize `lockupStart`.
created_at: 2025-02-13T02:22:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,418 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+
+    struct Account {
+        address owner;           // allowed to operate on the account
+        uint256 funds;          // amount of funds in the account
+        uint256 lockupBase;     // locked funds (always non-negative)
+        uint256 lockupRate;     // rate at which funds are locked (always non-negative)
+        bool hasLockupStarted;
+        uint256 lockupStart;    // epoch at which the lockup rate begins to apply
+        uint256 lockupInsufficientSince;  // epoch when account stopped having enough locked funds
+    }
+
+    struct Rail {
+        bool isRateSet;
+        address token;          // token being used for payment
+        address from;           // payer address
+        address to;            // payee address
+        address operator;      // operator address (typically the market contract)
+        address arbiter;       // optional arbiter address for payment validation
+        uint256 paymentRate;   // rate at which this rail pays the payee
+        uint256 lockupPeriod;  // time into the future up-to-which funds will always be locked
+        uint256 lockupFixed;   // fixed amount of locked funds
+        uint256 lastSettledAt; // epoch at which the rail was last settled
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer; // optional arbitrer address approved for payment validation by the client
+        uint256 maxRate;    // max rate at which operator can establish payments
+        uint256 maxBase;    // amount operator is allowed to spend outside of rate
+        uint256 rate_used;
+        uint256 base_used;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailExists(uint256 railId) {
+        require(rails[railId].from != address(0), "Rail does not exist");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "Rail does not exist");
+        require(accounts[rail.token][rail.from].owner != address(0), "From account does not exist");
+        require(accounts[rail.token][rail.to].owner != address(0), "To account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "Only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].owner;
+        require(owner != address(0), "Account does not exist");
+        require(owner == msg.sender, "Not account owner");
+        _;
+    }
+
+    /// @notice Approves or modifies approval for an operator to create and manage payment rails
+    /// @dev This sets approval limits for new rails and rail modifications going forward.
+    /// When reducing approvals, existing rails continue operating under their original
+    /// terms (i.e. existing rails are grandfathered).
+    /// However, any modifications to existing rails must fit within these new approval limits.
+    /// @dev Approval tracking works as follows:
+    /// - New rails check against current maxRate/maxBase minus rate_used/base_used
+    /// - Rail modifications (e.g. rate increases) also check against these current limits
+    /// - Existing unmodified rails continue with their original terms
+    /// This allows users to reduce exposure while honoring existing commitments
+    /// @param token The ERC20 token address this approval is for
+    /// @param operator The address being approved to create/modify rails
+    /// @param arbiter Optional address that can validate payments (0x0 for none)
+    /// @param maxRate Maximum rate at which the sum of all rails operated by this operator can pay out.
+    /// Payments made via rail payment rates count against this limit. Unused rate does not accumulate.
+    /// @param maxBase Maximum amount operator can spend outside of rate-based payments. This covers:
+    /// 1) Lockup amounts (sum of rail.rate * rail.lockup_period + rail.lockup_fixed for all operator rails).
+    /// Lockup modifications count against but do not modify base.
+    /// 2) One-time payments made via ModifyRailPayment's 'once' parameter.
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 maxRate,
+        uint256 maxBase
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "Token address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.maxRate = maxRate;
+        approval.maxBase = maxBase;
+        approval.isApproved = true;
+    }
+
+    // TODO: Debt handling
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] storage railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail owner");
+
+            settleRail(railIds[i]);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupBase -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.maxRate = 0;
+            approval.maxBase = 0;
+            approval.rate_used = 0;
+            approval.base_used = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: Debt payment ?
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "Token address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(amount > 0, "Amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.owner == address(0)) {
+            account.owner = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+    }
+
+    /// @notice Allows an account owner to withdraw funds from their account.
+    /// @dev This function settles the account's lockup before calculating available funds.
+    /// @dev Withdrawal is only possible if the account has sufficient unlocked funds after accounting
+    /// for lockup.
+    /// @dev The available balance for withdrawal is calculated as:
+    ///      max(0, account.funds - account.lockupBase)
+    /// @dev If the requested amount exceeds available unlocked funds, the transaction will revert
+    /// @param token The address of the ERC20 token to withdraw
+    /// @param amount The amount of tokens to withdraw
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        applyAccumulatedRateLockup(acct);
+
+        uint256 available = acct.funds > acct.lockupBase
+            ? acct.funds - acct.lockupBase
+            : 0;
+
+        require(amount <= available, "Insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    /// @notice Creates a new payment rail between a payer and payee, initiated by an approved operator.
+    /// @dev This function checks that:
+    /// 1. The token, from, to, and operator addresses are all valid (non-zero).
+    /// 2. Both the payer (from) and payee (to) have existing accounts for the given token.
+    /// 3. The payer account has a non-zero balance.
+    /// 4. The operator has been pre-approved by the payer to create rails on their behalf.
+    /// 5. If the operator approval specifies an arbiter, it must match the one passed to this function.
+    /// @param token The ERC20 token to use for payments on this rail.
+    /// @param from The payer account.
+    /// @param to The payee account.
+    /// @param operator The account creating and managing this rail, must be pre-approved by the payer.
+    /// @param arbiter An optional account that can validate payments, must match operator approval if set.
+    /// @return railId The unique ID of the newly created payment rail.
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "Token address cannot be zero");
+        require(from != address(0), "From address cannot be zero");
+        require(to != address(0), "To address cannot be zero");
+        require(operator != address(0), "Operator address cannot be zero");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.owner != address(0), "To account does not exist");
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.owner != address(0), "From account does not exist");
+        require(fromAccount.funds > 0, "From account has no funds");
+        require(toAccount.funds > 0, "To account has no funds");
+
+        OperatorApproval storage approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "Operator not approved");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "Arbiter mismatch");
+        }
+
+        uint256 railId = nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.lastSettledAt = block.number;
+
+        rails[railId] = rail;
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 fixedLockup
+        ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        applyAccumulatedRateLockup(payer);
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = fixedLockup + (rail.paymentRate * period);
+
+        require(approval.base_used >= oldLockup, "base used cannot be less than oldLockup");
+        require(approval.base_used - oldLockup + newLockup <= approval.maxBase, "Exceeds operator base approval");
+        require(payer.lockupBase >= oldLockup, "payer lockup base cannot be less than oldLockup");
+
+        // Update base used
+        approval.base_used = approval.base_used - oldLockup + newLockup;
+
+        // Update payer's lockup base
+        payer.lockupBase = payer.lockupBase - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = fixedLockup;
+
+        // Calculate and return deficit if any
+        if (payer.funds < payer.lockupBase) {
+            return payer.lockupBase - payer.funds;
+        }
+        return 0;
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 rate,
+        uint256 once
+    ) external validateRailExists(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) returns (uint256) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the rail before modifying payment
+        // This ensures that all past payments are accounted for before changing the rate
+        settleRail(railId);
+
+        // Calculate the change in rate
+        uint256 oldRate = rail.paymentRate;
+
+        // Check if the new rate exceeds the operator's approval
+        require(approval.rate_used - oldRate + rate <= approval.maxRate, "Exceeds operator rate approval");
+
+        // Update the operator's used rate
+        approval.rate_used = approval.rate_used - oldRate + rate;
+
+        // Update rail payment rate
+        rail.paymentRate = rate;
+
+
+        // Handle one-time payment if specified
+        if (once > 0) {
+            require(approval.base_used + once <= approval.maxBase, "Exceeds operator base approval");
+            require(payer.funds >= once, "Insufficient funds for one-time payment");
+
+            payer.funds -= once;
+            payee.funds += once;
+
+            // Update operator's used base
+            approval.base_used += once;
+        }
+
+        // Update payer's lockup rate and base
+        payer.lockupBase = payer.lockupBase - (oldRate * rail.lockupPeriod) + (rate * rail.lockupPeriod);
+        payer.lockupRate = payer.lockupRate - oldRate + rate;
+
+        // Init hasLockupStarted flag and lockupStart for the first rail with non-zero rate
+        if (rate > 0 && !payer.hasLockupStarted) {
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Yeah this makes sense. If `totalLockupRate` is 0, the `lockupstart` does not matter.

I'm fixing naming across this contract now for all state. I don't think these names accurately reflect what they're doing. `lockupBase` for eg dosen't reflect that it's doing three things:

1) Locking up a fixed security deposit
2) Locking up accrued payments for services already taken by the client but not yet settled by the provider
3) Locking up a portion of payments for future services that have yet to be provided to provide some sort of "guarantee" to the provider.
created_at: 2025-02-13T08:50:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,467 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+
+    struct Account {
+        // The address of the account owner.
+        address owner;
+
+        // The total amount of funds in the account, including both locked and unlocked funds.
+        uint256 funds;
+
+        // The required amount of funds that should be locked in the account. This reflects three components:
+        // 1. Fixed lockups from all rails associated with this account.
+        // 2. Accumulated rate-based lockups that have been converted to fixed lockups across all rails.
+        // 3. Funds reserved for future payments based on current payment rates and lockup periods across all rails.
+        uint256 requiredTotalLockedFunds;
path: src/Payments.sol
user: login: Stebalien
body: I've updated the design doc. I've named this "lockup current" because it covers funds that are _actually_ locked, not the current target locked. I didn't use the word required because the _actually_ locked funds may be less than the "target" (required).
created_at: 2025-02-13T20:21:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
path: src/Payments.sol
user: login: Stebalien
body: We're going to need two features WRT account management:

1. A way to efficiently (ish) determine the status of an account. Total funds across all tokens, etc. so we can display it in some webui.
2. Ideally a way to efficiently _change_ the owner address? That's complicated.

Even if we don't support (2), we'll need some top-level account abstraction to support (1).
created_at: 2025-02-18T17:13:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
path: src/Payments.sol
user: login: Stebalien
body: OT: WTF solidity???? Why?! People call rust "sigil soup".
created_at: 2025-02-18T17:17:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
path: src/Payments.sol
user: login: Stebalien
body: Note: IMO this is a good idea but I want to note that it's not _strictly_ necessary.
created_at: 2025-02-18T18:44:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
path: src/Payments.sol
user: login: Stebalien
body: Need to restrict the sender to the account owner. We could also allow operators to do this but... it's probably better to just have the account owner set these up.
created_at: 2025-02-18T18:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: Stebalien
body: Are you sure we want to approve a specific arbiter? I can see the appeal but, in that case... I think we can just let the client decide if they're ok with an arbiter on a per-rail basis.
created_at: 2025-02-18T18:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
path: src/Payments.sol
user: login: Stebalien
body: They don't need funds, do they?
created_at: 2025-02-18T19:02:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
path: src/Payments.sol
user: login: Stebalien
body: Technically, the sender doesn't need any funds either.
created_at: 2025-02-18T19:03:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
path: src/Payments.sol
user: login: Stebalien
body: TBD: need to discuss approved operators and who, precisely, is allowed to call `createRail`. 
created_at: 2025-02-18T19:08:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
path: src/Payments.sol
user: login: Stebalien
body: OT: Ethereum's "everything exists, it just defaults to zero" gets me every time. It's kind of cool but also really disconcerting.
created_at: 2025-02-18T19:10:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien I mean not strictly speaking. We can get rid of this yeah.
created_at: 2025-02-18T19:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Technically no but I'd like to keep this so that rails are only created with clients who have interacted with the contract before. Just think it's better that way.
created_at: 2025-02-18T19:46:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien What's "OT" ?
created_at: 2025-02-18T19:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+# Compiler files
+cache/
+out/
+
+# Ignores development broadcast logs
+!/broadcast
+/broadcast/*/31337/
+/broadcast/**/dry-run/
path: .gitignore
user: login: Stebalien
body: Why not just ignore all of /broadcast?
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
path: src/Payments.sol
user: login: Stebalien
body: We should consider adding an arbitrary "note" field. Could be useful to explain why the full amount isn't approved.
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: Stebalien
body: Do we need `approved`? IMO, we should just revert if we're not approving anything, but I'm not really a solidity expert.
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
path: src/Payments.sol
user: login: Stebalien
body: Let's comment on the units here (and all the other fields). E.g. lockup rate is attotoken/epoch.
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
path: src/Payments.sol
user: login: Stebalien
body: Hm. Now that I see this, I wonder if we should call this "settledUpTo" instead of "lastSettledAt".
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
path: src/Payments.sol
user: login: Stebalien
body: Do we need this field?
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
path: src/Payments.sol
user: login: Stebalien
body: Do we need this field either? Isn't it implicit?
created_at: 2025-02-18T19:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
path: src/Payments.sol
user: login: Stebalien
body: "Off topic". I.e., "nothing to do with my review, ignore this, I just wanted to say it."
created_at: 2025-02-18T20:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
path: src/Payments.sol
user: login: Stebalien
body: Well, this goes back to who can create a rail. IMO, the answer is: the client.

It would be nice if a client can go through the entire signup flow without having to deposit any funds until they actually start using the service.
created_at: 2025-02-18T20:02:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
+            }
+
+            // Settle the segment from currentEpoch up to nextBoundary.
+            (uint256 segmentAmount, uint256 segmentSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                segmentRate
+            );
+
+
+            require(segmentSettledEpoch <= nextBoundary, "segment settled epoch exceeds boundary");
+
+            // Short circuit and return if _settleSegment returns an epoch less than what we sent it
+            if (segmentSettledEpoch < nextBoundary) {
+                return (totalSettled + segmentAmount, segmentSettledEpoch);
+            }
+
+            totalSettled += segmentAmount;
+            currentEpoch = segmentSettledEpoch;
+            rateQueue.dequeue();
+            }
+
+
+        finalEpoch = currentEpoch;
+        return (totalSettled, finalEpoch);
+    }
+
+    /// @notice Returns the minimum of two numbers.
+    function min(uint256 a, uint256 b) internal pure returns (uint256) {
+        return a < b ? a : b;
+    }
path: src/Payments.sol
user: login: Stebalien
body: Make this a free function?
created_at: 2025-02-18T20:21:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
+            }
+
+            // Settle the segment from currentEpoch up to nextBoundary.
+            (uint256 segmentAmount, uint256 segmentSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                segmentRate
+            );
+
+
+            require(segmentSettledEpoch <= nextBoundary, "segment settled epoch exceeds boundary");
+
+            // Short circuit and return if _settleSegment returns an epoch less than what we sent it
+            if (segmentSettledEpoch < nextBoundary) {
+                return (totalSettled + segmentAmount, segmentSettledEpoch);
+            }
+
+            totalSettled += segmentAmount;
+            currentEpoch = segmentSettledEpoch;
+            rateQueue.dequeue();
+            }
path: src/Payments.sol
user: login: Stebalien
body: ```suggestion
        }
```
created_at: 2025-02-18T20:21:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
path: src/Payments.sol
user: login: Stebalien
body: ```suggestion
    }

    function _settleWithRateChanges(
```
created_at: 2025-02-18T20:21:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
path: src/Payments.sol
user: login: Stebalien
body: ```suggestion
        // assert that operator allowance is respected
```
created_at: 2025-02-18T20:21:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
path: src/Payments.sol
user: login: Stebalien
body: We need to validate that:

1. `arbResult.settledUntil <= settledUntil`.
2. `arbResult.modifiedAmount <= rate * (arbResult.settledUntil - segmentStart)`.
created_at: 2025-02-18T22:11:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
path: src/Payments.sol
user: login: Stebalien
body: Need to check that `untilEpoch` is at or before the current epoch.
created_at: 2025-02-18T22:11:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
path: src/Payments.sol
user: login: Stebalien
body: I've spent a lot of time staring at this code and I'm still not sure if it's right. Let's talk through it tomorrow.
created_at: 2025-02-19T03:44:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien This is for the Arbitrer contract to gracefully let us know that it is/is not rejecting the payment. 

The Payment contract does revert the settlement if it sees a disapproval from the Arbitrer but doing it this way lets us decide what we wanna do when we see a rejection from the Arbitrer.

We can change this so that the Arbitrer reverts rather than returning this `approved ` flag and then handle the revert in the Payment contract as follows:

```
// Low-level call with error handling
(bool success, bytes memory returnData) = IArbitrer(Y).call(....);
if (!success) {
    // Handle the error
}
```

but we need to write some additional encoding/decoding code here for serialisation.

No opinion either way. Wdyt ?
created_at: 2025-02-19T06:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: aarshkshah1992
body: The other option is to not gracefully handle any reverts from the `Arbitrer` contract at all and simply revert right away (in Solidity, when contract X calls contract Y and contract Y reverts -> the call to X reverts right away unless you handle it as above).

The way we've implemented it here -> any revert from `Arbitrer` will bubble up right away and the settlement will revert but atleast for payment refusals -> we get to handle it gracefully and do more stuff if we want to before we revert.
created_at: 2025-02-19T06:38:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
path: src/Payments.sol
user: login: aarshkshah1992
body: Added this and also now returning this to the caller of `settleRail`
created_at: 2025-02-19T07:07:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Yeah it's the easiest way to track the fact that an operator has been disapproved. Solidity HashMap/Array deletes aren't clean/gas cheap to do.
created_at: 2025-02-19T07:08:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien My idea was to have the client have a say in who gets to be an arbitrer.
created_at: 2025-02-19T07:09:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien 

Can't we implement those getters as view functions that can be invoked off-chain on RPC providers ? We can add some state to the contract that makes those view/getter functions slightly less compute intensive but IMO making it a view function that can be invoked off-chain means we can offload the worrying about efficiency to client <> RPC provider relationships without having to add too much state to the contract.
created_at: 2025-02-19T07:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Yeah plan is to raise a subsequent PR for comprehensive docs for both state and functions once we've locked in a meaningful draft.
created_at: 2025-02-19T07:15:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien You mean the client creates the rail OR that a client has to approve a rail that has been created by an operator ? Both have different UX implications.
created_at: 2025-02-19T07:16:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien It does make the UX harder for clients and operators. It's more intuitive for an operator to create a rail as they know who the provider of the service is etc (without always leaking that to the client).

If we get clients to create a rail -> we need to add a step where the operator has to approve the rail and the operator needs to wait/watch for rail creation before they can start providing services. It really does complicate the entire flow.
created_at: 2025-02-19T07:18:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
path: src/Payments.sol
user: login: aarshkshah1992
body: I agree. It is involved.
created_at: 2025-02-19T07:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
path: src/Payments.sol
user: login: aarshkshah1992
body: Let me try to simplify it more today before we sync up.
created_at: 2025-02-19T07:21:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
path: src/Payments.sol
user: login: aarshkshah1992
body: For 1, it should be `arbResult.settledUntil <=segmentEnd`, right ? Yeah making this change.
created_at: 2025-02-19T07:23:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: aarshkshah1992
body: See my comment at https://github.com/FilOzone/payments/pull/1/files#r1961082248.
created_at: 2025-02-19T07:33:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-19T07:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-19T07:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
path: src/Payments.sol
user: login: aarshkshah1992
body: Also renamed the corresponding `ArbitrationResult` field to `settledUpTo`
created_at: 2025-02-19T08:41:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: aarshkshah1992
body: The Arbitrer MUST revert if it wants to reject payment. 
created_at: 2025-02-19T16:21:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: aarshkshah1992
body: if arbitrer wants to reject and NOT revert, you can just set ` settleUpto` to the start epoch.
created_at: 2025-02-19T16:24:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
path: src/Payments.sol
user: login: aarshkshah1992
body: Can leave a note if successful too. For better UX, it's a catch all field for arbitrer to send us info.
created_at: 2025-02-19T16:26:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: aarshkshah1992
body: An operator can have many different arbitrers for different rails.
created_at: 2025-02-19T16:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
path: src/Payments.sol
user: login: aarshkshah1992
body: Do we really need this ?
created_at: 2025-02-19T16:31:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: aarshkshah1992
body: So remove this.
created_at: 2025-02-19T16:38:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
path: src/Payments.sol
user: login: aarshkshah1992
body: The answer here is to trust the operator.
created_at: 2025-02-19T16:41:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
path: src/Payments.sol
user: login: aarshkshah1992
body: We agree that the operator creates the rails.
created_at: 2025-02-19T16:45:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
path: src/Payments.sol
user: login: aarshkshah1992
body: we don't need this as the `msg.sender` will/should ALWAYS be the operator.
created_at: 2025-02-19T16:46:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
path: src/Payments.sol
user: login: Stebalien
body: You're right, let's let the operator do this.
created_at: 2025-02-19T16:49:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
path: src/Payments.sol
user: login: aarshkshah1992
body: We agree that neither `from` or `to` needs funds.
created_at: 2025-02-19T16:50:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
path: src/Payments.sol
user: login: Stebalien
body: Ok, we can solve (1) by:

1. Adding a `mapping(address => address[])` of account owners to tokens.
2. Optionally: changing `token => owner => Account` to `owner => token => Account`. This isn't strictly necessary, but it mirrors the mapping in (1).

As discussed, we can drop (2) and let the user handle indirection through smart contracts. 
created_at: 2025-02-19T17:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
path: src/Payments.sol
user: login: Stebalien
body: Actually, maybe we want a single `owner => Account` mapping where `Account` contains:

1. The client's operator approvals (map of `operator => Approval`).
2. An array of approved client operators.
3. An array of client rail IDs.
4. etc...
created_at: 2025-02-19T17:03:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
path: src/Payments.sol
user: login: aarshkshah1992
body: who calls this ?

answer: always the client.
created_at: 2025-02-19T17:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
path: src/Payments.sol
user: login: Stebalien
body: Resolution: we're still exploring the API, let's build that first then figure out what these data structures should look at.
created_at: 2025-02-19T17:14:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Please can you review this function ?
created_at: 2025-02-19T17:14:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: aarshkshah1992
body: Write down all the `getter` APIs. That will determine state design.
created_at: 2025-02-19T17:16:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: aarshkshah1992
body: Try to make it as easy as possible to access basic state without an Indexer.
created_at: 2025-02-19T17:17:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: aarshkshah1992
body: Strike a balance between on-chain state and off-chain compute than an RPC provider needs to do to serve Getter requests.
created_at: 2025-02-19T17:18:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: aarshkshah1992
body: Storing the keys of a Hasmap in an a set if you need iteration. Do it only when needed.
created_at: 2025-02-19T17:21:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
path: src/Payments.sol
user: login: aarshkshah1992
body: An operator can always do this by setting everything to 0.
created_at: 2025-02-19T17:23:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
path: src/Payments.sol
user: login: aarshkshah1992
body: If a client wants to terminate a rail -> they go the operator and get them to modify the rates and lockups for the rail to 0.
created_at: 2025-02-19T17:24:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
path: src/Payments.sol
user: login: aarshkshah1992
body: We need this to track rail termination !
created_at: 2025-02-19T17:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
path: src/Payments.sol
user: login: aarshkshah1992
body: can read ` payer.lockupLastSettledAt` from return value.
created_at: 2025-02-19T17:28:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
path: src/Payments.sol
user: login: aarshkshah1992
body: We can put the `railRateChangeQueues ` in the `Rail` itself.
created_at: 2025-02-19T17:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
path: src/Payments.sol
user: login: Stebalien
body: Need to support settling nothing to handle partial settlements. E.g., we're going through the queue settling segment by segment and then we hit a segment where we can settle nothing. We need to accept that and settle everything so far without reverting.
created_at: 2025-02-19T17:33:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
path: src/Payments.sol
user: login: aarshkshah1992
body: this is the current rail rate. fix naming.
created_at: 2025-02-19T17:34:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
path: src/Payments.sol
user: login: aarshkshah1992
body: currentEpoch < targetEpoch is what we need here. if it is equal, we are done and we should exit the loop.
created_at: 2025-02-19T17:35:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
path: src/Payments.sol
user: login: Stebalien
body: This second condition won't be true if I'm trying to settle up to some historical epoch in the past.
created_at: 2025-02-19T17:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                activeRate
+            );
+
+            // Update the total settled. 
+            totalSettled += segmentAmount;
+
+            // If the arbitration shortened the segment, stop and return immediately.
+            // but keep the rate change in the queue as we've not fully settled the segment.
+            if (segmentEndEpoch < nextBoundary) {
+                return (totalSettled, segmentEndEpoch, arbNote);
+            }
+
+            // Otherwise, we have fully settled up to nextBoundary.
+            currentEpoch = segmentEndEpoch;
+            note = arbNote;
path: src/Payments.sol
user: login: Stebalien
body: It would be kind of nice to somehow join these notes together when non-empty, but that isn't critical. We mostly care about the last note anyways.
created_at: 2025-02-19T17:38:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                activeRate
+            );
+
+            // Update the total settled. 
+            totalSettled += segmentAmount;
+
+            // If the arbitration shortened the segment, stop and return immediately.
+            // but keep the rate change in the queue as we've not fully settled the segment.
+            if (segmentEndEpoch < nextBoundary) {
+                return (totalSettled, segmentEndEpoch, arbNote);
+            }
+
+            // Otherwise, we have fully settled up to nextBoundary.
+            currentEpoch = segmentEndEpoch;
+            note = arbNote;
+
+            // Dequeue the rate change we just passed (if it was indeed used this round).
+            if (!rateQueue.isEmpty()) {
+                // Peek again to confirm it's the one we used.
+                RateChangeQueue.RateChange memory frontChange = rateQueue.peek();
path: src/Payments.sol
user: login: Stebalien
body: How would this not be the case?
created_at: 2025-02-19T17:39:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
path: src/Payments.sol
user: login: aarshkshah1992
body: We need to remove this assert.
created_at: 2025-02-19T17:43:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                activeRate
+            );
+
+            // Update the total settled. 
+            totalSettled += segmentAmount;
+
+            // If the arbitration shortened the segment, stop and return immediately.
+            // but keep the rate change in the queue as we've not fully settled the segment.
+            if (segmentEndEpoch < nextBoundary) {
+                return (totalSettled, segmentEndEpoch, arbNote);
+            }
+
+            // Otherwise, we have fully settled up to nextBoundary.
+            currentEpoch = segmentEndEpoch;
+            note = arbNote;
+
+            // Dequeue the rate change we just passed (if it was indeed used this round).
+            if (!rateQueue.isEmpty()) {
+                // Peek again to confirm it's the one we used.
+                RateChangeQueue.RateChange memory frontChange = rateQueue.peek();
path: src/Payments.sol
user: login: aarshkshah1992
body: if the queue is non-empty AND next element has expired -> until epoch is in the past or < targetEpoch -> pop.
created_at: 2025-02-19T17:45:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
path: src/Payments.sol
user: login: aarshkshah1992
body: min(next, target)
created_at: 2025-02-19T17:49:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
path: src/Payments.sol
user: login: aarshkshah1992
body: nextBoundary = min(nextBoundary, target)
created_at: 2025-02-19T17:50:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                activeRate
+            );
+
+            // Update the total settled. 
+            totalSettled += segmentAmount;
+
+            // If the arbitration shortened the segment, stop and return immediately.
+            // but keep the rate change in the queue as we've not fully settled the segment.
+            if (segmentEndEpoch < nextBoundary) {
+                return (totalSettled, segmentEndEpoch, arbNote);
+            }
+
+            // Otherwise, we have fully settled up to nextBoundary.
+            currentEpoch = segmentEndEpoch;
+            note = arbNote;
+
+            // Dequeue the rate change we just passed (if it was indeed used this round).
+            if (!rateQueue.isEmpty()) {
+                // Peek again to confirm it's the one we used.
+                RateChangeQueue.RateChange memory frontChange = rateQueue.peek();
path: src/Payments.sol
user: login: aarshkshah1992
body: actually -> we can JUST dequeue if we reach here.
created_at: 2025-02-19T17:51:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
path: src/Payments.sol
user: login: Stebalien
body: pass `min(nextBoundary, targetEpoch)`
created_at: 2025-02-19T17:51:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
path: src/Payments.sol
user: login: Stebalien
body: fix comparison.
created_at: 2025-02-19T17:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
path: src/Payments.sol
user: login: aarshkshah1992
body: mark the function as `nonReEntrant`.
created_at: 2025-02-19T17:54:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
path: src/Payments.sol
user: login: aarshkshah1992
body: Infact all functions that modify rail state should have a per-rail lock.
created_at: 2025-02-19T17:57:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
path: src/Payments.sol
user: login: aarshkshah1992
body: Audit and REALLY think through it.
created_at: 2025-02-19T17:57:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
path: src/Payments.sol
user: login: aarshkshah1992
body: rentrancy
created_at: 2025-02-19T18:34:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
path: src/Payments.sol
user: login: aarshkshah1992
body: per-rail settlement lock
created_at: 2025-02-19T18:35:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
path: src/Payments.sol
user: login: aarshkshah1992
body: move this to the Rail struct.
created_at: 2025-02-19T18:46:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
path: src/Payments.sol
user: login: Stebalien
body: Also, if _operators_ create rails, it makes sense to:

1. Approve the operator with no allowance.
2. Let the operator create rails.
3. Then give them allowance.

(IMO)
created_at: 2025-02-19T20:32:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
path: src/Payments.sol
user: login: aarshkshah1992
body: We agree that we shouldn't have this here as the arbitrer might be different for each rail.
created_at: 2025-02-26T08:32:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
path: src/Payments.sol
user: login: aarshkshah1992
body: Have changed `createRail` to automagically approve an operator with zero allowance if it does not already exist. Done.
created_at: 2025-02-26T08:51:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T08:53:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.

@Stebalien -> Not only do they not need funds now, but we've fixed this so that both `from` and `to` accounts will be created if they don't already exist.
created_at: 2025-02-26T08:57:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
path: src/Payments.sol
user: login: aarshkshah1992
body: We agreed that only operators can create rails.
created_at: 2025-02-26T09:26:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
path: src/Payments.sol
user: login: aarshkshah1992
body: Removed this function.
created_at: 2025-02-26T11:55:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
path: src/Payments.sol
user: login: aarshkshah1992
body: Done. Removed this.
created_at: 2025-02-26T11:56:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,572 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settledUntil;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 lastSettledAt;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.lastSettledAt = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+            uint256 railId,
+            uint256 period,
+            uint256 lockupFixed
+        ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account is not funded upto to the current epoch; revert
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(payer);
+        require(funded && settleEpoch == block.number, "cannot modify lockup as client does not have enough funds for current settlement");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allownace is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer lockup lockup_current cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup_current cannot be greater than funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        returns (uint256 deficit)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool lockupSettled, uint256 settledEpoch) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(lockupSettled && settledEpoch == block.number, "lockup not fully settled; cannot change rate");
+        }
+
+        // Save the current rate for computing lockup deltas.
+        uint256 oldRate = rail.paymentRate;
+
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUntil) = settleRail(railId, block.number);
+            require(settledUntil == block.number, "not able to settle rail at current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+        // Update the payer's current locked funds:
+        // Remove the old continuous lockup and also subtract the one-time payment,
+        // then add the new continuous lockup requirement.
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;
+
+        rail.paymentRate = newRate;
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(rail.lockupFixed >= 0, "rail lockupFixed must be non-negative");
+        require(payer.lockupCurrent >= 0, "payer lockupCurrent must be non-negative");
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+
+        return 0;
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        require(oneTimePayment <= rail.lockupFixed, "one-time payment exceeds rail fixed lockup");
+
+        uint256 oldLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newLockup = (newRate * rail.lockupPeriod) + (rail.lockupFixed - oneTimePayment);
+
+        // Check that new total lockup + one time payment is within allowance
+        require(newLockup + oneTimePayment <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        // Adjust the operator's available lockup allowance.
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - (newLockup + oneTimePayment);
+        require(newRate <= approval.rateAllowance, "new rate exceeds operator rate allowance");
+    }
+
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+
+    function settleRail(uint256 railId, uint256 untilEpoch)
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch
+        )
+    {
+        // Get the rail and the involved accounts.
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        settleAccountLockup(payer);
+
+        uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod);
+
+        // Begin settlement from the last settled epoch.
+        uint256 currentSettlementEpoch = rail.lastSettledAt;
+        uint256 currentRate = rail.paymentRate;
+        totalSettledAmount = 0;
+
+        // Use the rail's rate–change queue.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If there are no queued rate changes, settle the entire segment in one shot.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch) = _settleSegment(
+                railId,
+                rail,
+                currentSettlementEpoch,
+                settlementTargetEpoch,
+                currentRate
+            );
+        } else {
+            // Otherwise, settle the rail in segments (each up to the next rate–change boundary).
+            (totalSettledAmount, finalSettledEpoch) = _settleWithRateChanges(
+                railId,
+                rail,
+                rateQueue,
+                currentRate,
+                currentSettlementEpoch,
+                settlementTargetEpoch
+            );
+        }
+
+        // Update account balances.
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        require(payer.lockupCurrent >= totalSettledAmount, "Insufficient lockup funds");
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Record the new settlement epoch on the rail.
+        rail.lastSettledAt = finalSettledEpoch;
+
+        require(payer.lockupCurrent <= payer.funds, "insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 segmentStart,
+        uint256 segmentEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, uint256 settledUntil) {
+        // Calculate the intended payment for the entire segment.
+        uint256 duration = segmentEnd - segmentStart;
+        settledAmount = rate * duration;
+        settledUntil = segmentEnd;
+
+        if (rail.arbiter != address(0)) {
+            // Call the external arbitrator.
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                segmentStart,
+                segmentEnd
+            );
+            require(arbResult.approved, "arbitrer refused payment");
+            // Do not settle past the segment end.
+            settledUntil = arbResult.settledUntil;
+            settledAmount = arbResult.modifiedAmount;
+        }
+
+        // If no progress is made, revert.
+        require(settledUntil > segmentStart, "no progress made in settlement");
+
+        return (settledAmount, settledUntil);
+    }
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+
+        while (currentEpoch < targetEpoch) {
+            // Determine the next boundary: either the next queued rate–change or the targetEpoch.
+            uint256 nextBoundary = targetEpoch;
+            uint256 segmentRate = currentRate;
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextChange = rateQueue.peek();
+                if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) {
+                    nextBoundary = nextChange.untilEpoch;
+                    segmentRate = nextChange.rate;
+                }
path: src/Payments.sol
user: login: aarshkshah1992
body: Have refactored this entire flow to make it easy to reason about.
created_at: 2025-02-26T11:56:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
path: src/Payments.sol
user: login: aarshkshah1992
body: This is done.
created_at: 2025-02-26T11:57:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
path: src/Payments.sol
user: login: aarshkshah1992
body: Removed  this. Will add it back if needed.
created_at: 2025-02-26T11:57:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:58:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:58:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:58:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
+                activeRate
+            );
+
+            // Update the total settled. 
+            totalSettled += segmentAmount;
+
+            // If the arbitration shortened the segment, stop and return immediately.
+            // but keep the rate change in the queue as we've not fully settled the segment.
+            if (segmentEndEpoch < nextBoundary) {
+                return (totalSettled, segmentEndEpoch, arbNote);
+            }
+
+            // Otherwise, we have fully settled up to nextBoundary.
+            currentEpoch = segmentEndEpoch;
+            note = arbNote;
+
+            // Dequeue the rate change we just passed (if it was indeed used this round).
+            if (!rateQueue.isEmpty()) {
+                // Peek again to confirm it's the one we used.
+                RateChangeQueue.RateChange memory frontChange = rateQueue.peek();
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:59:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:59:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
+            uint256 nextBoundary = targetEpoch;
+
+            // If there's an upcoming rate change, check if it applies within our current range.
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek();
+                bool isWithinRange = (
+                    upcomingChange.untilEpoch >= currentEpoch &&
+                    upcomingChange.untilEpoch <= targetEpoch
+                );
+                require(isWithinRange, "rate queue is in an invalid state");
+                nextBoundary = upcomingChange.untilEpoch;
+                activeRate = upcomingChange.rate;
+            }
+
+            // Settle the segment from the current epoch up to the next boundary (or early if arbitration says so).
+            (uint256 segmentAmount, uint256 segmentEndEpoch, string memory arbNote) = _settleSegment(
+                railId,
+                rail,
+                currentEpoch,
+                nextBoundary,
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:59:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) public rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval))) public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[])) public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(accounts[rail.token][rail.from].ownerAddress != address(0), "from account does not exist");
+        require(accounts[rail.token][rail.to].ownerAddress != address(0), "to account does not exist");
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(rails[railId].operator == msg.sender, "only the rail operator can perform this action");
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        address arbiter,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][msg.sender][operator];
+        approval.arbitrer = arbiter;
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Revisit
+    function terminateOperator(address operator) external  {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            settleRail(railIds[i], block.number);
+
+            Account storage account = accounts[rail.token][msg.sender];
+            account.lockupCurrent -= rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+            account.lockupRate -= rail.paymentRate;
+
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            OperatorApproval storage approval = operatorApprovals[rail.token][msg.sender][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+        }
+    }
+
+    // TODO: implement
+    function terminateRail() public {
+
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(amount <= available, "insufficient unlocked funds for withdrawal");
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address operator,
+        address arbiter
+    ) external returns (uint256) {
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval memory approval = operatorApprovals[token][from][operator];
+        require(approval.isApproved, "operator not approved");
+
+        Account storage toAccount = accounts[token][to];
+        require(toAccount.ownerAddress != address(0), "to account does not exist");
+        require(toAccount.funds > 0, "to account has no funds");
+
+        Account storage fromAccount = accounts[token][from];
+        require(fromAccount.ownerAddress != address(0), "from account does not exist");
+        require(fromAccount.funds > 0, "from account has no funds");
+
+        if (approval.arbitrer != address(0)) {
+            require(arbiter == approval.arbitrer, "arbiter mismatch");
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+
+        Account storage payer = accounts[rail.token][rail.from];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        require(fullySettled && lockupSettledUpto == block.number, "cannot modify lockup as client does not have enough funds for current account lockup");
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(newLockup <= oldLockup + approval.lockupAllowance, "exceeds operator lockup allowance");
+        approval.lockupAllowance = approval.lockupAllowance + oldLockup - newLockup;
+
+        // Update payer's lockup
+        require(payer.lockupCurrent >= oldLockup, "payer's current lockup cannot be less than old lockup");
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(payer.lockupCurrent <= payer.funds, "payer's current lockup cannot be greater than their funds");
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) external validateRailActive(railId) validateRailAccountsExist(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(payer);
+        if (newRate != rail.paymentRate) {
+            require(fullySettled && lockupSettledUpto == block.number, "account lockup not fully settled; cannot change rate");
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(rail, approval, oldRate, newRate, oneTimePayment);
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(settledUpto == block.number, "not able to settle rail upto current epoch");
+        } else {
+            railRateChangeQueues[railId].enqueue(rail.paymentRate, block.number);
+        }
+
+        require(rail.lockupFixed >= oneTimePayment, "one time payment cannot be greater than rail lockupFixed");
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(payer.lockupRate >= oldRate, "payer lockup rate cannot be less than old rate");
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(payer.lockupCurrent >= ((oldRate * rail.lockupPeriod) + oneTimePayment), "failed to modify rail payment: insufficient current lockup");
+        payer.lockupCurrent = payer.lockupCurrent - (oldRate * rail.lockupPeriod) + (newRate * rail.lockupPeriod) - oneTimePayment;        
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(payer.lockupCurrent <= payer.funds, "payer lockup cannot exceed funds");
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(payer.funds >= oneTimePayment, "insufficient funds for one-time payment");
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) + rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) + rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance = approval.lockupAllowance + oldTotalLockup - newTotalLockup;
+
+        require(newRate <= oldRate + approval.rateAllowance, "new rate exceeds operator rate allowance");
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+        
+    }
+
+    function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) {
+        Rail storage rail = rails[railId];
+        OperatorApproval storage approval = operatorApprovals[rail.token][rail.from][rail.operator];
+        require(approval.isApproved, "operator not approved");
+        if (approval.arbitrer != address(0)) {
+            require(newArbiter == approval.arbitrer, "arbiter mismatch");
+        }
+        // Update the arbiter
+        rail.arbiter = newArbiter;
+    }
+
+    function settleRail(uint256 railId, uint256 untilEpoch) public validateRailActive(railId) validateRailAccountsExist(railId)    
+    returns (
+        uint256 totalSettledAmount,
+        uint256 finalSettledEpoch,
+        string memory note
+    ) {
+        require(untilEpoch <= block.number, "failed to settle: cannot settle future epochs");
+
+        Rail storage paymentRail = rails[railId];
+        Account storage payer = accounts[paymentRail.token][paymentRail.from];
+        Account storage payee = accounts[paymentRail.token][paymentRail.to];
+
+        settleAccountLockup(payer);
+
+        // Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod.
+        uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod);
+
+        // Begin from the last epoch that was actually settled.
+        uint256 lastSettledEpoch = paymentRail.settledUpTo;
+        uint256 currentPaymentRate = paymentRail.paymentRate;
+
+        // Retrieve any queued rate changes for this rail.
+        RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId];
+
+        // If no rate changes are queued, settle the entire segment from lastSettledEpoch to maxSettlementEpoch.
+        if (rateQueue.isEmpty()) {
+            (totalSettledAmount, finalSettledEpoch, note) = _settleSegment(
+                railId,
+                paymentRail,
+                lastSettledEpoch,
+                maxSettlementEpoch,
+                currentPaymentRate
+            );
+        } else {
+            // Otherwise, handle each segment up to the next rate–change boundary.
+            (totalSettledAmount, finalSettledEpoch, note) = _settleWithRateChanges(
+                railId,
+                paymentRail,
+                rateQueue,
+                currentPaymentRate,
+                lastSettledEpoch,
+                maxSettlementEpoch
+            );
+        }
+
+        // Check and reduce the payer's funds and lockup after we know how much is settled.
+        require(payer.lockupCurrent >= totalSettledAmount, "failed to settle: insufficient lockup funds");
+        payer.funds -= totalSettledAmount;
+        payee.funds += totalSettledAmount;
+        payer.lockupCurrent -= totalSettledAmount;
+
+        // Update the final settled epoch on the rail record.
+        paymentRail.settledUpTo = finalSettledEpoch;
+
+        // Sanity check: the lockup should never exceed total funds.
+        require(payer.lockupCurrent <= payer.funds, "failed to settle: insufficient funds");
+
+        return (totalSettledAmount, finalSettledEpoch, note);
+    }
+
+    /**
+     * @notice Settles a payment segment for the specified epoch range at the given rate.
+     *         If an arbiter is set, an external arbitration call is made to potentially
+     *         modify the final settlement amount or end epoch.
+     * @param railId The identifier of the rail being settled.
+     * @param rail A storage reference to the rail.
+     * @param epochStart The starting epoch of this settlement segment.
+     * @param epochEnd The ending epoch of this settlement segment.
+     * @param rate The rate for this settlement segment.
+     * @return settledAmount The final amount to settle for this segment.
+     * @return settledUntil The final epoch that effectively got settled.
+     * @return note An optional note returned by the arbitrator.
+     */
+    function _settleSegment(
+        uint256 railId,
+        Rail storage rail,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) 
+        internal 
+        returns (
+            uint256 settledAmount, 
+            uint256 settledUntil, 
+            string memory note
+        ) 
+    {
+        // Calculate the intended settlement amount for the time span.
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        settledUntil = epochEnd;
+
+        // If the rail has an assigned arbiter, call it for arbitration on this segment.
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory arbResult = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            require(arbResult.approved, "failed to settle: arbiter refused payment");
+            require(
+                arbResult.settleUpto <= epochEnd,
+                "failed to settle: arbiter settled beyond segment end"
+            );
+            require(
+                arbResult.modifiedAmount <= rate * (arbResult.settleUpto - epochStart),
+                "failed to settle: arbiter modified amount exceeds maximum for settled duration"
+            );
+
+            // Adjust the amount and the final epoch according to the arbiter's decision.
+            settledUntil = arbResult.settleUpto;
+            settledAmount = arbResult.modifiedAmount;
+            note = arbResult.note;
+        }
+
+        // Revert if no progress was made (i.e., the final epoch did not advance).
+        require(settledUntil > epochStart, "failed to settle: no progress made in settlement");
+
+        return (settledAmount, settledUntil, note);
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        Rail storage rail,
+        RateChangeQueue.Queue storage rateQueue,
+        uint256 initialRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    ) internal returns (uint256 totalSettled, uint256 finalEpoch, string memory note) {
+        totalSettled = 0;
+        uint256 currentEpoch = startEpoch;
+        uint256 activeRate = initialRate;
+        note = "";
+
+        while (currentEpoch <= targetEpoch) {
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        bool approved;
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard {
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        address arbitrer;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // railId => RateChangeQueue
+    mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues;
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-02-26T11:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+#!/bin/bash
path: configure
user: login: Stebalien
body: Can we move this into the ~/.github directory?
created_at: 2025-02-26T19:29:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+#!/bin/bash
path: configure
user: login: Stebalien
body: Also, do we even need this for the github actions? As far as I can tell, the foundry action handles this for us.
created_at: 2025-02-26T19:32:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+#!/bin/bash
+
+# GH Actions foundry setup
+curl -L https://foundry.paradigm.xyz | bash
path: configure
user: login: Stebalien
body: ```suggestion
curl -fL https://foundry.paradigm.xyz | bash
```
created_at: 2025-02-26T19:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: Stebalien
body: We should change this to the MIT license and include that in this directory.
created_at: 2025-02-26T21:33:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: Stebalien
body: We're going to need to handle multiple rate-changes in a single epoch.
created_at: 2025-02-27T22:00:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
path: src/Payments.sol
user: login: Stebalien
body: Any reason not to put a locked field in the rail itself?
created_at: 2025-02-27T22:14:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
path: src/Payments.sol
user: login: Stebalien
body: We're going to need to be able to terminate operators without settling the rails because that might require the operator's approval::

1. Settle lockup across all rails, not rails themselves.
2. Tombstone the rail.
3. Remove the rail's lockup rate from the payer's account's lockup rate (no more funds get locked for this rail).

But there are likely hidden complexities.

IMO, we should consider punting this feature to the next PR, stubbing it in this PR.
created_at: 2025-02-27T22:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
path: src/Payments.sol
user: login: Stebalien
body: I can't remember if we talked about this but... do we need this field? If we get rid of it, we don't even need to "create" the account, it'll just implicitly exist with no funds.
created_at: 2025-02-27T22:30:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
path: src/Payments.sol
user: login: Stebalien
body: need a reentrency check here as well.
created_at: 2025-02-27T22:33:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
path: src/Payments.sol
user: login: Stebalien
body: note: I wonder if we want to allow withdrawing to a different account. Is there a standard "withdraw" signature?
created_at: 2025-02-27T22:34:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
path: src/Payments.sol
user: login: Stebalien
body: I'd abort in this case.
created_at: 2025-02-27T22:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
path: src/Payments.sol
user: login: Stebalien
body: Also important if, e.g., we terminate the operator (where we'll have to set `isApproved` to `false`.
created_at: 2025-02-27T22:35:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
path: src/Payments.sol
user: login: Stebalien
body: We're going to have to talk about this with respect to operator termination. Ideally the operator would be allowed to reduce lockup at this point.
created_at: 2025-02-27T22:37:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: Stebalien
body: This shouldn't be necessary, right? If the rail exists, the accounts exist.
created_at: 2025-02-27T22:46:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: Stebalien
body: ditto
created_at: 2025-02-27T22:55:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+        }
+
+        require(
+            rail.lockupFixed >= oneTimePayment,
+            "one time payment cannot be greater than rail lockupFixed"
+        );
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(
+            payer.lockupRate >= oldRate,
+            "payer lockup rate cannot be less than old rate"
+        );
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(
+            payer.lockupCurrent >=
+                ((oldRate * rail.lockupPeriod) + oneTimePayment),
+            "failed to modify rail payment: insufficient current lockup"
+        );
+        payer.lockupCurrent =
+            payer.lockupCurrent -
+            (oldRate * rail.lockupPeriod) +
+            (newRate * rail.lockupPeriod) -
+            oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer lockup cannot exceed funds"
+        );
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(
+                payer.funds >= oneTimePayment,
+                "insufficient funds for one-time payment"
+            );
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldTotalLockup -
+            newTotalLockup;
+
+        require(
+            newRate <= oldRate + approval.rateAllowance,
+            "new rate exceeds operator rate allowance"
+        );
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+    }
+
+    function settleRail(
+        uint256 railId,
+        uint256 untilEpoch
+    )
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        nonReentrant
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch,
+            string memory note
+        )
+    {
+        require(
+            untilEpoch <= block.number,
+            "failed to settle: cannot settle future epochs"
+        );
+
+        Rail storage rail = rails[railId];
+
+        // Update the payer's lockup to account for elapsed time
+        Account storage payer = accounts[rail.token][rail.from];
+        settleAccountLockup(payer);
+
+        uint256 maxSettlementEpoch = min(
+            untilEpoch,
+            payer.lockupLastSettledAt + rail.lockupPeriod
+        );
+
+        uint256 startEpoch = rail.settledUpTo;
+
+        // nothing to settle (already settled or zero-duration)
+        if (startEpoch >= maxSettlementEpoch) {
+            return (0, startEpoch, "already settled upto requested epoch");
+        }
+
+        // for zero rate rails with empty queue, just advance the settlement epoch
+        // without transferring funds
+        uint256 currentRate = rail.paymentRate;
+        if (currentRate == 0 && rail.rateChangeQueue.isEmpty()) {
+            rail.settledUpTo = maxSettlementEpoch;
+            return (0, maxSettlementEpoch, "zero rate payment rail");
+        }
+
+        // Process settlement depending on whether rate changes exist
+        if (rail.rateChangeQueue.isEmpty()) {
+            (uint256 amount, string memory segmentNote) = _settleSegment(
+                railId,
+                startEpoch,
+                maxSettlementEpoch,
+                currentRate
+            );
+
+            require(rail.settledUpTo > startEpoch, "No progress in settlement");
+            return (amount, rail.settledUpTo, segmentNote);
+        } else {
+            return
+                _settleWithRateChanges(
+                    railId,
+                    currentRate,
+                    startEpoch,
+                    maxSettlementEpoch
+                );
+        }
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    )
+        internal
+        returns (uint256 totalSettled, uint256 finalEpoch, string memory note)
+    {
+        Rail storage rail = rails[railId];
+        RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue;
+
+        totalSettled = 0;
+        uint256 processedEpoch = startEpoch;
+        note = "";
+
+        // Process each segment until we reach the target epoch or hit an early exit condition
+        while (processedEpoch < targetEpoch) {
+            // Default boundary is the target we want to reach
+            uint256 segmentEndBoundary = targetEpoch;
+            uint256 segmentRate;
+
+            // If we have rate changes in the queue, use the rate from the next change
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextRateChange = rateQueue
+                    .peek();
+
+                // Validate rate change queue consistency
+                require(
+                    nextRateChange.untilEpoch >= processedEpoch,
+                    "rate queue is in an invalid state"
+                );
+
+                // Boundary is the minimum of our target or the next rate change epoch
+                segmentEndBoundary = min(
+                    targetEpoch,
+                    nextRateChange.untilEpoch
+                );
+                segmentRate = nextRateChange.rate;
+            } else {
+                // If queue is empty, use the current rail rate
+                segmentRate = currentRate;
+
+                // if current rate is zero, there's nothing left to do and we've finished settlement
+                if (segmentRate == 0) {
+                    rail.settledUpTo = targetEpoch;
+                    return (
+                        totalSettled,
+                        rail.settledUpTo,
+                        "Zero rate payment rail"
+                    );
+                }
+            }
+
+            // Settle the current segment with potentially arbitrated outcomes
+            (
+                uint256 segmentAmount,
+                string memory arbitrationNote
+            ) = _settleSegment(
+                    railId,
+                    processedEpoch,
+                    segmentEndBoundary,
+                    segmentRate
+                );
+
+            // If arbiter returned no progress, exit early without updating state
+            if (rail.settledUpTo <= processedEpoch) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Add the settled amount to our running total
+            totalSettled += segmentAmount;
+
+            // If arbiter partially settled the segment, exit early
+            if (rail.settledUpTo < segmentEndBoundary) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Successfully settled full segment, update tracking values
+            processedEpoch = rail.settledUpTo;
+            note = arbitrationNote;
+
+            // Remove the processed rate change from the queue
+            if (!rateQueue.isEmpty()) {
+                rateQueue.dequeue();
+            }
+        }
+
+        // We've successfully settled up to the target epoch
+        return (totalSettled, rail.settledUpTo, note);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, string memory note) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        // Calculate the default settlement values (without arbitration)
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        uint256 settledUntilEpoch = epochEnd;
+        note = "";
+
+        // If this rail has an arbiter, let it decide on the final settlement amount
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            // Ensure arbiter doesn't settle beyond our segment's end boundary
+            require(
+                result.settleUpto <= epochEnd,
+                "arbiter settled beyond segment end"
+            );
+
+            settledUntilEpoch = result.settleUpto;
+            settledAmount = result.modifiedAmount;
+            note = result.note;
+
+            // If arbiter made no progress, return early without updating any state
+            if (settledUntilEpoch <= epochStart) {
+                return (0, 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"
+            );
path: src/Payments.sol
user: login: Stebalien
body: I'd:

1. Require that `settledUntilEpoch >= epochStart`.
2. Require that the amount settled is within expectations given the `settledUntilEpoch`.
3. Finally, short-circuit if no funds were actually settled.  Or, TBH, I'd just go to the end of the function anyways and not short-circuit.

Specifically, I don't want to return `(0, note)` if the arbiter returns nonsensical results (e.g., settle 1M FIL up till some epoch in the past).
created_at: 2025-02-27T23:06:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T01:50:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+// SPDX-License-Identifier: UNLICENSED
path: src/Payments.sol
user: login: aarshkshah1992
body: Added license and made the changes.
created_at: 2025-03-03T01:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T01:59:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T02:00:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: aarshkshah1992
body: I don't think we need the ` validateRailAccountsExist` modifier at all now as all accounts always exist implicitly with zero funds (we've now removed the `ownerAddress` field from Account).
created_at: 2025-03-03T02:10:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: aarshkshah1992
body: I don't think we need the  validateRailAccountsExist modifier at all now as all accounts always exist implicitly with zero funds (we've now removed the ownerAddress field from Account).
created_at: 2025-03-03T02:10:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T02:10:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T02:10:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
path: src/Payments.sol
user: login: aarshkshah1992
body: Yeah, you are right. Removing this field and also removing the `onlyAccount` modifier simplifies things a bit and we can always infer the account owner as being the msg sender for any methods that need the sender to prove account ownership.
created_at: 2025-03-03T02:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T02:11:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,694 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+        bool isLocked; // Indicates if the rail is currently being modified
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(!rails[railId].isLocked, "Modification already in progress");
+        rails[railId].isLocked = true;
+        _;
+        rails[railId].isLocked = false;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Implement Me
+    // https://github.com/FilOzone/payments/pull/1/files#r1974415624
+    function terminateOperator(address operator) external {}
+
+    function deposit(
+        address token,
+        address to,
+        uint256 amount
+    ) external nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external nonReentrant {
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien 

I have also implemented a `function withdrawTo(
        address token,
        address to,
        uint256 amount
    )`
    
    function that allows users to withdraw funds to any account of their choosing.
created_at: 2025-03-03T02:18:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
path: src/Payments.sol
user: login: aarshkshah1992
body: Done.
created_at: 2025-03-03T02:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Let's talk about this in our 1:1 today.
created_at: 2025-03-03T02:29:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Good catch ! How do we want to implement this ?

If a user calls `modifyRailPayment` multiple times in a single epoch -> should we only enque the previous (before this epoch) `rail.paymentRate ` ONLY once and ensure that `rail.paymentRate ` reflects the last rate update ?

Basically, the rate changes within a single epoch do not end up on the queue.

created_at: 2025-03-03T03:00:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+        }
+
+        require(
+            rail.lockupFixed >= oneTimePayment,
+            "one time payment cannot be greater than rail lockupFixed"
+        );
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(
+            payer.lockupRate >= oldRate,
+            "payer lockup rate cannot be less than old rate"
+        );
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(
+            payer.lockupCurrent >=
+                ((oldRate * rail.lockupPeriod) + oneTimePayment),
+            "failed to modify rail payment: insufficient current lockup"
+        );
+        payer.lockupCurrent =
+            payer.lockupCurrent -
+            (oldRate * rail.lockupPeriod) +
+            (newRate * rail.lockupPeriod) -
+            oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer lockup cannot exceed funds"
+        );
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(
+                payer.funds >= oneTimePayment,
+                "insufficient funds for one-time payment"
+            );
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldTotalLockup -
+            newTotalLockup;
+
+        require(
+            newRate <= oldRate + approval.rateAllowance,
+            "new rate exceeds operator rate allowance"
+        );
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+    }
+
+    function settleRail(
+        uint256 railId,
+        uint256 untilEpoch
+    )
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        nonReentrant
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch,
+            string memory note
+        )
+    {
+        require(
+            untilEpoch <= block.number,
+            "failed to settle: cannot settle future epochs"
+        );
+
+        Rail storage rail = rails[railId];
+
+        // Update the payer's lockup to account for elapsed time
+        Account storage payer = accounts[rail.token][rail.from];
+        settleAccountLockup(payer);
+
+        uint256 maxSettlementEpoch = min(
+            untilEpoch,
+            payer.lockupLastSettledAt + rail.lockupPeriod
+        );
+
+        uint256 startEpoch = rail.settledUpTo;
+
+        // nothing to settle (already settled or zero-duration)
+        if (startEpoch >= maxSettlementEpoch) {
+            return (0, startEpoch, "already settled upto requested epoch");
+        }
+
+        // for zero rate rails with empty queue, just advance the settlement epoch
+        // without transferring funds
+        uint256 currentRate = rail.paymentRate;
+        if (currentRate == 0 && rail.rateChangeQueue.isEmpty()) {
+            rail.settledUpTo = maxSettlementEpoch;
+            return (0, maxSettlementEpoch, "zero rate payment rail");
+        }
+
+        // Process settlement depending on whether rate changes exist
+        if (rail.rateChangeQueue.isEmpty()) {
+            (uint256 amount, string memory segmentNote) = _settleSegment(
+                railId,
+                startEpoch,
+                maxSettlementEpoch,
+                currentRate
+            );
+
+            require(rail.settledUpTo > startEpoch, "No progress in settlement");
+            return (amount, rail.settledUpTo, segmentNote);
+        } else {
+            return
+                _settleWithRateChanges(
+                    railId,
+                    currentRate,
+                    startEpoch,
+                    maxSettlementEpoch
+                );
+        }
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    )
+        internal
+        returns (uint256 totalSettled, uint256 finalEpoch, string memory note)
+    {
+        Rail storage rail = rails[railId];
+        RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue;
+
+        totalSettled = 0;
+        uint256 processedEpoch = startEpoch;
+        note = "";
+
+        // Process each segment until we reach the target epoch or hit an early exit condition
+        while (processedEpoch < targetEpoch) {
+            // Default boundary is the target we want to reach
+            uint256 segmentEndBoundary = targetEpoch;
+            uint256 segmentRate;
+
+            // If we have rate changes in the queue, use the rate from the next change
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextRateChange = rateQueue
+                    .peek();
+
+                // Validate rate change queue consistency
+                require(
+                    nextRateChange.untilEpoch >= processedEpoch,
+                    "rate queue is in an invalid state"
+                );
+
+                // Boundary is the minimum of our target or the next rate change epoch
+                segmentEndBoundary = min(
+                    targetEpoch,
+                    nextRateChange.untilEpoch
+                );
+                segmentRate = nextRateChange.rate;
+            } else {
+                // If queue is empty, use the current rail rate
+                segmentRate = currentRate;
+
+                // if current rate is zero, there's nothing left to do and we've finished settlement
+                if (segmentRate == 0) {
+                    rail.settledUpTo = targetEpoch;
+                    return (
+                        totalSettled,
+                        rail.settledUpTo,
+                        "Zero rate payment rail"
+                    );
+                }
+            }
+
+            // Settle the current segment with potentially arbitrated outcomes
+            (
+                uint256 segmentAmount,
+                string memory arbitrationNote
+            ) = _settleSegment(
+                    railId,
+                    processedEpoch,
+                    segmentEndBoundary,
+                    segmentRate
+                );
+
+            // If arbiter returned no progress, exit early without updating state
+            if (rail.settledUpTo <= processedEpoch) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Add the settled amount to our running total
+            totalSettled += segmentAmount;
+
+            // If arbiter partially settled the segment, exit early
+            if (rail.settledUpTo < segmentEndBoundary) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Successfully settled full segment, update tracking values
+            processedEpoch = rail.settledUpTo;
+            note = arbitrationNote;
+
+            // Remove the processed rate change from the queue
+            if (!rateQueue.isEmpty()) {
+                rateQueue.dequeue();
+            }
+        }
+
+        // We've successfully settled up to the target epoch
+        return (totalSettled, rail.settledUpTo, note);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, string memory note) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        // Calculate the default settlement values (without arbitration)
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        uint256 settledUntilEpoch = epochEnd;
+        note = "";
+
+        // If this rail has an arbiter, let it decide on the final settlement amount
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            // Ensure arbiter doesn't settle beyond our segment's end boundary
+            require(
+                result.settleUpto <= epochEnd,
+                "arbiter settled beyond segment end"
+            );
+
+            settledUntilEpoch = result.settleUpto;
+            settledAmount = result.modifiedAmount;
+            note = result.note;
+
+            // If arbiter made no progress, return early without updating any state
+            if (settledUntilEpoch <= epochStart) {
+                return (0, 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"
+            );
path: src/Payments.sol
user: login: aarshkshah1992
body: This is done.
created_at: 2025-03-03T03:29:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+        }
+
+        require(
+            rail.lockupFixed >= oneTimePayment,
+            "one time payment cannot be greater than rail lockupFixed"
+        );
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(
+            payer.lockupRate >= oldRate,
+            "payer lockup rate cannot be less than old rate"
+        );
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(
+            payer.lockupCurrent >=
+                ((oldRate * rail.lockupPeriod) + oneTimePayment),
+            "failed to modify rail payment: insufficient current lockup"
+        );
+        payer.lockupCurrent =
+            payer.lockupCurrent -
+            (oldRate * rail.lockupPeriod) +
+            (newRate * rail.lockupPeriod) -
+            oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer lockup cannot exceed funds"
+        );
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(
+                payer.funds >= oneTimePayment,
+                "insufficient funds for one-time payment"
+            );
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldTotalLockup -
+            newTotalLockup;
+
+        require(
+            newRate <= oldRate + approval.rateAllowance,
+            "new rate exceeds operator rate allowance"
+        );
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+    }
+
+    function settleRail(
+        uint256 railId,
+        uint256 untilEpoch
+    )
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        nonReentrant
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch,
+            string memory note
+        )
+    {
+        require(
+            untilEpoch <= block.number,
+            "failed to settle: cannot settle future epochs"
+        );
+
+        Rail storage rail = rails[railId];
+
+        // Update the payer's lockup to account for elapsed time
+        Account storage payer = accounts[rail.token][rail.from];
+        settleAccountLockup(payer);
+
+        uint256 maxSettlementEpoch = min(
+            untilEpoch,
+            payer.lockupLastSettledAt + rail.lockupPeriod
+        );
+
+        uint256 startEpoch = rail.settledUpTo;
+
+        // nothing to settle (already settled or zero-duration)
+        if (startEpoch >= maxSettlementEpoch) {
+            return (0, startEpoch, "already settled upto requested epoch");
+        }
+
+        // for zero rate rails with empty queue, just advance the settlement epoch
+        // without transferring funds
+        uint256 currentRate = rail.paymentRate;
+        if (currentRate == 0 && rail.rateChangeQueue.isEmpty()) {
+            rail.settledUpTo = maxSettlementEpoch;
+            return (0, maxSettlementEpoch, "zero rate payment rail");
+        }
+
+        // Process settlement depending on whether rate changes exist
+        if (rail.rateChangeQueue.isEmpty()) {
+            (uint256 amount, string memory segmentNote) = _settleSegment(
+                railId,
+                startEpoch,
+                maxSettlementEpoch,
+                currentRate
+            );
+
+            require(rail.settledUpTo > startEpoch, "No progress in settlement");
+            return (amount, rail.settledUpTo, segmentNote);
+        } else {
+            return
+                _settleWithRateChanges(
+                    railId,
+                    currentRate,
+                    startEpoch,
+                    maxSettlementEpoch
+                );
+        }
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    )
+        internal
+        returns (uint256 totalSettled, uint256 finalEpoch, string memory note)
+    {
+        Rail storage rail = rails[railId];
+        RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue;
+
+        totalSettled = 0;
+        uint256 processedEpoch = startEpoch;
+        note = "";
+
+        // Process each segment until we reach the target epoch or hit an early exit condition
+        while (processedEpoch < targetEpoch) {
+            // Default boundary is the target we want to reach
+            uint256 segmentEndBoundary = targetEpoch;
+            uint256 segmentRate;
+
+            // If we have rate changes in the queue, use the rate from the next change
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextRateChange = rateQueue
+                    .peek();
+
+                // Validate rate change queue consistency
+                require(
+                    nextRateChange.untilEpoch >= processedEpoch,
+                    "rate queue is in an invalid state"
+                );
+
+                // Boundary is the minimum of our target or the next rate change epoch
+                segmentEndBoundary = min(
+                    targetEpoch,
+                    nextRateChange.untilEpoch
+                );
+                segmentRate = nextRateChange.rate;
+            } else {
+                // If queue is empty, use the current rail rate
+                segmentRate = currentRate;
+
+                // if current rate is zero, there's nothing left to do and we've finished settlement
+                if (segmentRate == 0) {
+                    rail.settledUpTo = targetEpoch;
+                    return (
+                        totalSettled,
+                        rail.settledUpTo,
+                        "Zero rate payment rail"
+                    );
+                }
+            }
+
+            // Settle the current segment with potentially arbitrated outcomes
+            (
+                uint256 segmentAmount,
+                string memory arbitrationNote
+            ) = _settleSegment(
+                    railId,
+                    processedEpoch,
+                    segmentEndBoundary,
+                    segmentRate
+                );
+
+            // If arbiter returned no progress, exit early without updating state
+            if (rail.settledUpTo <= processedEpoch) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Add the settled amount to our running total
+            totalSettled += segmentAmount;
+
+            // If arbiter partially settled the segment, exit early
+            if (rail.settledUpTo < segmentEndBoundary) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Successfully settled full segment, update tracking values
+            processedEpoch = rail.settledUpTo;
+            note = arbitrationNote;
+
+            // Remove the processed rate change from the queue
+            if (!rateQueue.isEmpty()) {
+                rateQueue.dequeue();
+            }
+        }
+
+        // We've successfully settled up to the target epoch
+        return (totalSettled, rail.settledUpTo, note);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, string memory note) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        // Calculate the default settlement values (without arbitration)
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        uint256 settledUntilEpoch = epochEnd;
+        note = "";
+
+        // If this rail has an arbiter, let it decide on the final settlement amount
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            // Ensure arbiter doesn't settle beyond our segment's end boundary
+            require(
+                result.settleUpto <= epochEnd,
+                "arbiter settled beyond segment end"
+            );
+
+            settledUntilEpoch = result.settleUpto;
+            settledAmount = result.modifiedAmount;
+            note = result.note;
+
+            // If arbiter made no progress, return early without updating any state
+            if (settledUntilEpoch <= epochStart) {
+                return (0, 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"
+            );
path: src/Payments.sol
user: login: aarshkshah1992
body: TODO for @aarshkshah1992 : Audit the code for off by ones to enforce that the `untilEpoch` in the rate-change queue is not inclusive of the end epoch and that `rail.settledUpto` is inclusive of the settledEpoch.
created_at: 2025-03-03T03:30:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien Implemented this.
created_at: 2025-03-03T03:31:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien I also realised after looking at this again that we're basically saying that the new rate will be applicable **_AFTER_** the current block. The rail will be settled upto and **including** this epoch using the old rate. 

Just wanted to confirm if that's the expected behaviour. IMO, it makes sense.
created_at: 2025-03-03T13:09:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: Stebalien
body: > Basically, the rate changes within a single epoch do not end up on the queue.

Exactly.

> Just wanted to confirm if that's the expected behaviour. IMO, it makes sense.

IMO we could do it either way but... I'd keep it the current way as it's simpler.
created_at: 2025-03-03T16:08:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+library RateChangeQueue {
+    struct RateChange {
+        uint256 rate;
+        uint256 untilEpoch;
+    }
+
+    struct Queue {
+        // Map from index to RateChange
+        mapping(uint256 => RateChange) changes;
+        uint256 head;
+        uint256 tail;
+    }
+
+    function enqueue(Queue storage queue, uint256 rate, uint256 untilEpoch) internal {
path: src/RateChangeQueue.sol
user: login: Stebalien
body: TODO: drop enqueued rate-changes if `queue.changes[queue.tail].untilEpoch == untilEpoch`.
created_at: 2025-03-04T01:16:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,700 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+        bool isLocked; // Indicates if the rail is currently being modified
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(!rails[railId].isLocked, "Modification already in progress");
+        rails[railId].isLocked = true;
+        _;
+        rails[railId].isLocked = false;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Implement Me
+    // https://github.com/FilOzone/payments/pull/1/files#r1974415624
+    function terminateOperator(address operator) external {}
+
+    function deposit(
+        address token,
+        address to,
+        uint256 amount
+    ) external nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external nonReentrant {
+        withdrawTo(token, msg.sender, amount);
+    }
+
+    function withdrawTo(
+        address token,
+        address to,
+        uint256 amount
+    ) public nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "recipient address cannot be zero");
+
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(to, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        require(approval.isApproved, "operator not approved");
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: Stebalien
body: We should probably skip all of this if the rate hasn't changed at all (if we're _just_ making a one-time payment. But it should be a no-op otherwise so I'm happy doing this in a new PR.
created_at: 2025-03-04T01:20:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+        }
+
+        require(
+            rail.lockupFixed >= oneTimePayment,
+            "one time payment cannot be greater than rail lockupFixed"
+        );
+        rail.lockupFixed = rail.lockupFixed - oneTimePayment;
+        rail.paymentRate = newRate;
+
+        require(
+            payer.lockupRate >= oldRate,
+            "payer lockup rate cannot be less than old rate"
+        );
+        payer.lockupRate = payer.lockupRate - oldRate + newRate;
+
+        require(
+            payer.lockupCurrent >=
+                ((oldRate * rail.lockupPeriod) + oneTimePayment),
+            "failed to modify rail payment: insufficient current lockup"
+        );
+        payer.lockupCurrent =
+            payer.lockupCurrent -
+            (oldRate * rail.lockupPeriod) +
+            (newRate * rail.lockupPeriod) -
+            oneTimePayment;
+
+        // --- Process the One-Time Payment ---
+        processOneTimePayment(payer, payee, oneTimePayment);
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer lockup cannot exceed funds"
+        );
+    }
+
+    function processOneTimePayment(
+        Account storage payer,
+        Account storage payee,
+        uint256 oneTimePayment
+    ) internal {
+        if (oneTimePayment > 0) {
+            require(
+                payer.funds >= oneTimePayment,
+                "insufficient funds for one-time payment"
+            );
+            payer.funds -= oneTimePayment;
+            payee.funds += oneTimePayment;
+        }
+    }
+
+    function validateAndModifyRateChangeApproval(
+        Rail storage rail,
+        OperatorApproval storage approval,
+        uint256 oldRate,
+        uint256 newRate,
+        uint256 oneTimePayment
+    ) internal {
+        // Ensure the one-time payment does not exceed the available fixed lockup on the rail.
+        require(
+            oneTimePayment <= rail.lockupFixed,
+            "one-time payment exceeds rail fixed lockup"
+        );
+
+        // Calculate the original total lockup amount:
+        uint256 oldTotalLockup = (oldRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+        uint256 newTotalLockup = (newRate * rail.lockupPeriod) +
+            rail.lockupFixed;
+
+        require(
+            newTotalLockup <= oldTotalLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldTotalLockup -
+            newTotalLockup;
+
+        require(
+            newRate <= oldRate + approval.rateAllowance,
+            "new rate exceeds operator rate allowance"
+        );
+        approval.rateAllowance = approval.rateAllowance + oldRate - newRate;
+    }
+
+    function settleRail(
+        uint256 railId,
+        uint256 untilEpoch
+    )
+        public
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        nonReentrant
+        returns (
+            uint256 totalSettledAmount,
+            uint256 finalSettledEpoch,
+            string memory note
+        )
+    {
+        require(
+            untilEpoch <= block.number,
+            "failed to settle: cannot settle future epochs"
+        );
+
+        Rail storage rail = rails[railId];
+
+        // Update the payer's lockup to account for elapsed time
+        Account storage payer = accounts[rail.token][rail.from];
+        settleAccountLockup(payer);
+
+        uint256 maxSettlementEpoch = min(
+            untilEpoch,
+            payer.lockupLastSettledAt + rail.lockupPeriod
+        );
+
+        uint256 startEpoch = rail.settledUpTo;
+
+        // nothing to settle (already settled or zero-duration)
+        if (startEpoch >= maxSettlementEpoch) {
+            return (0, startEpoch, "already settled upto requested epoch");
+        }
+
+        // for zero rate rails with empty queue, just advance the settlement epoch
+        // without transferring funds
+        uint256 currentRate = rail.paymentRate;
+        if (currentRate == 0 && rail.rateChangeQueue.isEmpty()) {
+            rail.settledUpTo = maxSettlementEpoch;
+            return (0, maxSettlementEpoch, "zero rate payment rail");
+        }
+
+        // Process settlement depending on whether rate changes exist
+        if (rail.rateChangeQueue.isEmpty()) {
+            (uint256 amount, string memory segmentNote) = _settleSegment(
+                railId,
+                startEpoch,
+                maxSettlementEpoch,
+                currentRate
+            );
+
+            require(rail.settledUpTo > startEpoch, "No progress in settlement");
+            return (amount, rail.settledUpTo, segmentNote);
+        } else {
+            return
+                _settleWithRateChanges(
+                    railId,
+                    currentRate,
+                    startEpoch,
+                    maxSettlementEpoch
+                );
+        }
+    }
+
+    function _settleWithRateChanges(
+        uint256 railId,
+        uint256 currentRate,
+        uint256 startEpoch,
+        uint256 targetEpoch
+    )
+        internal
+        returns (uint256 totalSettled, uint256 finalEpoch, string memory note)
+    {
+        Rail storage rail = rails[railId];
+        RateChangeQueue.Queue storage rateQueue = rail.rateChangeQueue;
+
+        totalSettled = 0;
+        uint256 processedEpoch = startEpoch;
+        note = "";
+
+        // Process each segment until we reach the target epoch or hit an early exit condition
+        while (processedEpoch < targetEpoch) {
+            // Default boundary is the target we want to reach
+            uint256 segmentEndBoundary = targetEpoch;
+            uint256 segmentRate;
+
+            // If we have rate changes in the queue, use the rate from the next change
+            if (!rateQueue.isEmpty()) {
+                RateChangeQueue.RateChange memory nextRateChange = rateQueue
+                    .peek();
+
+                // Validate rate change queue consistency
+                require(
+                    nextRateChange.untilEpoch >= processedEpoch,
+                    "rate queue is in an invalid state"
+                );
+
+                // Boundary is the minimum of our target or the next rate change epoch
+                segmentEndBoundary = min(
+                    targetEpoch,
+                    nextRateChange.untilEpoch
+                );
+                segmentRate = nextRateChange.rate;
+            } else {
+                // If queue is empty, use the current rail rate
+                segmentRate = currentRate;
+
+                // if current rate is zero, there's nothing left to do and we've finished settlement
+                if (segmentRate == 0) {
+                    rail.settledUpTo = targetEpoch;
+                    return (
+                        totalSettled,
+                        rail.settledUpTo,
+                        "Zero rate payment rail"
+                    );
+                }
+            }
+
+            // Settle the current segment with potentially arbitrated outcomes
+            (
+                uint256 segmentAmount,
+                string memory arbitrationNote
+            ) = _settleSegment(
+                    railId,
+                    processedEpoch,
+                    segmentEndBoundary,
+                    segmentRate
+                );
+
+            // If arbiter returned no progress, exit early without updating state
+            if (rail.settledUpTo <= processedEpoch) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Add the settled amount to our running total
+            totalSettled += segmentAmount;
+
+            // If arbiter partially settled the segment, exit early
+            if (rail.settledUpTo < segmentEndBoundary) {
+                return (totalSettled, rail.settledUpTo, arbitrationNote);
+            }
+
+            // Successfully settled full segment, update tracking values
+            processedEpoch = rail.settledUpTo;
+            note = arbitrationNote;
+
+            // Remove the processed rate change from the queue
+            if (!rateQueue.isEmpty()) {
+                rateQueue.dequeue();
+            }
+        }
+
+        // We've successfully settled up to the target epoch
+        return (totalSettled, rail.settledUpTo, note);
+    }
+
+    function _settleSegment(
+        uint256 railId,
+        uint256 epochStart,
+        uint256 epochEnd,
+        uint256 rate
+    ) internal returns (uint256 settledAmount, string memory note) {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        // Calculate the default settlement values (without arbitration)
+        uint256 duration = epochEnd - epochStart;
+        settledAmount = rate * duration;
+        uint256 settledUntilEpoch = epochEnd;
+        note = "";
+
+        // If this rail has an arbiter, let it decide on the final settlement amount
+        if (rail.arbiter != address(0)) {
+            IArbiter arbiter = IArbiter(rail.arbiter);
+            IArbiter.ArbitrationResult memory result = arbiter.arbitratePayment(
+                railId,
+                settledAmount,
+                epochStart,
+                epochEnd
+            );
+
+            // Ensure arbiter doesn't settle beyond our segment's end boundary
+            require(
+                result.settleUpto <= epochEnd,
+                "arbiter settled beyond segment end"
+            );
+
+            settledUntilEpoch = result.settleUpto;
+            settledAmount = result.modifiedAmount;
+            note = result.note;
+
+            // If arbiter made no progress, return early without updating any state
+            if (settledUntilEpoch <= epochStart) {
+                return (0, 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"
+            );
path: src/Payments.sol
user: login: aarshkshah1992
body: Done. See  https://github.com/FilOzone/payments/pull/2 .
created_at: 2025-03-04T08:21:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,793 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        address ownerAddress;
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    mapping(uint256 => bool) private _railModificationLocks;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(
+            !_railModificationLocks[railId],
+            "Modification already in progress"
+        );
+        _railModificationLocks[railId] = true;
+        _;
+        _railModificationLocks[railId] = false;
+    }
+
+    modifier validateRailAccountsExist(uint256 railId) {
+        Rail storage rail = rails[railId];
+        require(rail.from != address(0), "rail does not exist");
+        require(
+            accounts[rail.token][rail.from].ownerAddress != address(0),
+            "from account does not exist"
+        );
+        require(
+            accounts[rail.token][rail.to].ownerAddress != address(0),
+            "to account does not exist"
+        );
+        _;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    modifier onlyAccountOwner(address token) {
+        address owner = accounts[token][msg.sender].ownerAddress;
+        require(owner != address(0), "account does not exist");
+        require(owner == msg.sender, "not account owner");
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external onlyAccountOwner(token) {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    function terminateOperator(address operator) external {
+        require(operator != address(0), "operator address invalid");
+
+        uint256[] memory railIds = clientOperatorRails[msg.sender][operator];
+        for (uint256 i = 0; i < railIds.length; i++) {
+            Rail storage rail = rails[railIds[i]];
+            require(rail.from == msg.sender, "Not rail payer");
+            if (!rail.isActive) {
+                continue;
+            }
+
+            // Settle the rail up to the current block
+            (, uint256 settledUntilEpoch, ) = settleRail(
+                railIds[i],
+                block.number
+            );
+            require(
+                settledUntilEpoch == block.number,
+                "Failed to settle rail completely"
+            );
+
+            Account storage account = accounts[rail.token][msg.sender];
+
+            uint256 railLockup = rail.lockupFixed +
+                (rail.paymentRate * rail.lockupPeriod);
+            require(
+                account.lockupCurrent >= railLockup,
+                "Lockup accounting error"
+            );
+            account.lockupCurrent -= railLockup;
+
+            // Check to avoid underflow
+            require(
+                account.lockupRate >= rail.paymentRate,
+                "Rate accounting error"
+            );
+            account.lockupRate -= rail.paymentRate;
+
+            // Set rail parameters to zero
+            rail.paymentRate = 0;
+            rail.lockupFixed = 0;
+            rail.lockupPeriod = 0;
+            rail.isActive = false;
+
+            // Update operator approval
+            OperatorApproval storage approval = operatorApprovals[rail.token][
+                msg.sender
+            ][operator];
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+            approval.isApproved = false;
+
+            // Ensure invariant: lockup should never exceed funds
+            require(
+                account.lockupCurrent <= account.funds,
+                "Lockup exceeds funds after terminating operator"
+            );
+        }
+    }
+
+    function deposit(address token, address to, uint256 amount) external {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+        if (account.ownerAddress == address(0)) {
+            account.ownerAddress = to;
+        }
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(
+        address token,
+        uint256 amount
+    ) external onlyAccountOwner(token) nonReentrant {
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(msg.sender, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved, if not, auto-approve with zero allowances
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        if (!approval.isApproved) {
+            approval.isApproved = true;
+            approval.rateAllowance = 0;
+            approval.lockupAllowance = 0;
+        }
+
+        // Create to account if it doesn't exist
+        Account storage toAccount = accounts[token][to];
+        if (toAccount.ownerAddress == address(0)) {
+            toAccount.ownerAddress = to;
+        }
+
+        // Create from account if it doesn't exist
+        Account storage fromAccount = accounts[token][from];
+        if (fromAccount.ownerAddress == address(0)) {
+            fromAccount.ownerAddress = from;
+        }
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        validateRailAccountsExist(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: aarshkshah1992
body: This is done.
created_at: 2025-03-04T08:23:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+library RateChangeQueue {
+    struct RateChange {
+        uint256 rate;
+        uint256 untilEpoch;
+    }
+
+    struct Queue {
+        // Map from index to RateChange
+        mapping(uint256 => RateChange) changes;
+        uint256 head;
+        uint256 tail;
+    }
+
+    function enqueue(Queue storage queue, uint256 rate, uint256 untilEpoch) internal {
path: src/RateChangeQueue.sol
user: login: aarshkshah1992
body: @Stebalien I want to keep the queue implementation simpler. I've made this change at the call site in `modifyRailPayment`.
created_at: 2025-03-04T08:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,700 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "./RateChangeQueue.sol";
+
+interface IArbiter {
+    struct ArbitrationResult {
+        uint256 modifiedAmount;
+        uint256 settleUpto;
+        string note;
+    }
+
+    function arbitratePayment(
+        uint256 railId,
+        uint256 proposedAmount,
+        uint256 fromEpoch,
+        uint256 toEpoch
+    ) external returns (ArbitrationResult memory result);
+}
+
+contract Payments is
+    Initializable,
+    UUPSUpgradeable,
+    OwnableUpgradeable,
+    ReentrancyGuard
+{
+    using SafeERC20 for IERC20;
+    using RateChangeQueue for RateChangeQueue.Queue;
+
+    struct Account {
+        uint256 funds;
+        uint256 lockupCurrent;
+        uint256 lockupRate;
+        uint256 lockupLastSettledAt;
+    }
+
+    struct Rail {
+        bool isActive;
+        address token;
+        address from;
+        address to;
+        address operator;
+        address arbiter;
+        uint256 paymentRate;
+        uint256 lockupPeriod;
+        uint256 lockupFixed;
+        uint256 settledUpTo;
+        RateChangeQueue.Queue rateChangeQueue;
+        bool isLocked; // Indicates if the rail is currently being modified
+    }
+
+    struct OperatorApproval {
+        bool isApproved;
+        uint256 rateAllowance;
+        uint256 lockupAllowance;
+    }
+
+    // Counter for generating unique rail IDs
+    uint256 private _nextRailId;
+
+    // token => owner => Account
+    mapping(address => mapping(address => Account)) public accounts;
+
+    // railId => Rail
+    mapping(uint256 => Rail) internal rails;
+
+    // token => client => operator => Approval
+    mapping(address => mapping(address => mapping(address => OperatorApproval)))
+        public operatorApprovals;
+
+    // client => operator => railIds
+    mapping(address => mapping(address => uint256[]))
+        public clientOperatorRails;
+
+    /// @custom:oz-upgrades-unsafe-allow constructor
+    constructor() {
+        _disableInitializers();
+    }
+
+    function initialize() public initializer {
+        __Ownable_init(msg.sender);
+        __UUPSUpgradeable_init();
+    }
+
+    function _authorizeUpgrade(
+        address newImplementation
+    ) internal override onlyOwner {}
+
+    modifier validateRailActive(uint256 railId) {
+        require(rails[railId].from != address(0), "rail does not exist");
+        require(rails[railId].isActive, "rail is inactive");
+        _;
+    }
+
+    modifier noRailModificationInProgress(uint256 railId) {
+        require(!rails[railId].isLocked, "Modification already in progress");
+        rails[railId].isLocked = true;
+        _;
+        rails[railId].isLocked = false;
+    }
+
+    modifier onlyRailOperator(uint256 railId) {
+        require(
+            rails[railId].operator == msg.sender,
+            "only the rail operator can perform this action"
+        );
+        _;
+    }
+
+    function approveOperator(
+        address token,
+        address operator,
+        uint256 rateAllowance,
+        uint256 lockupAllowance
+    ) external {
+        require(token != address(0), "token address cannot be zero");
+        require(operator != address(0), "operator address cannot be zero");
+
+        OperatorApproval storage approval = operatorApprovals[token][
+            msg.sender
+        ][operator];
+        approval.rateAllowance = rateAllowance;
+        approval.lockupAllowance = lockupAllowance;
+        approval.isApproved = true;
+    }
+
+    // TODO: Implement Me
+    // https://github.com/FilOzone/payments/pull/1/files#r1974415624
+    function terminateOperator(address operator) external {}
+
+    function deposit(
+        address token,
+        address to,
+        uint256 amount
+    ) external nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+        require(amount > 0, "amount must be greater than 0");
+
+        // Create account if it doesn't exist
+        Account storage account = accounts[token][to];
+
+        // Transfer tokens from sender to contract
+        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+
+        // Update account balance
+        account.funds += amount;
+
+        // settle account lockup now that we have more funds
+        settleAccountLockup(account);
+    }
+
+    function withdraw(address token, uint256 amount) external nonReentrant {
+        withdrawTo(token, msg.sender, amount);
+    }
+
+    function withdrawTo(
+        address token,
+        address to,
+        uint256 amount
+    ) public nonReentrant {
+        require(token != address(0), "token address cannot be zero");
+        require(to != address(0), "recipient address cannot be zero");
+
+        Account storage acct = accounts[token][msg.sender];
+
+        (bool funded, uint256 settleEpoch) = settleAccountLockup(acct);
+        require(funded && settleEpoch == block.number, "insufficient funds");
+
+        uint256 available = acct.funds > acct.lockupCurrent
+            ? acct.funds - acct.lockupCurrent
+            : 0;
+
+        require(
+            amount <= available,
+            "insufficient unlocked funds for withdrawal"
+        );
+        acct.funds -= amount;
+        IERC20(token).safeTransfer(to, amount);
+    }
+
+    function createRail(
+        address token,
+        address from,
+        address to,
+        address arbiter
+    ) external noRailModificationInProgress(_nextRailId) returns (uint256) {
+        address operator = msg.sender;
+        require(token != address(0), "token address cannot be zero");
+        require(from != address(0), "from address cannot be zero");
+        require(to != address(0), "to address cannot be zero");
+
+        // Check if operator is approved
+        OperatorApproval storage approval = operatorApprovals[token][from][
+            operator
+        ];
+        require(approval.isApproved, "operator not approved");
+
+        uint256 railId = _nextRailId++;
+
+        Rail storage rail = rails[railId];
+        rail.token = token;
+        rail.from = from;
+        rail.to = to;
+        rail.operator = operator;
+        rail.arbiter = arbiter;
+        rail.isActive = true;
+        rail.settledUpTo = block.number;
+
+        clientOperatorRails[from][operator].push(railId);
+        return railId;
+    }
+
+    function modifyRailLockup(
+        uint256 railId,
+        uint256 period,
+        uint256 lockupFixed
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+
+        require(approval.isApproved, "operator not approved");
+
+        // settle account lockup and if account lockup is not settled upto to the current epoch; revert
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        require(
+            fullySettled && lockupSettledUpto == block.number,
+            "cannot modify lockup as client does not have enough funds for current account lockup"
+        );
+
+        // Calculate the change in base lockup
+        uint256 oldLockup = rail.lockupFixed +
+            (rail.paymentRate * rail.lockupPeriod);
+        uint256 newLockup = lockupFixed + (rail.paymentRate * period);
+
+        // assert that operator allowance is respected
+        require(
+            newLockup <= oldLockup + approval.lockupAllowance,
+            "exceeds operator lockup allowance"
+        );
+        approval.lockupAllowance =
+            approval.lockupAllowance +
+            oldLockup -
+            newLockup;
+
+        // Update payer's lockup
+        require(
+            payer.lockupCurrent >= oldLockup,
+            "payer's current lockup cannot be less than old lockup"
+        );
+        payer.lockupCurrent = payer.lockupCurrent - oldLockup + newLockup;
+
+        // Update rail lockup parameters
+        rail.lockupPeriod = period;
+        rail.lockupFixed = lockupFixed;
+
+        require(
+            payer.lockupCurrent <= payer.funds,
+            "payer's current lockup cannot be greater than their funds"
+        );
+    }
+
+    function modifyRailPayment(
+        uint256 railId,
+        uint256 newRate,
+        uint256 oneTimePayment
+    )
+        external
+        validateRailActive(railId)
+        onlyRailOperator(railId)
+        noRailModificationInProgress(railId)
+    {
+        Rail storage rail = rails[railId];
+        Account storage payer = accounts[rail.token][rail.from];
+        Account storage payee = accounts[rail.token][rail.to];
+
+        OperatorApproval storage approval = operatorApprovals[rail.token][
+            rail.from
+        ][rail.operator];
+        require(approval.isApproved, "Operator not approved");
+
+        // Settle the payer's lockup.
+        // If we're changing the rate, we require full settlement.
+        (bool fullySettled, uint256 lockupSettledUpto) = settleAccountLockup(
+            payer
+        );
+        if (newRate != rail.paymentRate) {
+            require(
+                fullySettled && lockupSettledUpto == block.number,
+                "account lockup not fully settled; cannot change rate"
+            );
+        }
+
+        uint256 oldRate = rail.paymentRate;
+        // --- Operator Approval Checks ---
+        validateAndModifyRateChangeApproval(
+            rail,
+            approval,
+            oldRate,
+            newRate,
+            oneTimePayment
+        );
+
+        // --- Settlement Prior to Rate Change ---
+        // If there is no arbiter, settle the rail immediately.
+        if (rail.arbiter == address(0)) {
+            (, uint256 settledUpto, ) = settleRail(railId, block.number);
+            require(
+                settledUpto == block.number,
+                "not able to settle rail upto current epoch"
+            );
+        } else {
+            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
path: src/Payments.sol
user: login: aarshkshah1992
body: Doing this in the next PR.
created_at: 2025-03-04T08:25:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -326,7 +326,14 @@ contract Payments is
                 "not able to settle rail upto current epoch"
             );
         } else {
-            rail.rateChangeQueue.enqueue(rail.paymentRate, block.number);
+            // handle multiple rate changes by the operator in a single epoch
+            // by queueuing the previous rate only once
+            if (
+                rail.rateChangeQueue.isEmpty() ||
path: src/Payments.sol
user: login: aarshkshah1992
body: @Stebalien FYI
created_at: 2025-03-04T08:29:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: Stebalien
created_at: 2025-02-20T02:49:17Z
body: The re-entency issue:

We discussed a few approaches:

1. Lock everything preventing both read and write access to the entire contract while settling a rail. This would restrict access to important information, e.g., information about the rail.
2. Lock the rail only. Unfortunately, if we lock read access, we'd still be restricting information about the rail. Furthermore, if we don't adjust payee/payer funds while settling and still allow, e.g., the arbiter to _see_ those fields, the arbiter won't see the "full" effects of the last section of payment it arbitrated.

We settled on:

1. Locking modifications (preventing settling, changing lockup, etc.) to the rail while settling.
2. Updating the rail, the payee, and the payer after _each_ call to the arbiter such that the arbiter always sees the payment contract in the correct state. Effectively, we move most of the rail settlement logic into the "settle segment" function.

Thinking on this more, there _are_ a few more solutions (which I don't like but I figured I'd mention for completeness).

1. We _could_ not lock anything, record important state before calling the arbiter, call the arbiter, then revert if "something has changed". This "works" but is strictly more brittle and doesn't provide any benefit.
2. We _could_ allow payment/lockup changes as long as we're really careful as those changes can only affect future time periods. But I don't think it's worth it and re-entrant modifications are likely always bugs.
author_association: CONTRIBUTOR
user: login: Stebalien
created_at: 2025-02-20T02:51:22Z
body: We also discussed how this relates to withdrawal. IMO, the difference between this and withdrawal is that all the "withdraw" call can do is revert. It can't cause us to change our minds and withdraw _less_ than we initially intended so there are no funky intermediate states.
author_association: CONTRIBUTOR
user: login: Stebalien
created_at: 2025-02-27T23:13:24Z
body: This is looking really good. I'd say punt operator termination stuff to a new PR, finish up the last nits here, then we can merge it.
author_association: CONTRIBUTOR
user: login: aarshkshah1992
created_at: 2025-03-03T08:41:39Z
body: @Stebalien 

I've addressed all your comments.

Only one comment remains unresolved at https://github.com/FilOzone/payments/pull/1#discussion_r1976817452. Let's go over it in our sync today and we can then merge it.

I am now starting work on raising a new PR to implement operator termination and all the other design changes we've added to  https://www.notion.so/filecoindev/Payments-Design-Iteration-17ddc41950c180468806c7e0097c2e14?pvs=4 since our first draft.
author_association: CONTRIBUTOR
user: login: aarshkshah1992
created_at: 2025-03-03T10:27:57Z
body: @Stebalien Fixed the CI and removed the `configure` script.
author_association: CONTRIBUTOR
user: login: Stebalien
created_at: 2025-03-04T01:21:32Z
body: I'll let you press the merge button when you wake up.
author_association: CONTRIBUTOR