Debt Splitter

View on GitHub


Debt Splitter is a trustless expense-splitting protocol built entirely in Solidity. No backend, no server — expenses and debts live on-chain. A factory contract deploys per-group instances that track member balances, compute minimal-transfer debt settlements with a greedy two-pointer algorithm, and settle everything in ETH atomically.

Highlights

  • Trustless — no backend, no server; expenses and debts live on-chain
  • RegistryGroups — factory that deploys and indexes Group contracts
  • Group — tracks member balances, computes debts via a greedy algorithm, and handles ETH settlement

Contract Architecture

RegistryGroups  (factory + global registry)
  ├── deploys Group instances
  └── holds a shared Utils instance

Group           (per-group logic)
  ├── addExpense(payer, amount)
  ├── split()
  ├── payAll()  { payable }
  └── V2 stubs: addMember, pay, leaveGroup, dissolveGroup

Utils           (stateless helpers, deployed once)
  ├── abs(int256) → uint256
  └── isSorted / isSortedDesc

Balance Model

Each member holds an int256 balance:

SignMeaning
PositiveCreditor — is owed money
NegativeDebtor — owes money
ZeroSettled

When addExpense(payer, amount) is called, the cost is split evenly among all members (amount / totalMembers). Integer-division remainder stays with the payer — no wei is ever trapped in the contract.

Debt Algorithm (splitgetDebts)

split() partitions members into debtors (balance < 0) and creditors (balance ≥ 0), then calls getDebts(), which applies a greedy two-pointer algorithm to minimise the number of transfers required. Sorting by absolute balance descending is done off-chain (caller’s responsibility) to keep gas costs low.

Settlement with payAll

payAll() is payable and requires msg.value to exactly equal the caller’s total outstanding debt. It iterates over the debt array, transfers ETH directly to each creditor via .call{value}(), and updates all balances in the same block. Partial payments are not supported in V1.

Flow

User ──► addExpense(payer, amount) ──► balances updated ──────────────────────► ExpenseAdded(...)
User ──► split()                   ──► getDebts() greedy ──► debts[] stored  ──► DebtsSplit(...)
User ──► payAll() { value }        ──► .call each creditor ──► balances reset ──► AllPaid(...)

Test Coverage

ContractLinesStatementsBranchesFunctions
Group.sol92.59%98.95%82.61%58.33%
RegistryGroups.sol100.00%100.00%100.00%100.00%
Utils.sol27.27%17.65%25.00%33.33%
Total86.14%87.70%74.07%58.82%