Skip to content
jorgesandevPublic

About

1st place at Ethereum Mexico 2025 - decentralized invoice financing on Arbitrum.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

LiquiFi

Invoice financing, built as an unaudited hackathon prototype.

1st place, Arbitrum Innovation Track, Ethereum México 2025 — team-reported award.

LiquiFi explores how a small business could obtain liquidity against an invoice represented by an NFT. A liquidity provider deposits test tokens into an ERC-4626 vault; a borrower locks an invoice NFT, borrows up to 70% of its simulated value, and repays principal plus interest to recover it. This repository demonstrates the software flow, not verified receivables, a security audit, or a production lending product. Never use real funds.

LiquiFi vault in the local demonstration

Actual local browser test with disposable accounts and mUSDC, which has no monetary value. Borrower flow screenshot.

What is demonstrated

Area Implemented and locally tested Boundary
LP flow Wallet deposit, shares, exact-asset withdrawal Withdrawals limited to idle cash; no withdrawal queue
Borrower flow Upload → mint → NFT approval → borrow → repay → NFT returned Uploaded invoice values are simulated
Accounting Cash + outstanding principal; one repayment collection; principal write-off on default Interest recognized only at repayment
Authorization Signed requests, ownership checks, one-use write nonces, mint allowlist EOA wallets; not a complete identity or abuse-prevention system
Database Migrations, invoice deduplication, atomic mint reservation, verified loan-origin records Local Postgres/PostgREST tested; hosted Supabase untested
ENS Optional same-chain contract gate tested with a mock NameWrapper Browser KYB only reserves a database label; no mainnet registration

The deterministic KYB score is 90/100 for every demo organization. Invoice debtor names are fictional, and the amount is derived from a file hash. No SAT/CFDI validation, exchange rate, corporate registry, credit assessment, or legal claim verification exists.

Run from a clean checkout

Use Node 22 LTS, Bun 1.3.14, and Git. Docker/Compose is needed only for the full local API/browser demo. Bun lockfiles are authoritative. Downloads of dependencies, Solidity's compiler and browser binaries require internet access; tests themselves use local services.

git clone https://github.com/jorgesandev/liquifi.git
cd liquifi
bun install --frozen-lockfile
(cd contracts && bun install --frozen-lockfile)
bun run compile
bun run check:abis
bun run test                 # unit + in-process Hardhat tests
bun run lint
bun run typecheck
(cd contracts && bun run typecheck)
bun run build                # succeeds without production credentials

Use bun run test, not bun test, to run the configured combined test command.

Short contract-only demonstration

cd contracts
bunx hardhat run scripts/demo-local.ts

This uses an ephemeral in-process chain, deploys local test contracts, deposits 100,000 mUSDC, borrows 7,000 against a 10,000 invoice, advances 30 days, and repays. Assertions check single collection, zero remaining receivables, a closed loan, and returned collateral. The script refuses non-local networks. No database, browser wallet, or external key is needed.

Full local API and browser demonstration

Start from a fresh fixture. Ports 8545, 3000, and 55321 must be available. No hosted Supabase account is needed: Compose supplies disposable PostgreSQL and PostgREST behind a Supabase-compatible /rest/v1/ URL. It does not emulate Supabase Auth or Storage.

Terminal 1:

cd contracts
bunx hardhat node --hostname 127.0.0.1

Terminal 2, at the repository root:

docker compose -p liquifi-demo -f compose.demo.yml up -d --wait
bun run setup:local
bun --env-file=.env.demo run dev --hostname 127.0.0.1

The setup script deploys to localhost only, seeds 500,000 mUSDC in the vault and 200,000 in local borrower account #2, and writes an ignored, permission-restricted .env.demo. It refuses to overwrite that file. The generated operator key is Hardhat's publicly known fixture key, explicitly configured for this local demo. It is never a runtime fallback. The local database JWT expires after 24 hours.

Terminal 3:

bun run test:integration      # real HTTP routes, DB, generated ABIs and local contracts
bunx playwright install chromium
bun run test:browser          # Chromium + an injected local test-wallet adapter

Open the local app to explore it manually. In a disposable wallet profile, use Hardhat account #2 from the local node output and network 31337 at http://127.0.0.1:8545. Upload a fictional .xml or .pdf, mint, borrow, and repay. The optional KYB step does not gate loans. The generated allowlist permits only account #2 to request server-paid mints. The investor page supports deposits and cash-limited withdrawals. After refreshing the borrower page, enter a loan ID to read its live status and repay it.

Stop the two terminals and run docker compose -p liquifi-demo -f compose.demo.yml down to discard this demo's database. The database uses tmpfs and is intentionally non-persistent. Move aside .env.demo before starting a new fixture. Never reuse that file with public RPCs. Do not run the time-advancing contract demo against the browser fixture: off-chain due dates use wall-clock time. Use the in-process contract command above instead.

Architecture and accounting

flowchart LR
  Browser[Next.js / wallet] -->|signed request| API[Next.js API]
  API -->|service role| DB[(Postgres / PostgREST)]
  API -->|allowlisted operator mint| NFT[Invoice NFT]
  Browser -->|wallet transactions| Manager[LoanManager]
  Browser -->|deposit / withdraw| Vault[ERC-4626 vault]
  Manager -->|escrow NFT| NFT
  Manager -->|lend / collect once / write off| Vault
