Skip to content

FarmerRegistry.sol

The first contract deployed. Stores verified farmer identities, Uganda NIN (National ID), phone numbers, MAAIF government IDs, GPS farm data references, and cooperative memberships. All other contracts depend on it.

:::important Contract Status ✅ Implemented — UUPS upgradeable. Sprint 1 added nationalId, farmerName, phoneNumber fields and cooperative self-management of agents. Sprint 3 added on-chain agent cap enforcement: max(3, ceil(totalFarmers/50)) enforced in _grantRole override. :::

  • Stores the canonical list of verified farmers keyed by deterministic NIN-derived address
  • Links Uganda NIN to wallet address (one-to-one, enforced via maaifToWallet reverse lookup)
  • Stores farmer name and phone number required for Fonbnk MTN MoMo payouts
  • References GPS polygon IPFS CIDs (not raw coordinates on-chain)
  • Cooperatives self-manage agents via _setRoleAdmin(AGENT_ROLE, COOP_ROLE) — no protocol admin bottleneck
  • Agent cap enforced on-chain: a cooperative can have at most max(3, ceil(totalFarmers/50)) active agents. The _grantRole override reverts if the cap is exceeded. _revokeRole decrements the counter.
  • GFW deforestation check is recorded at registration
struct Farmer {
// ─── Original fields ───
string maaifFarmerId; // MAAIF National Traceability System ID
address cooperativeWallet; // Cooperative wallet — or INDEPENDENT_AGGREGATOR
bytes32 farmBoundaryIpfsCid; // IPFS CID of GeoJSON polygon (EUDR DDS)
uint256 farmAreaHectares; // Farm area × 100 (e.g. 250 = 2.50 ha)
bool gfwDeforestationFree; // Global Forest Watch check result
bool active; // false = deactivated (soft delete)
uint256 registrationTimestamp;
// ─── Sprint 1 fields (append only) ───
string nationalId; // Uganda NIN — primary on-chain identity
string farmerName; // Human-readable name (Fonbnk recipientName)
string phoneNumber; // MTN phone (+2567XXXXXXXX) — Fonbnk recipientPhone
}
mapping(address => Farmer) public farmers; // wallet → Farmer
mapping(string => address) public maaifToWallet; // MAAIF ID / NIN → wallet (uniqueness guard)
address public INDEPENDENT_AGGREGATOR; // Set post-deploy via setIndependentAggregator()
// Register a new farmer — AGENT_ROLE required
// Wallet is derived deterministically from NIN (not passed by caller)
function registerFarmer(
address farmerWallet,
string calldata maaifFarmerId,
address cooperativeWallet,
bytes32 farmBoundaryIpfsCid,
uint256 farmAreaHectares,
bool gfwDeforestationFree,
string calldata nationalId, // NEW: Uganda NIN
string calldata farmerName, // NEW: farmer display name
string calldata phoneNumber // NEW: MTN phone for payouts
) external onlyRole(AGENT_ROLE);
// Returns true if farmer is registered and active
function isRegistered(address farmerWallet) external view returns (bool);
// Returns full Farmer struct. Reverts only if never registered.
function getFarmer(address farmerWallet) external view returns (Farmer memory);
// Returns true if farmer's cooperativeWallet == INDEPENDENT_AGGREGATOR.
function isIndependent(address farmerWallet) external view returns (bool);
// NEW: Returns the number of farmers registered under a cooperative wallet.
// Used by API to enforce agent caps: max(3, ceil(farmerCount / 50))
function getFarmerCount(address cooperativeWallet) external view returns (uint256);
// Update GFW deforestation status — AGENT_ROLE
function verifyFarmer(address farmerWallet, bool gfwDeforestationFree) external;
// Deactivate a farmer — AGENT_ROLE or COOP_ROLE
function deactivateFarmer(address farmerWallet) external;
// Set or update the INDEPENDENT_AGGREGATOR — DEFAULT_ADMIN_ROLE
function setIndependentAggregator(address aggregator) external;
// Migrate an independent farmer to a named cooperative — DEFAULT_ADMIN_ROLE
function migrateFarmer(address farmerWallet, address newCooperativeWallet) external;
bytes32 public constant AGENT_ROLE = keccak256("AGENT_ROLE");
bytes32 public constant COOP_ROLE = keccak256("COOP_ROLE");

Sprint 1: Cooperatives self-manage their agents:

_setRoleAdmin(AGENT_ROLE, COOP_ROLE);

This means:

  • Protocol admin grants COOP_ROLE once (cooperative wallet)
  • Cooperative manager grants AGENT_ROLE to field agents directly
  • Cooperative manager revokes AGENT_ROLE when an agent leaves
  • No protocol admin involvement in day-to-day agent management

Sprint 3 — Agent cap enforcement:

The contract tracks totalFarmers and agentCount counters. The _grantRole and _revokeRole functions are overridden to enforce:

function _computeMaxAgents() internal view returns (uint256) {
uint256 base = totalFarmers / 50; // 1 agent per 50 farmers
if (totalFarmers % 50 != 0) base += 1; // ceil division
return base < 3 ? 3 : base; // minimum 3 agents
}

The cap applies globally (not per-cooperative). A cooperative cannot have more agents than max(3, ceil(totalFarmers / 50)) regardless of how many farmers they manage.

Farmer wallet addresses are not generated — they are derived deterministically from NIN:

function addressFromNin(nin: string): `0x${string}` {
const hash = keccak256(toBytes(`asilichain:${nin}`));
return `0x${hash.slice(26)}`;
}

The farmer never signs transactions and does not need a private key. The address is purely an on-chain identifier. For future self-custody, migrateFarmer() reassigns to a farmer-controlled wallet.

~55-77% of Uganda farmers operate outside formal cooperatives. These farmers are registered with cooperativeWallet = INDEPENDENT_AGGREGATOR, a protocol-managed multisig set post-deploy.

  • isIndependent(farmerWallet) — checks if farmer uses INDEPENDENT_AGGREGATOR
  • migrateFarmer(farmerWallet, newCooperative) — moves farmer to a cooperative
  • Independent farmer batches can have stages advanced by AGENT_ROLE (see TraceLog changes)
RoleDefault HolderCan
DEFAULT_ADMIN_ROLEAsiliChain deployer multisigGrant/revoke all roles; setIndependentAggregator(); migrateFarmer(); upgrade contract
COOP_ROLECooperative walletGrant/revoke AGENT_ROLE; deactivate farmers
AGENT_ROLEField agent walletsRegister farmers, verify GFW status, deactivate farmers