DEV Community

Cover image for 🌾 Day 18 of #30DaysOfSolidity β€” Decentralized Crop Insurance using Chainlink Oracles (Foundry + React Frontend)
Saurav Kumar
Saurav Kumar

Posted on

🌾 Day 18 of #30DaysOfSolidity β€” Decentralized Crop Insurance using Chainlink Oracles (Foundry + React Frontend)

🧭 Introduction

Farmers depend on rain β€” but nature isn’t always kind.
Today, we’ll use blockchain + oracles to bring weather-based crop insurance on-chain 🌦️

This smart contract automatically pays farmers if rainfall drops below a set threshold.
We’ll use Chainlink Oracles for real-world weather data, build & test it with Foundry, and interact with it via React + Ethers.js.


🧠 What You’ll Learn

  • How to integrate Chainlink Oracles for real-world data
  • How to build automated insurance on-chain
  • How to test smart contracts locally using Foundry
  • How to connect a React frontend with Ethers.js

πŸ“‚ Project Structure

Here’s the project layout for your reference:

day18-crop-insurance/
β”‚
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ CropInsurance.sol
β”‚   β”œβ”€β”€ MockOracle.sol
β”‚
β”œβ”€β”€ test/
β”‚   └── CropInsurance.t.sol
β”‚
β”œβ”€β”€ script/
β”‚   └── DeployCropInsurance.s.sol
β”‚
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ package.json
β”‚   β”œβ”€β”€ vite.config.js
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ App.jsx
β”‚   β”‚   └── App.css
β”‚   └── public/
β”‚
└── foundry.toml
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Smart Contracts

πŸ“„ CropInsurance.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IOracle {
    function requestRainfallData(uint256 latE6, uint256 lonE6, uint256 startTs, uint256 endTs)
        external
        returns (bytes32 requestId);
}

contract CropInsurance {
    struct Policy {
        address payable farmer;
        uint256 premium;
        uint256 payout;
        uint256 latE6;
        uint256 lonE6;
        uint256 startTs;
        uint256 endTs;
        uint256 rainfallThresholdMM;
        bool active;
        bool paid;
        bytes32 requestId;
    }

    mapping(uint256 => Policy) public policies;
    uint256 public nextPolicyId;
    address public owner;
    IOracle public oracle;

    event PolicyCreated(uint256 indexed id, address farmer);
    event PolicyEvaluated(uint256 indexed id, uint256 rainfall);
    event PolicyPaid(uint256 indexed id, address farmer, uint256 amount);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not authorized");
        _;
    }

    constructor(address _oracle) {
        owner = msg.sender;
        oracle = IOracle(_oracle);
    }

    function createPolicy(
        address payable farmer,
        uint256 premium,
        uint256 payout,
        uint256 latE6,
        uint256 lonE6,
        uint256 startTs,
        uint256 endTs,
        uint256 rainfallThresholdMM
    ) external onlyOwner {
        require(farmer != address(0), "Invalid farmer");
        require(startTs < endTs, "Invalid timestamps");

        uint256 id = nextPolicyId++;
        policies[id] = Policy({
            farmer: farmer,
            premium: premium,
            payout: payout,
            latE6: latE6,
            lonE6: lonE6,
            startTs: startTs,
            endTs: endTs,
            rainfallThresholdMM: rainfallThresholdMM,
            active: true,
            paid: false,
            requestId: bytes32(0)
        });

        emit PolicyCreated(id, farmer);
    }

    function evaluatePolicy(uint256 policyId) external {
        Policy storage p = policies[policyId];
        require(block.timestamp > p.endTs, "Season not ended");
        require(p.active, "Not active");

        bytes32 requestId =
            oracle.requestRainfallData(p.latE6, p.lonE6, p.startTs, p.endTs);
        p.requestId = requestId;
    }

    function fulfill(bytes32 requestId, uint256 rainfallMM) external {
        for (uint256 i = 0; i < nextPolicyId; i++) {
            Policy storage p = policies[i];
            if (p.requestId == requestId && p.active) {
                p.active = false;
                emit PolicyEvaluated(i, rainfallMM);

                if (rainfallMM < p.rainfallThresholdMM && !p.paid) {
                    p.paid = true;
                    (bool ok, ) = p.farmer.call{value: p.payout}("");
                    require(ok, "Transfer failed");
                    emit PolicyPaid(i, p.farmer, p.payout);
                }
                break;
            }
        }
    }

    receive() external payable {}
}
Enter fullscreen mode Exit fullscreen mode

🧩 MockOracle.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./CropInsurance.sol";

