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
        );
    }
}