DEV Community

Cover image for πŸ’Έ Day 7: Building an On-Chain IOU System with Solidity β€” Track and Settle Debts Securely
Saurav Kumar
Saurav Kumar

Posted on

πŸ’Έ Day 7: Building an On-Chain IOU System with Solidity β€” Track and Settle Debts Securely

πŸ’Έ Day 7: Building an On-Chain IOU System with Solidity β€” Track and Settle Debts Securely

Hey builders πŸ‘‹

Welcome to Day 7 of my #30DaysOfSolidity journey!
After learning about digital wallets and savings in previous projects, today we’re taking it a step further by creating something powerful and social β€” an on-chain IOU system for friends, small groups, or private DAOs.

This contract lets members deposit ETH, record who owes whom, and settle debts directly on-chain, all in a secure and transparent way.

Let’s dive in. πŸš€


🎯 What We’re Building

We’re creating a Solidity-based IOU system that works like a mini decentralized bank for a private group.

With this system:

  • Members can deposit and withdraw ETH
  • Record debts between members (borrower β†’ lender)
  • Repay debts either using on-chain ETH or internal balances
  • Admins can manage members and forgive debts if needed

It’s like a trust-minimized shared ledger that lives forever on the Ethereum blockchain.


🧠 Smart Contract Overview

The contract is named IOU and includes the following key features:

πŸ” Core Features

  • Membership Control: Only approved members can interact.
  • Deposits & Withdrawals: Users can safely manage ETH balances within the contract.
  • Internal Transfers: Members can send ETH to one another internally without on-chain gas costs.
  • Debt Management: Members can record, reduce, forgive, and repay debts.
  • Reentrancy Protection: Built-in reentrancy guard for safe withdrawals.

πŸ’» Full Solidity Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// @title IOU - Simple on-chain IOU system for a private group
/// @author Saurav
/// @notice Friends deposit ETH, record who owes whom, and settle debts on-chain.
/// @dev Uses nested mappings debts[borrower][lender] = amount borrower owes lender.
///      Contains a tiny ReentrancyGuard and uses call for safe ETH transfer.

contract IOU {
    // ------------------------
    // Events
    // ------------------------
    event MemberAdded(address indexed member);
    event MemberRemoved(address indexed member);
    event Deposit(address indexed who, uint256 amount);
    event Withdraw(address indexed who, uint256 amount);
    event InternalTransfer(address indexed from, address indexed to, uint256 amount);
    event DebtRecorded(address indexed borrower, address indexed lender, uint256 amount);
    event DebtReduced(address indexed borrower, address indexed lender, uint256 amount);
    event DebtForgiven(address indexed borrower, address indexed lender, uint256 amount);
    event DebtSettledOnChain(address indexed borrower, address indexed lender, uint256 amount, uint256 repaidWith);

    address public owner;

    mapping(address => uint256) private balances;
    mapping(address => mapping(address => uint256)) private debts;
    mapping(address => bool) public isMember;

    uint256 private _locked = 1;
    modifier nonReentrant() {
        require(_locked == 1, "Reentrant call");
        _locked = 2;
        _;
        _locked = 1;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner");
        _;
    }

    modifier onlyMember() {
        require(isMember[msg.sender], "Not a member");
        _;
    }

    constructor(address[] memory initialMembers) {
        owner = msg.sender;
        isMember[owner] = true;
        emit MemberAdded(owner);

        for (uint i = 0; i < initialMembers.length; i++) {
            if (initialMembers[i] != address(0) && !isMember[initialMembers[i]]) {
                isMember[initialMembers[i]] = true;
                emit MemberAdded(initialMembers[i]);
            }
        }
    }

    // Membership management
    function addMember(address _member) external onlyOwner {
        require(_member != address(0), "Zero address");
        require(!isMember[_member], "Already member");
        isMember[_member] = true;
        emit MemberAdded(_member);
    }

    function removeMember(address _member) external onlyOwner {
        require(isMember[_member], "Not a member");
        isMember[_member] = false;
        emit MemberRemoved(_member);
    }

    // Deposit / Withdraw
    receive() external payable {
        _deposit(msg.sender, msg.value);
    }

    fallback() external payable {
        _deposit(msg.sender, msg.value);
    }

    function deposit() external payable onlyMember {
        _deposit(msg.sender, msg.value);
    }

    function _deposit(address _who, uint256 _amount) internal {
        require(_amount > 0, "Zero deposit");
        balances[_who] += _amount;
        emit Deposit(_who, _amount);
    }

    function withdraw(uint256 amount) external nonReentrant onlyMember {
        require(amount > 0, "Zero withdraw");
        require(balances[msg.sender] >= amount, "Insufficient balance");

        balances[msg.sender] -= amount;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        emit Withdraw(msg.sender, amount);
    }

    // Internal transfers
    function transferInternal(address to, uint256 amount) external onlyMember {
        require(isMember[to], "Receiver not a member");
        require(to != address(0), "Zero address");
        require(balances[msg.sender] >= amount, "Insufficient balance");

        balances[msg.sender] -= amount;
        balances[to] += amount;
        emit InternalTransfer(msg.sender, to, amount);
    }

    // Debt recording
    function recordDebt(address borrower, address lender, uint256 amount) external onlyMember {
        require(amount > 0, "Zero amount");
        require(isMember[borrower] && isMember[lender], "Both must be members");
        require(borrower != lender, "Same person");

        debts[borrower][lender] += amount;
        emit DebtRecorded(borrower, lender, amount);
    }

    function reduceDebt(address borrower, address lender, uint256 amount) external onlyOwner {
        require(amount > 0, "Zero amount");
        uint256 current = debts[borrower][lender];
        require(current >= amount, "Reduce > debt");
        debts[borrower][lender] = current - amount;
        emit DebtReduced(borrower, lender, amount);
    }

    function forgiveDebt(address borrower, address lender) external onlyOwner {
        uint256 amt = debts[borrower][lender];
        if (amt > 0) {
            debts[borrower][lender] = 0;
            emit DebtForgiven(borrower, lender, amt);
        }
    }

    // Settle Debt On-Chain
    function repayOnChain(address payable lender, uint256 amount) external payable nonReentrant onlyMember {
        require(amount > 0, "Zero repay");
        require(isMember[lender], "Lender must be member");

        uint256 currentDebt = debts[msg.sender][lender];
        require(currentDebt >= amount, "Repay > debt");

        uint256 remaining = amount;

        if (msg.value > 0) {
            if (msg.value >= remaining) {
                (bool s, ) = lender.call{value: remaining}("");
                require(s, "Pay lender failed");
                emit DebtSettledOnChain(msg.sender, lender, amount, remaining);

                uint256 extra = msg.value - remaining;
                if (extra > 0) {
                    balances[msg.sender] += extra;
                    emit Deposit(msg.sender, extra);
                }
                debts[msg.sender][lender] = currentDebt - amount;
                return;
            } else {
                (bool s2, ) = lender.call{value: msg.value}("");
                require(s2, "Partial pay failed");
                remaining -= msg.value;
            }
        }

        require(balances[msg.sender] >= remaining, "Insufficient internal balance");
        balances[msg.sender] -= remaining;

        (bool s3, ) = lender.call{value: remaining}("");
        require(s3, "Transfer failed");

        debts[msg.sender][lender] = currentDebt - amount;
        emit DebtSettledOnChain(msg.sender, lender, amount, msg.value + remaining);
    }

    // Getters
    function balanceOf(address who) external view returns (uint256) {
        return balances[who];
    }

    function debtOf(address borrower, address lender) external view returns (uint256) {
        return debts[borrower][lender];
    }

    function totalDebtOf(address borrower, address[] calldata lenders) external view returns (uint256) {
        uint256 total = 0;
        for (uint i = 0; i < lenders.length; i++) {
            total += debts[borrower][lenders[i]];
        }
        return total;
    }

    // Emergency owner withdraw
    function emergencyWithdraw(address payable to, uint256 amount) external onlyOwner nonReentrant {
        require(to != address(0), "Zero address");
        (bool s, ) = to.call{value: amount}("");
        require(s, "Emergency withdraw failed");
    }
}
Enter fullscreen mode Exit fullscreen mode