Loading
  • Asset units: integer base units, 6 decimals; database amounts are digit strings to preserve JSON precision. No floating-point arithmetic determines transaction amounts. Generated Solidity ABIs are committed in lib/abis.ts; CI checks they match freshly compiled artifacts (bun run generate:abis regenerates them).
  • Lending: positive principal ≤ floor(invoice amount × 7000 / 10000). Only the NFT owner may borrow; the NFT moves to the vault. One loan per NFT, ever. Invoices at or past their due timestamp cannot originate a loan. Byte-identical uploads are deduplicated; modified files and off-platform duplicate pledges are not detected.
  • Interest: floor(principal × 1000 × elapsedSeconds / (10000 × 365 days)). It continues after maturity until repayment or liquidation. This is a borrower rate, not an LP APY.
  • Repayment: borrower approves the vault, then calls repayLoan(id, maximumPayment). The vault pulls the exact amount owed at execution once; only principal reduces receivables. The UI quotes current debt and authorizes a buffer of 1% plus 1 mUSDC, which is a payment cap rather than an extra fee. Unused allowance remains until revoked.
  • Valuation: totalAssets = cash + outstanding principal; unpaid interest is excluded. Default write-offs reduce share value. maxWithdraw/maxRedeem are cash-capped; tiny deposits that would mint zero shares revert. ERC-4626 rounding can leave dust.
  • Default: anyone may liquidate strictly after the invoice due date + two days. Principal is written off once and the NFT remains in the vault. There is no auction or recovery. A late borrower may still repay before someone liquidates; whichever transaction confirms first determines the outcome.
  • ENS: when a nonzero same-chain registrar is configured, a nonempty authorized label is mandatory. A mainnet address cannot enforce authorization on Arbitrum without a bridge; no bridge exists here. The default local configuration has no registrar.

API trust and configuration

See .env.example. Missing signing key, allowlist, database configuration, request origin or server RPC fails explicitly; no signing key is substituted. The server checks RPC chain identity and supports only localhost 31337 or Arbitrum Sepolia 421614. Mainnet signing was removed from the web API. Historical deployment scripts are retained, but were not run or validated on live networks during hardening.

Route Access and behavior
POST /api/invoices Signed upload, ≤5 MB, hash-derived demo data and wallet ownership
GET /api/invoices/[id] Resource-bound signed read; only the owning wallet
POST /api/mint Signed + owner + operator allowlist; atomically reserves one mint attempt
POST /api/loans/record Checks actual LoanInitiated receipt and borrower; keyed by chain + manager + loan
GET/POST /api/kyb Signed, own organization only; deterministic simulation
POST /api/mint-musdc Signed, allowlisted, feature-flagged; up to 10,000 test tokens per request
POST /api/ens/check-label Public database label availability; not an ENS registry query

Signatures bind origin, action, HTTP method, path/query, exact body hash, timestamp and nonce. Write nonces are consumed atomically in Postgres and cannot be replayed across workers. Reads are replayable only for that same resource within the five-minute validity window. Expired nonce rows may be pruned after one day. This does not provide rate limits, a gas budget, SIWE sessions, contract-wallet signatures, or production key custody.

For a manually configured Supabase instance, apply 0001_init.sql, then 20260920141418_hardening.sql from supabase/migrations/. All application tables have RLS enabled and no anonymous policies. add_ens_columns.sql is historical; the base schema includes those columns. These scripts are tested for fresh databases, not unknown existing hackathon schemas. Back up and compare any existing schema before migrating it.

Minting and a database commit cannot be one atomic blockchain operation. A failed or ambiguous attempt remains in minting; it is not automatically retried. See reconciliation instructions. Loan DB rows record origination and may remain active after repayment; the UI reads live contract state and does not invent status.

Limitations worth discussing in an interview

  • No security audit or production-readiness claim. Owner powers remain broad: invoice minting/deactivation, vault manager replacement, and registrar changes.
  • Defaults are recognized only when liquidated. Until then bad debt is still valued at par; early withdrawals can shift losses to remaining LPs. New depositors can share previously accrued but unpaid interest. There is no robust lending-market valuation model.
  • Standard, non-rebasing, non-fee-on-transfer 6-decimal MockUSDC is the tested asset. Do not assume this accounting works with arbitrary ERC-20s or real USDC.
  • No minimum-shares-out protection for deposits beyond rejecting zero shares; donations and repricing can still create slippage.
  • No real invoice ownership proof, underwriting, collections, or cross-chain identity.
  • No operator gas budget or transaction queue. The allowlist is suitable for a controlled local demo; concurrent different mint operations can still require manual reconciliation.
  • Browser tests use an injected test-wallet adapter, not a real MetaMask extension. Hardware wallets, mobile wallets, rejection/replacement UX and hosted Supabase remain untested.
  • Existing testnet deployments do not demonstrate the current source. No live deployment was performed. See verification results and dependency findings.

My contribution

Repository history attributes the commits to Jorge Alejandro Sandoval Romo (jorgesandev / Jorgexe). The portfolio contribution is the end-to-end prototype integration: Next.js borrower/investor UI, wallet transactions, invoice persistence, and contract interaction. The hackathon award is credited to the team, not claimed as an individual award.

Post-hackathon hardening was AI-assisted: repayment/accounting fixes, request authorization, regression tests, reproducible local infrastructure, CI, and documentation. The repository does not establish a precise division of work among hackathon teammates; confirm that attribution before adding a more specific résumé claim.

Project map

  • contracts/ — Solidity 0.8.28 (Cancun target), OpenZeppelin 5, Hardhat, fixtures and tests.
  • app/, components/, lib/ — Next.js, React, wagmi/viem, ethers server calls.
  • supabase/migrations/ — schema and hardening migration.
  • compose.demo.yml, scripts/local/ — disposable database/API fixture.
  • tests/, scripts/test-local-api.ts — unit, browser and HTTP integration checks.
  • .github/workflows/ci.yml — checks requiring no production credentials.

No license is included. The repository is available for review; contact the owner about reuse.

About

1st place at Ethereum Mexico 2025 - decentralized invoice financing on Arbitrum.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages