Skip to content

BatchToken Lifecycle

Every BatchToken follows a defined lifecycle from mint to burn. Understanding this lifecycle is essential for building integrations, writing tests, and reasoning about collateral state at any point in time.

stateDiagram-v2
    [*] --> DELIVERED : BatchToken.mintBatch()
Field agent submits delivery
    DELIVERED --> GRADED : TraceLog.updateStage(GRADED)
Quality officer signs off
    GRADED --> MILLED : TraceLog.updateStage(MILLED)
Mill operator confirms
    MILLED --> WAREHOUSED : TraceLog.updateStage(WAREHOUSED)
Warehouse manager confirms
UWRSA receipt issued
    WAREHOUSED --> COMMITTED : PurchaseOrder.confirmPurchaseOrder()
Buyer PO confirmed
Auto-advances TraceLog
    COMMITTED --> EXPORTED : TraceLog.updateStage(EXPORTED)
UCDA export confirmed
⚡ Triggers LendingVault.onExported()
    EXPORTED --> SETTLED : LendingVault.onExported()
Auto-executes:
- Repay MFI pool
- Collect protocol fee
- Update CreditScore
- Emit NetDisbursementReady
    SETTLED --> [*] : BatchToken.burnSettled()
Token destroyed
StageHas loan available?LTVToken transferable?
DELIVEREDYes60%Yes (if no active loan)
GRADEDYes65%Yes (if no active loan)
MILLEDYes67%Yes (if no active loan)
WAREHOUSEDYes70%Yes (if no active loan)
COMMITTEDYes80%No (PO locks transfer)
EXPORTEDNo new loansNo (settlement in progress)
SETTLEDBurned

Loan State Machine (Parallel to Stage Machine)

Section titled “Loan State Machine (Parallel to Stage Machine)”
No loan:
DELIVERED → GRADED → MILLED → WAREHOUSED → COMMITTED
BatchToken transferable. Farmer receives coffee payment at DELIVERED.
With loan originated at DELIVERED:
DELIVERED (loan active, token locked)
→ GRADED (LTV increases, loan can be topped up)
→ MILLED
→ WAREHOUSED
→ COMMITTED (LTV increases to 80%)
→ EXPORTED (auto-repayment triggered)
→ SETTLED (loan closed, CreditScore +50, token burned)
With forbearance:
EXPORTED event delayed (harvest failure)
→ 90-day forbearance granted by multisig
→ If export occurs within 90 days: normal SETTLED flow
→ If no export after 90 days: credit loss reserve absorbs, MFI protected

The contract architecture enforces these invariants:

  • ❌ A BatchToken cannot go backward in stage (GRADED → DELIVERED is impossible)
  • ❌ A locked BatchToken cannot be transferred or used as collateral for a second loan
  • ❌ A token cannot be burned before SETTLED (burn requires VAULT_ROLE, only callable by LendingVault)
  • ❌ A DDS cannot be generated for a token below GRADED stage
  • ❌ Two PurchaseOrders cannot exist for the same BatchToken simultaneously
test/BatchTokenLifecycle.test.ts
describe('BatchToken full lifecycle', () => {
it('should complete DELIVERED → SETTLED with auto-repayment', async () => {
// 1. Register farmer
await farmerRegistry.registerFarmer(farmer.address, 'UG-TEST-001', ...);
// 2. Mint batch
const tokenId = await batchToken.mintBatch(coop.address, 'UG-TEST-001', ...);
// 3. Originate loan
await lendingVault.originate(tokenId, { from: farmer.address });
expect(await batchToken.hasActiveLoan(tokenId)).to.be.true;
// 4. Advance through stages
await traceLog.updateStage(tokenId, TraceStage.GRADED, evidenceCid);
await traceLog.updateStage(tokenId, TraceStage.MILLED, evidenceCid);
await traceLog.updateStage(tokenId, TraceStage.WAREHOUSED, evidenceCid);
// 5. Confirm PO
await purchaseOrder.createPurchaseOrder(tokenId, 'Test Buyer', 500e6);
await purchaseOrder.confirmPurchaseOrder(0);
// 6. Export — triggers auto-repayment
await traceLog.updateStage(tokenId, TraceStage.EXPORTED, evidenceCid);
// 7. Assert settlement
expect(await creditScore.getScore('UG-TEST-001')).to.equal(550); // 500 + 50
expect(await batchToken.exists(tokenId)).to.be.false; // burned
});
});