βš™οΈ How It Works

πŸ’° Deposit & Withdraw

Members can deposit ETH into the contract to maintain an internal balance and withdraw whenever they need.

πŸ”„ Record and Track Debts

The contract keeps track of β€œwho owes whom” using a nested mapping:

mapping(address => mapping(address => uint256)) private debts;
Enter fullscreen mode Exit fullscreen mode

Example:
If Alice borrows 1 ETH from Bob β†’ debts[Alice][Bob] = 1 ETH

πŸ’Έ Settle Debts On-Chain

Borrowers can repay directly from:

  • Their internal balance
  • Or by sending ETH with the transaction All repayments are transparent, verifiable, and logged via events.

🏦 Real-World Use Cases

This simple IOU system can power:

  • Private group savings or lending clubs
  • Small DAO treasuries
  • On-chain bookkeeping between friends or colleagues
  • DeFi micro-lending prototypes

It’s a great foundation for more advanced systems like credit protocols, collateralized loans, or community trust networks.


🧩 Future Enhancements

Here’s what could make it even more robust:

  • πŸ” Multisig-based governance for member management
  • πŸ“Š Debt visualization dashboard
  • πŸͺ™ ERC20 token integration instead of ETH
  • 🧾 Interest or repayment deadlines
  • βš–οΈ Dispute resolution mechanism

🌐 Deploy & Test

You can easily deploy it using:

  • Remix IDE (best for testing)
  • Hardhat or Foundry (for professional workflows)

Steps:

  1. Paste the code in Remix
  2. Deploy using Injected Web3 (MetaMask)
  3. Interact with deposit, recordDebt, and repayOnChain functions

✨ Conclusion

The IOU Smart Contract brings trust, transparency, and accountability into financial relationships β€” without relying on middlemen.

It’s a perfect demonstration of how Solidity can make financial agreements autonomous and enforceable on-chain.


🧡 Follow My #30DaysOfSolidity Journey

πŸ“˜ GitHub β†’ 30 Days of Solidity Submissions
🐦 Twitter β†’ @sauravkumar8178
πŸ’Ό LinkedIn β†’ Saurav Kumar

Top comments (0)