Tokenized Expense Reimbursement
What it does:
A smart contract that allows organizations or DAOs to reimburse approved expenses using tokens, with every claim and payout recorded transparently on-chain.
Why it matters:
Traditional reimbursements are slow, opaque, and paperwork-heavy. This contract automates approval and payment, creating a real-time, auditable reimbursement system.
How it works:
-
The treasury deposits reimbursement tokens into the contract.
-
Users submit expense claims with metadata (hash, category, amount).
-
An authorized approver (or DAO) approves or rejects claims.
-
Approved claims mint or release tokens to the claimant.
-
All claims and payouts are permanently recorded on-chain.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title TokenizedExpenseReimbursement
* @author Nam
* @notice On-chain expense reimbursement using ERC20 tokens
*/
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
contract TokenizedExpenseReimbursement {
// -------------------- DATA STRUCTURES --------------------
enum ClaimStatus { Pending, Approved, Rejected, Paid }
struct Claim {
address claimant;
uint256 amount;
string metadata; // IPFS / hash reference
ClaimStatus status;
}
IERC20 public immutable token;
address public approver;
uint256 public claimCount;
mapping(uint256 => Claim) public claims;
// -------------------- EVENTS --------------------
event ClaimSubmitted(uint256 indexed claimId, address claimant, uint256 amount);
event ClaimApproved(uint256 indexed claimId);
event ClaimRejected(uint256 indexed claimId);
event ClaimPaid(uint256 indexed claimId, address claimant, uint256 amount);
// -------------------- MODIFIERS --------------------
modifier onlyApprover() {
require(msg.sender == approver, "Not approver");
_;
}
// -------------------- CONSTRUCTOR --------------------
constructor(address _token, address _approver) {
require(_token != address(0), "Invalid token");
require(_approver != address(0), "Invalid approver");
token = IERC20(_token);
approver = _approver;
}
// -------------------- CLAIM SUBMISSION --------------------
/**
* @notice Submit an expense claim
*/
function submitClaim(
uint256 _amount,
string calldata _metadata
) external {
require(_amount > 0, "Invalid amount");
claims[claimCount] = Claim({
claimant: msg.sender,
amount: _amount,
metadata: _metadata,
status: ClaimStatus.Pending
});
emit ClaimSubmitted(claimCount, msg.sender, _amount);
claimCount++;
}
// -------------------- APPROVAL LOGIC --------------------
/**
* @notice Approve a claim
*/
function approveClaim(uint256 _claimId)
external
onlyApprover
{
Claim storage c = claims[_claimId];
require(c.status == ClaimStatus.Pending, "Invalid status");
c.status = ClaimStatus.Approved;
emit ClaimApproved(_claimId);
}
/**
* @notice Reject a claim
*/
function rejectClaim(uint256 _claimId)
external
onlyApprover
{
Claim storage c = claims[_claimId];
require(c.status == ClaimStatus.Pending, "Invalid status");
c.status = ClaimStatus.Rejected;
emit ClaimRejected(_claimId);
}
// -------------------- PAYMENT LOGIC --------------------
/**
* @notice Pay approved claim
*/
function payClaim(uint256 _claimId)
external
onlyApprover
{
Claim storage c = claims[_claimId];
require(c.status == ClaimStatus.Approved, "Not approved");
c.status = ClaimStatus.Paid;
require(
token.transfer(c.claimant, c.amount),
"Token transfer failed"
);
emit ClaimPaid(_claimId, c.claimant, c.amount);
}
// -------------------- VIEW FUNCTIONS --------------------
/**
* @notice Get claim details
*/
function getClaim(uint256 _claimId)
external
view
returns (
address claimant,
uint256 amount,
string memory metadata,
ClaimStatus status
)
{
Claim memory c = claims[_claimId];
return (
c.claimant,
c.amount,
c.metadata,
c.status
);
}
}