πΈ 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");
}
}
βοΈ 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;
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:
- Paste the code in Remix
- Deploy using Injected Web3 (MetaMask)
- 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)