contract MockOracle is IOracle {
    bytes32 public lastRequestId;

    function requestRainfallData(uint256, uint256, uint256, uint256)
        external
        override
        returns (bytes32 requestId)
    {
        requestId = keccak256(abi.encodePacked(block.timestamp, msg.sender));
        lastRequestId = requestId;
        return requestId;
    }

    function fulfillRequest(address cropInsurance, bytes32 requestId, uint256 rainfallMM) external {
        CropInsurance(cropInsurance).fulfill(requestId, rainfallMM);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Foundry Test β€” CropInsurance.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/CropInsurance.sol";
import "../src/MockOracle.sol";

contract CropInsuranceTest is Test {
    CropInsurance crop;
    MockOracle oracle;
    address farmer = address(0xBEEF);

    function setUp() public {
        oracle = new MockOracle();
        crop = new CropInsurance(address(oracle));
        vm.deal(address(crop), 10 ether);
    }

    function testPayoutBelowThreshold() public {
        crop.createPolicy(payable(farmer), 0, 1 ether, 12345678, 87654321, block.timestamp, block.timestamp + 5, 50);
        vm.warp(block.timestamp + 6);
        crop.evaluatePolicy(0);

        bytes32 requestId = oracle.lastRequestId();
        oracle.fulfillRequest(address(crop), requestId, 10); // rainfall = 10mm

        assertEq(farmer.balance, 1 ether);
    }
}
Enter fullscreen mode Exit fullscreen mode

🧰 Run Locally with Foundry

forge build
forge test -vv
Enter fullscreen mode Exit fullscreen mode

βœ… Tests show automatic payouts when rainfall < threshold.


βš›οΈ Frontend Integration (React + Ethers.js)

πŸͺ„ Setup Frontend

cd ..
mkdir frontend && cd frontend
npm create vite@latest crop-insurance-dapp -- --template react
cd crop-insurance-dapp
npm install ethers
Enter fullscreen mode Exit fullscreen mode

βš›οΈ App.jsx

import { useState } from "react";
import { ethers } from "ethers";
import "./App.css";

const contractAddress = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
const contractABI = [
  "function createPolicy(address farmer,uint256,uint256,uint256,uint256,uint256,uint256,uint256)",
  "function evaluatePolicy(uint256 policyId)",
  "event PolicyCreated(uint256 indexed id,address farmer)",
  "event PolicyPaid(uint256 indexed id,address farmer,uint256 amount)"
];

function App() {
  const [farmer, setFarmer] = useState("");
  const [status, setStatus] = useState("");

  async function createPolicy() {
    if (!window.ethereum) return alert("MetaMask not found");
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const contract = new ethers.Contract(contractAddress, contractABI, signer);

    setStatus("Creating policy...");
    const tx = await contract.createPolicy(
      farmer,
      0,
      ethers.parseEther("1"),
      12345678,
      87654321,
      Math.floor(Date.now() / 1000),
      Math.floor(Date.now() / 1000) + 60,
      50
    );
    await tx.wait();
    setStatus("βœ… Policy Created!");
  }

  async function evaluatePolicy() {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const contract = new ethers.Contract(contractAddress, contractABI, signer);
    const tx = await contract.evaluatePolicy(0);
    await tx.wait();
    setStatus("🌦️ Policy evaluated");
  }

  return (
    <div className="app">
      <h1>🌾 Crop Insurance DApp</h1>
      <input
        type="text"
        placeholder="Farmer address"
        value={farmer}
        onChange={(e) => setFarmer(e.target.value)}
      />
      <button onClick={createPolicy}>Create Policy</button>
      <button onClick={evaluatePolicy}>Evaluate Policy</button>
      <p>{status}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

🎨 App.css

.app {
  text-align: center;
  margin-top: 5rem;
}
input {
  padding: 8px;
  width: 280px;
  margin-right: 10px;
  border-radius: 6px;
}
button {
  margin: 10px;
  padding: 10px 16px;
  border-radius: 8px;
  cursor: pointer;
  background-color: #2b9348;
  color: white;
  border: none;
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Run Frontend

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:5173
Now you can create and evaluate insurance policies using MetaMask! πŸŽ‰


πŸš€ Final Thoughts

You just built a real-world DeFi insurance use case that connects blockchain with external data through Chainlink oracles.
From Solidity β†’ Foundry β†’ React β†’ Ethers.js β€” you’ve completed the full stack of Web3 integration.


πŸ”— Resources


πŸ’¬ Next: Day 19 β€” Build a tokenized weather reward system β˜€οΈπŸ’§


πŸ”— Follow Me

Top comments (0)