The FWS Payments contract enables ERC20 token payment flows through "rails" - automated payment channels between clients and recipients. The contract supports continuous payments, one-time transfers, and payment arbitration.
- Account: Represents a user's token balance and locked funds
- Rail: A payment channel between a client and recipient with configurable terms
- Arbiter: An optional contract that can mediate payment disputes
- Operator: An authorized third party who can manage rails on behalf of clients
Tracks the funds, lockup, obligations, etc. associated with a single “owner” (where the owner is a smart contract or a wallet). Accounts can be both clients and SPs but we’ll often talk about them as if they were separate types.
- Client — An account that pays an SP (also referred to as the payer)
- SP — An account managed by a service provider to receive payment from a client (also referred to as the payee).
A rail along which payments flow from a client to an SP. Rails track lockup, maximum payment rates, and obligations between a client and an SP. Client-SP pairs can have multiple payment rails between them but they can also reuse the same rail across multiple deals. Importantly, rails: - Specify the maximum rate at which the client will pay the SP, the actual amount paid for any given period is subject to arbitration by the arbiter described below. - Specify the period in advanced the client is required to lock funds (the lockup period). There’s no way to force clients to lock funds in advanced, but we can prevent them from withdrawing them and make it easy for SPs to tell if their clients haven’t met their lockup minimums, giving them time to settle their accounts.
An arbiter is an (optional) smart contract that can arbitrate payments associated with a single rail. For example, a payment rail used for PDP will specify the PDP service as its arbiter An arbiter can:
- Prevent settlement of a payment rail entirely.
- Refuse to settle a payment rail past some epoch.
- Reduce the amount paid out by a rail for a period of time (e.g., to account for actual services rendered, penalties, etc.).
An operator is a smart contract (likely the service contract) that manages rails on behalf of clients & SPs, with approval from the client (the client approves the operator to spend its funds at a specific rate). The operator smart contract must be trusted by both the client and the SP as it can arbitrarily alter payments (within the allowance specified by the client). It:
- Creates rails from clients to service providers.
- Changes payment rates, lockups, etc. of payment rails created by this operator.
- The sum of payment rates across all rails operated by this contract for a specific client must be at most the maximum per-operator spend rate specified by the client.
- The sum of the lockup across all rails operated by this contract for a specific client must be at most the maximum per-operator lockup specified by the client.
- Specify/change the payment rail arbiter of payment rails created by this operator.
Deposits tokens into a specified account.
- Parameters:
token: ERC20 token contract addressto: Recipient account addressamount: Token amount to deposit
- Requirements:
- Caller must have approved the contract to transfer tokens
Withdraws available tokens from caller's account to caller's wallet.
- Parameters:
token: ERC20 token contract addressamount: Token amount to withdraw
- Requirements:
- Amount must not exceed unlocked funds
Withdraws available tokens from caller's account to a specified address.
- Parameters:
token: ERC20 token contract addressto: Recipient addressamount: Token amount to withdraw
- Requirements:
- Amount must not exceed unlocked funds
setOperatorApproval(address token, address operator, bool approved, uint256 rateAllowance, uint256 lockupAllowance)
Configures an operator's permissions to manage rails on behalf of the caller.
- Parameters:
token: ERC20 token contract addressoperator: Address to grant permissions toapproved: Whether the operator is approvedrateAllowance: Maximum payment rate the operator can set across all railslockupAllowance: Maximum funds the operator can lock for future payments
Creates a new payment rail between two parties.
- Parameters:
token: ERC20 token contract addressfrom: Client (payer) addressto: Recipient addressarbiter: Optional arbitration contract address (0x0 for none)
- Returns: Unique rail ID
- Requirements:
- Caller must be approved as an operator by the client
Retrieves the current state of a payment rail.
- Parameters:
railId: Rail identifier
- Returns: RailView struct with rail details
- Requirements:
- Rail must exist
Emergency termination of a payment rail, preventing new payments after the lockup period. This should only be used in exceptional cases where the operator contract is malfunctioning and refusing to cancel deals.
- Parameters:
railId: Rail identifier
- Requirements:
- Caller must be the rail's client and must have a fully funded account, or it must be the rail operator
- Rail must not be already terminated
Changes a rail's lockup parameters.
- Parameters:
railId: Rail identifierperiod: New lockup period in epochslockupFixed: New fixed lockup amount
- Requirements:
- Caller must be the rail operator
- For terminated rails: cannot change period or increase fixed lockup
- For active rails: changes restricted if client's account isn't fully funded
- Operator must have sufficient allowances
Modifies a rail's payment rate and/or makes a one-time payment.
- Parameters:
railId: Rail identifiernewRate: New per-epoch payment rateoneTimePayment: Optional immediate payment amount
- Requirements:
- Caller must be the rail operator
- For terminated rails: cannot increase rate
- For active rails: rate changes restricted if client's account isn't fully funded
- One-time payment must not exceed fixed lockup
Settles payments for a rail up to a specified epoch.
- Parameters:
railId: Rail identifieruntilEpoch: Target epoch (must not exceed current epoch)
- Returns:
totalSettledAmount: Amount transferredfinalSettledEpoch: Epoch to which settlement was completednote: Additional settlement information
- Requirements:
- Client must have sufficient funds to cover the payment
- Client's account must be fully funded or the rail must be terminated
- Cannot settle future epochs
Emergency settlement method for terminated rails with stuck arbitration.
- Parameters:
railId: Rail identifier
- Returns:
totalSettledAmount: Amount transferredfinalSettledEpoch: Epoch to which settlement was completednote: Additional settlement information
- Requirements:
- Caller must be rail client
- Rail must be terminated
- Current epoch must be past the rail's maximum settlement epoch
The contract supports optional payment arbitration through the IArbiter interface. When a rail has an arbiter:
- During settlement, the arbiter contract is called
- The arbiter can adjust payment amounts or partially settle epochs
- This provides dispute resolution capabilities for complex payment arrangements
This worked example demonstrates how users interact with the FWS Payments contract through a typical service deal lifecycle.
A client first deposits tokens to fund their account in the payments contract:
// 1. Client approves the Payments contract to spend tokens
IERC20(tokenAddress).approve(paymentsContractAddress, 100 * 10**18); // 100 tokens
// A client or anyone else can deposit to the client's account
Payments(paymentsContractAddress).deposit(
tokenAddress, // ERC20 token address
clientAddress, // Recipient's address (the client)
100 * 10**18 // Amount to deposit (100 tokens)
);After this operation, the client's Account.funds is credited with 100 tokens, enabling them to use services within the FWS ecosystem.
This operation may be deferred until the funds are actually required, funding is always "on-demand".
Before using a service, the client must approve the service's contract as an operator:
// Client approves a service contract as an operator
Payments(paymentsContractAddress).setOperatorApproval(
tokenAddress, // ERC20 token address
serviceContractAddress, // Operator address (service contract)
true, // Approval status
5 * 10**18, // Maximum rate (tokens per epoch) the operator can allocate
20 * 10**18 // Maximum lockup the operator can set
);This approval has two key components:
- The
rateAllowance(5 tokens/epoch) limits the total continuous payment rate across all rails created by this operator - The
lockupAllowance(20 tokens) limits the total fixed amount the operator can lock up for one-time payments or escrow
When a client proposes a deal with a service provider, the service contract (acting as an operator) creates a payment rail:
// Service contract creates a rail
uint256 railId = Payments(paymentsContractAddress).createRail(
tokenAddress, // Token used for payments
clientAddress, // Payer (client)
serviceProvider, // Payee (service provider)
arbiterAddress // Optional arbiter (can be address(0) for no arbitration)
);
// Set up initial lockup for onboarding costs - for example, 10 tokens as fixed lockup
Payments(paymentsContractAddress).modifyRailLockup(
railId, // Rail ID
100, // Lockup period (100 epochs)
10 * 10**18 // Fixed lockup amount (10 tokens for onboarding)
);At this point:
- A rail is established between the client and service provider
- The rail has a
fixedLockupof 10 tokens and alockupPeriodof 100 epochs - The payment
rateis still 0 (service hasn't started yet) - The client's account lockup threshold is increased by 10 tokens
When the service provider accepts the deal:
// Service contract (operator) increases the payment rate and makes a one-time payment
Payments(paymentsContractAddress).modifyRailPayment(
railId, // Rail ID
2 * 10**18, // New payment rate (2 tokens per epoch)
3 * 10**18 // One-time onboarding payment (3 tokens)
);This operation:
- Makes an immediate one-time payment of 3 tokens to the service provider, deducted from the rail's fixed lockup
- Updates the client's
lockupCurrentto include rate ×lockupPeriod - The client's account now locks
2 × 100 + (10-3) = 207tokens including the remaining fixed lockup, locking an additional 2 tokens every epoch
Payment settlement can be triggered by any rail participant:
// Settlement call - can be made by client, service provider, or operator
(uint256 amount, uint256 settledEpoch, string memory note) = Payments(paymentsContractAddress).settleRail(
railId, // Rail ID
block.number // Settle up to current epoch
);This settlement:
- Calculates amount owed based on rail's rate and time elapsed
- Transfers tokens from client's account to service provider's account
- If an arbiter is specified, it may modify the payment amount or limit settlement epochs
- Records the epoch up to which the rail has been settled
A rail may only be settled if either (a) the client's account is fully funded or (b) the rail is terminated (in which case the rail may be settled up to the rail's "end epoch").
If service terms change during the deal:
// Operator modifies payment parameters
Payments(paymentsContractAddress).modifyRailPayment(
railId, // Rail ID
4 * 10**18, // Increased rate (4 tokens per epoch)
0 // No one-time payment
);
// If lockup terms need changing
Payments(paymentsContractAddress).modifyRailLockup(
railId, // Rail ID
150, // Extended lockup period (150 epochs)
15 * 10**18 // Increased fixed lockup (15 tokens)
);When a user cancels a deal, the service contract will modify the rail's payment to take that into account. In this case, the service contract sets the rail's payment rate to zero and pays a fixed termination fee out of the rail's "fixed" lockup.
// Service contract reduces payment rate and possibly issues a termination payment
Payments(paymentsContractAddress).modifyRailPayment(
railId, // Rail ID
0, // Zero out payment rate
5 * 10**18 // Termination fee (5 tokens)
);After a terminated rail reaches its endEpoch, it can be fully settled to unlock all remaining funds.
// Final settlement
(uint256 amount, uint256 settledEpoch, string memory note) = Payments(paymentsContractAddress).settleRail(
railId, // Rail ID
rails[railId].endEpoch // Settle up to end epoch
);
// Client withdraws remaining funds
Payments(paymentsContractAddress).withdraw(
tokenAddress, // Token address
remainingAmount // Amount to withdraw
);If some component in the system (operator, arbiter, client, SP) misbehaves, all parties have escape hatches that allow them to walk away with predictable losses.
At any time, the client can reduce the operator's allowance (e.g., to zero) and/or change whether or not the operator is allowed to create new rails. Such modifications won't affect existing rails, although the operator will not be able to increase the payment rates on any rails they manage until they're back under their limits.
If something goes wrong (e.g., the operator is buggy and is refusing to terminate deals, stop payment, etc.), the client may terminate the to prevent future payment beyond the rail's lockup period. The client must ensure that their account is fully funded before they can terminate any rails.
// Client terminates the rail
Payments(paymentsContractAddress).terminateRail(railId);Termination:
- Forcibly reduces the rail's payment rate to zero
lockupPeriodepochs into the future. - Immediately stops locking new funds to the rail.
- Causes any fixed funds locked to the rail to automatically unlock after the
lockupPeriodelapses.
At any time, even if the client's account isn't fully funded, the operator can terminate a rail. This will allow the recipient to settle any funds available in the rail to receive partial payment.
If an arbiter contract is malfunctioning, the client may forcibly settle the rail the rail "in full" (skipping arbitration) to prevent the funds from getting stuck in the rail pending final arbitration. This can only be done after the rail has been terminated (either by the client or by the operator), and should be used as a last resort.
// Emergency settlement for terminated rails with stuck arbitration
(uint256 amount, uint256 settledEpoch, string memory note) = Payments(paymentsContractAddress).settleTerminatedRailWithoutArbitration(railId);