Blockchain for P2P Energy Trading: Smart Contracts and Grid Constraints
In 2025, Italy counts over 850 constituted Renewable Energy Communities (RECs) and more than 3,500 in the design phase, thanks to incentives from Legislative Decree 199/2021 and the 2.2 billion euros allocated by the PNRR. But there is a problem that none of the current incentive models fully solves: energy settlement within RECs still occurs through centralized intermediaries, with multi-day latencies and transaction costs that significantly erode the economic advantage for prosumers.
The solution emerging globally - from the Brooklyn Microgrid in New York to experimental RECs in Germany and the Netherlands - is blockchain-based peer-to-peer energy trading. The idea is elegant in its simplicity: a prosumer with excess photovoltaic panels can directly sell their energy to a neighbor who needs it, bypassing an intermediary utility, with automatic settlement managed by a Solidity smart contract and immediate payment in energy tokens.
The blockchain market in the energy sector is worth $5.1 billion in 2025 and is projected to reach $154.7 billion by 2035 with a CAGR of 40.9%. P2P energy trading represents the fastest-growing segment, driven by the proliferation of DERs (Distributed Energy Resources), the RED III directive requiring member states to reach 42.5% renewables by 2030, and the maturation of Layer 2 blockchain platforms that have reduced gas costs by 99% compared to Ethereum mainnet.
In this article we build from scratch a complete P2P energy trading system on blockchain: from the Solidity smart contract for the energy marketplace, to the oracle for smart meter data, all the way to integration with DSO (Distribution System Operator) APIs for grid constraint management. We also cover the regulatory landscape - Clean Energy Package, RED III, Legislative Decree 199/2021 - and the GDPR implications for on-chain consumption data.
What You Will Learn in This Article
- Complete architecture of a P2P energy marketplace on blockchain
- Solidity smart contracts: EnergyToken (ERC-20), OrderBook, EnergyMarketplace, Settlement
- Blockchain comparison for energy: Ethereum L2 (Polygon, Arbitrum), Energy Web Chain, Hyperledger
- Oracle design: Chainlink for grid prices, custom oracle for DLMS/COSEM smart meter data
- Tokenomics: energy tokens, carbon credits, prosumer incentives
- Smart meter integration: DLMS/COSEM, IEC 62056 protocols, on-chain feed
- Grid constraint management: capacity limits, congestion management, DSO API
- EU regulation: Clean Energy Package, RED III, Italian RECs
- Privacy by design: GDPR compliance, zero-knowledge proofs for consumption data
- Testing with Hardhat: complete test suite for the marketplace
- Case studies: Brooklyn Microgrid, Energy Web Foundation, Italian REC
EnergyTech Series - 10 Articles
| # | Article | Status |
|---|---|---|
| 1 | Smart Grid and IoT: Architecture for the Future Electrical Grid | Published |
| 2 | DERMS Architecture: Aggregating Millions of Distributed Resources | Published |
| 3 | Battery Management System: Control Algorithms for BESS | Published |
| 4 | Digital Twin of the Electrical Grid with Python and Pandapower | Published |
| 5 | Renewable Energy Forecasting: ML for Solar and Wind | Published |
| 6 | EV Load Balancing: V2G and Smart Charging with OCPP | Published |
| 7 | MQTT and InfluxDB for Real-Time Energy Telemetry | Published |
| 8 | IEC 61850: Communication in the Electrical Substation | Published |
| 9 | Carbon Accounting Software: Measuring and Reducing Emissions | Published |
| 10 | Blockchain for P2P Energy Trading: Smart Contracts and Constraints (you are here) | Current |
Why Blockchain Solves a Real Problem in Energy
To understand the value of blockchain in P2P energy trading, we first need to understand how the energy market within energy communities works today and why the centralized model has structural limitations that are difficult to overcome.
The Problem of Centralized Settlement
In a typical Italian REC, the value flow works like this: the prosumer produces energy with their photovoltaic system, the excess energy is fed into the grid, the GSE measures the energy shared within the primary substation, and after 3-6 months an incentive of approximately 110 EUR/MWh is paid on shared energy (premium tariff) plus savings on the electricity bill. Settlement is monthly or quarterly, managed by the GSE through the CACER portal.
This model has advantages (simplicity, institutional support) but also critical limitations for a dynamic P2P marketplace:
- Settlement latency: days or weeks instead of seconds
- Fixed price: no possibility of dynamic price discovery based on local supply and demand
- Limited time granularity: monthly settlement vs. hourly or sub-hourly PV variability
- Multiple intermediaries: GSE, distributor (e-distribuzione), utility retailer, each with their own costs
- Lack of transparency: prosumers cannot see in real-time how value is being distributed
What Blockchain Adds
A P2P platform on blockchain does not replace the GSE regulatory framework - at least not in the short term - but complements it, adding a transparent, automatic and near-real-time settlement layer for transactions internal to the community. The concrete benefits are:
Measured Benefits in International Pilots
- Transaction cost reduction: -60-80% compared to traditional intermediaries (source: Energy Web Foundation, 2024)
- Settlement speed: from days to seconds (L2 blockchain with near-instant finality)
- Audit transparency: every transaction verifiable on-chain, immutable
- Local price discovery: P2P price reflects real-time supply and demand within the REC
- Automation: zero manual operations for settlement, reconciliation, payment
- Interoperability: open standards (ERC-20, ERC-1155) for interchangeable energy tokens
P2P Energy Trading System Architecture
A complete P2P energy trading system on blockchain is articulated across five distinct layers, each with specific responsibilities and appropriate technologies.
Complete Architectural Stack
+-------------------------------------------------------------------------+
| LAYER 5: UI / DASHBOARD |
| React / Angular app * Prosumer wallet * REC Portal |
+-------------------------------------------------------------------------+
| LAYER 4: SMART CONTRACT |
| EnergyToken (ERC-20) * OrderBook * EnergyMarketplace |
| Settlement * CarbonCredit * ProducerNFT (ERC-721) |
| Blockchain: Polygon zkEVM / Energy Web Chain / Hyperledger Besu |
+-------------------------------------------------------------------------+
| LAYER 3: ORACLE LAYER |
| Chainlink (grid prices) * Smart Meter Oracle (DLMS readings) |
| DSO Constraint Oracle (capacity limits) * Weather Oracle |
+-------------------------------------------------------------------------+
| LAYER 2: INTEGRATION MIDDLEWARE |
| FastAPI backend * DLMS/COSEM adapter * DSO API client |
| GSE CACER connector * MQTT broker * InfluxDB time-series |
+-------------------------------------------------------------------------+
| LAYER 1: FIELD DEVICES |
| Smart meter (IEC 62056) * PV inverter * BESS controller |
| EV charger (OCPP) * REC gateway * RTU / IED |
+-------------------------------------------------------------------------+
Flow of a P2P Transaction
Here is how a single energy trading transaction works, from physical measurement to payment:
Smart Meter (Prosumer A)
|
+-> DLMS/COSEM reading every 15 minutes
| IEC 62056-21 / OBIS codes
|
v
Smart Meter Oracle (FastAPI backend)
|
+-> Validates data (cryptographic meter signature)
+-> Aggregates energy produced in the interval
+-> Calls oracle contract: reportProduction(meterId, wh, timestamp)
|
v
Oracle Smart Contract (on-chain)
|
+-> Verifies meter signature
+-> Updates ProductionRegistry[meterId]
+-> Mints EnergyToken (EWT) = Wh produced
| 1 EWT = 1 Wh of certified renewable energy
|
v
EnergyMarketplace Contract
|
+-> Prosumer A posts offer: 50 EWT @ 0.08 EUR/EWT
+-> OrderBook matching with Consumer B: 50 EWT @ 0.09 EUR/EWT
+-> Checks DSO constraints: available capacity at substation? YES
|
v
Settlement Contract
|
+-> Transfers 50 EWT from A to B
+-> Transfers 4 EUR (stablecoin) from B to A
+-> Emits event Trade(A, B, 50, 0.08, timestamp)
+-> Updates CarbonCredit Registry (+50g CO2 avoided)
|
v
DSO API (e-distribuzione / local DSO)
|
+-> Notifies transaction for physical reconciliation
POST /api/v1/p2p-transactions
{ "from": "POD-IT001E...", "to": "POD-IT001E...", "wh": 50 }
Choosing the Blockchain: Comparison for the Energy Use Case
Not all blockchains are equal for P2P energy trading. The sector-specific requirements - high throughput for micro-transactions, very low gas costs, regulatory compliance, participant identity - lead to precise architectural choices.
Comparative Table
| Blockchain | TPS | Gas Cost (tx) | Finality | Identity | Suitable for REC |
|---|---|---|---|---|---|
| Ethereum Mainnet | 15-30 | $2-50 | ~12 min | Anonymous | No (too expensive) |
| Polygon PoS | 7,000 | $0.001-0.01 | ~2 sec | Anonymous | Yes (dev/test) |
| Polygon zkEVM | 2,000+ | $0.01-0.05 | ~1 min (ZK proof) | Anonymous + ZK | Yes (privacy) |
| Arbitrum One | 4,000+ | $0.001-0.02 | ~1 sec | Anonymous | Yes |
| Energy Web Chain | 3,000 | ~$0 | ~5 sec | DID + SSI | Optimal |
| Hyperledger Besu | 1,000+ | $0 | <1 sec | Permissioned | Yes (enterprise) |
| Hyperledger Fabric | 3,500 | $0 | <1 sec | MSP + X.509 | Yes (B2B) |
Recommendation for Italian REC
Recommended Stack: Energy Web Chain + Polygon zkEVM
Energy Web Chain (EWC) is a public Proof of Authority blockchain specifically designed for the energy sector. It natively supports the EW-DID framework for decentralized identity of prosumers (eIDAS 2.0 compliant), has virtually zero transaction costs (validators are regulated utilities), and is already used by Siemens, Shell, Volkswagen, and over 100 energy organizations.
For the privacy of consumption data (GDPR requirement), Polygon zkEVM is used as L2 with Zero-Knowledge Proofs: consumption data remains private (off-chain), but its validity is proven on-chain without revealing the actual values.
Smart Contract Implementation in Solidity
We implement a complete smart contract system for P2P energy trading. The design follows the Diamond / Proxy pattern for upgradability and the ERC-20 + custom settlement pattern for energy tokens.
1. EnergyToken (ERC-20): The Energy Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
/**
* @title EnergyToken (EWT)
* @notice ERC-20 token representing 1 Wh of certified renewable energy.
* Minted by the oracle when the smart meter registers verified production.
* Burned when physically settled with the DSO.
*/
contract EnergyToken is ERC20, AccessControl, Pausable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
// Renewable certification metadata
struct EnergyBatch {
uint256 timestamp;
string sourceType; // "SOLAR", "WIND", "HYDRO"
string location; // REC code (e.g. "IT-CER-PG-001")
uint256 co2Avoided; // grams of CO2 avoided per Wh
}
mapping(uint256 => EnergyBatch) public batches; // batchId => metadata
mapping(address => uint256) public producerBatch; // prosumer => last batchId
uint256 public batchCounter;
// Registry of certified prosumers (EW-DID or Ethereum address)
mapping(address => bool) public certifiedProducers;
mapping(address => string) public producerDID; // Decentralized Identifier
event EnergyMinted(
address indexed producer,
uint256 indexed batchId,
uint256 amount, // Wh
string sourceType,
uint256 co2Avoided
);
event ProducerCertified(address indexed producer, string did);
constructor() ERC20("EnergyWh Token", "EWT") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
/**
* @notice Certifies a prosumer after off-chain KYC/DID verification.
* Only admin can do this (utility or GSE delegate).
*/
function certifyProducer(
address producer,
string calldata did
) external onlyRole(DEFAULT_ADMIN_ROLE) {
certifiedProducers[producer] = true;
producerDID[producer] = did;
emit ProducerCertified(producer, did);
}
/**
* @notice Mints EWT based on the verified smart meter reading.
* Called by the oracle after DLMS/COSEM data validation.
* @param producer Prosumer wallet address
* @param whAmount Energy produced in Wh (precision: integer, no decimals)
* @param sourceType Source type: "SOLAR", "WIND", etc.
* @param location REC membership code
* @param co2PerWh Grams of CO2 avoided per Wh (depends on source)
*/
function mintEnergy(
address producer,
uint256 whAmount,
string calldata sourceType,
string calldata location,
uint256 co2PerWh
) external onlyRole(MINTER_ROLE) whenNotPaused {
require(certifiedProducers[producer], "EWT: producer not certified");
require(whAmount > 0, "EWT: zero quantity not valid");
batchCounter++;
batches[batchCounter] = EnergyBatch({
timestamp: block.timestamp,
sourceType: sourceType,
location: location,
co2Avoided: whAmount * co2PerWh
});
producerBatch[producer] = batchCounter;
_mint(producer, whAmount);
emit EnergyMinted(producer, batchCounter, whAmount, sourceType, whAmount * co2PerWh);
}
/**
* @notice Burns EWT after physical settlement with the DSO.
* Guarantees that each token is used only once.
*/
function burnSettled(
address holder,
uint256 amount
) external onlyRole(BURNER_ROLE) {
_burn(holder, amount);
}
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); }
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); }
}
2. OrderBook: The Order Matching Mechanism
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title EnergyOrderBook
* @notice On-chain OrderBook for energy buy and sell orders.
* Supports continuous double auction (CDA).
* Optimized for low latency on L2 (Polygon / Arbitrum).
*/
contract EnergyOrderBook {
enum OrderType { BUY, SELL }
enum OrderStatus { OPEN, FILLED, CANCELLED, PARTIAL }
struct Order {
uint256 id;
address trader;
OrderType orderType;
uint256 whAmount; // Wh offered/requested
uint256 pricePerWh; // EUR in wei (using 18 decimal stablecoin)
uint256 minFillAmount; // minimum acceptable partial fill
uint256 expiresAt; // order expiry timestamp
uint256 filledAmount; // Wh already executed
OrderStatus status;
string cerCode; // REC membership (geographic constraint)
bytes gridConstraintSig; // DSO oracle signature on available capacity
}
mapping(uint256 => Order) public orders;
uint256 public orderCounter;
// Sorted order books (simplified - use RB-tree or heap in production)
uint256[] public sellOrderIds; // sorted by price ASC
uint256[] public buyOrderIds; // sorted by price DESC
event OrderPlaced(
uint256 indexed orderId,
address indexed trader,
OrderType orderType,
uint256 whAmount,
uint256 pricePerWh
);
event OrderMatched(
uint256 indexed sellOrderId,
uint256 indexed buyOrderId,
uint256 whAmount,
uint256 price
);
event OrderCancelled(uint256 indexed orderId);
modifier onlyOrderOwner(uint256 orderId) {
require(orders[orderId].trader == msg.sender, "OB: not the owner");
_;
}
/**
* @notice Places a buy or sell energy order.
* @param orderType BUY (0) or SELL (1)
* @param whAmount Quantity in Wh
* @param pricePerWh Price in stablecoin wei per Wh (e.g. 0.08 EUR = 80000000000000000)
* @param minFill Minimum acceptable partial quantity (0 = fill or kill)
* @param ttl Time-to-live in seconds (max 86400 = 1 day)
* @param cerCode REC code for geographic constraint
*/
function placeOrder(
OrderType orderType,
uint256 whAmount,
uint256 pricePerWh,
uint256 minFill,
uint256 ttl,
string calldata cerCode
) external returns (uint256 orderId) {
require(whAmount >= 100, "OB: minimum 100 Wh per order");
require(pricePerWh > 0, "OB: zero price not valid");
require(ttl <= 86400, "OB: TTL max 24 hours");
orderId = ++orderCounter;
orders[orderId] = Order({
id: orderId,
trader: msg.sender,
orderType: orderType,
whAmount: whAmount,
pricePerWh: pricePerWh,
minFillAmount: minFill,
expiresAt: block.timestamp + ttl,
filledAmount: 0,
status: OrderStatus.OPEN,
cerCode: cerCode,
gridConstraintSig: ""
});
if (orderType == OrderType.SELL) {
_insertSellOrder(orderId);
} else {
_insertBuyOrder(orderId);
}
emit OrderPlaced(orderId, msg.sender, orderType, whAmount, pricePerWh);
}
// Sorted insertion (simplified bubble sort - use heap in production)
function _insertSellOrder(uint256 newId) internal {
sellOrderIds.push(newId);
uint256 n = sellOrderIds.length;
for (uint256 i = n - 1; i > 0; i--) {
if (orders[sellOrderIds[i]].pricePerWh < orders[sellOrderIds[i-1]].pricePerWh) {
(sellOrderIds[i], sellOrderIds[i-1]) = (sellOrderIds[i-1], sellOrderIds[i]);
} else {
break;
}
}
}
function _insertBuyOrder(uint256 newId) internal {
buyOrderIds.push(newId);
uint256 n = buyOrderIds.length;
for (uint256 i = n - 1; i > 0; i--) {
if (orders[buyOrderIds[i]].pricePerWh > orders[buyOrderIds[i-1]].pricePerWh) {
(buyOrderIds[i], buyOrderIds[i-1]) = (buyOrderIds[i-1], buyOrderIds[i]);
} else {
break;
}
}
}
function cancelOrder(uint256 orderId) external onlyOrderOwner(orderId) {
Order storage o = orders[orderId];
require(o.status == OrderStatus.OPEN, "OB: order not cancellable");
o.status = OrderStatus.CANCELLED;
emit OrderCancelled(orderId);
}
}
3. EnergyMarketplace: The Main Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./EnergyToken.sol";
import "./EnergyOrderBook.sol";
/**
* @title EnergyMarketplace
* @notice Main contract for P2P energy trading within RECs.
* Handles order matching, EWT settlement, DSO constraint verification,
* and carbon credit incentive distribution.
*
* @dev Architecture:
* - EWT (EnergyToken) for energy Wh
* - EURC (Circle Euro stablecoin) for EUR payment
* - Automatic escrow during matching
* - Marketplace fee: 0.5% on transaction value (destination: REC treasury)
*/
contract EnergyMarketplace is ReentrancyGuard, AccessControl {
// =========== STATE VARIABLES ===========
EnergyToken public ewtToken;
IERC20 public stablecoin; // EURC or EURe
EnergyOrderBook public orderBook;
bytes32 public constant DSO_ORACLE_ROLE = keccak256("DSO_ORACLE_ROLE");
bytes32 public constant CER_ADMIN_ROLE = keccak256("CER_ADMIN_ROLE");
uint256 public constant FEE_BPS = 50; // 0.5% in basis points
uint256 public constant MIN_TRADE_WH = 100; // minimum 100 Wh per trade
uint256 public constant SETTLEMENT_WINDOW = 900; // 15 min max for settlement
// REC parameters (set by administrator)
struct CERConfig {
string cerCode;
address cerTreasury; // REC wallet receiving fees
uint256 maxCapacityWh; // max hourly capacity of primary substation
uint256 currentLoadWh; // current load reported by DSO oracle
bool active;
}
mapping(string => CERConfig) public cerConfigs;
// Trade registry for audit
struct Trade {
uint256 tradeId;
uint256 sellOrderId;
uint256 buyOrderId;
address seller;
address buyer;
uint256 whAmount;
uint256 pricePerWh;
uint256 totalValue; // in stablecoin wei
uint256 fee;
uint256 timestamp;
string cerCode;
}
mapping(uint256 => Trade) public trades;
uint256 public tradeCounter;
// Escrow: buyer deposits stablecoin before matching
mapping(uint256 => uint256) public escrow; // orderId => locked amount
// Pending settlement for DSO reconciliation
mapping(uint256 => bool) public pendingDSOSettlement; // tradeId => settled
// =========== EVENTS ===========
event TradeExecuted(
uint256 indexed tradeId,
address indexed seller,
address indexed buyer,
uint256 whAmount,
uint256 pricePerWh,
string cerCode
);
event EscrowDeposited(uint256 indexed orderId, uint256 amount);
event EscrowReleased(uint256 indexed orderId, uint256 amount);
event DSOSettlementConfirmed(uint256 indexed tradeId);
event GridConstraintViolation(string cerCode, uint256 requestedWh, uint256 availableWh);
// =========== CONSTRUCTOR ===========
constructor(
address _ewtToken,
address _stablecoin,
address _orderBook
) {
ewtToken = EnergyToken(_ewtToken);
stablecoin = IERC20(_stablecoin);
orderBook = EnergyOrderBook(_orderBook);
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
// =========== CER MANAGEMENT ===========
function configureCER(
string calldata cerCode,
address treasury,
uint256 maxCapacityWh
) external onlyRole(CER_ADMIN_ROLE) {
cerConfigs[cerCode] = CERConfig({
cerCode: cerCode,
cerTreasury: treasury,
maxCapacityWh: maxCapacityWh,
currentLoadWh: 0,
active: true
});
}
/**
* @notice Updates current grid load from the network, called by DSO oracle.
* Prevents transactions that would violate capacity limits.
*/
function updateGridLoad(
string calldata cerCode,
uint256 currentLoadWh
) external onlyRole(DSO_ORACLE_ROLE) {
cerConfigs[cerCode].currentLoadWh = currentLoadWh;
}
// =========== ESCROW ===========
/**
* @notice Buyer deposits stablecoin in escrow when posting a BUY order.
* Guarantees solvency before matching.
*/
function depositEscrow(
uint256 orderId,
uint256 amount
) external nonReentrant {
EnergyOrderBook.Order memory o = orderBook.orders(orderId);
require(o.trader == msg.sender, "MKT: not the order owner");
require(o.orderType == EnergyOrderBook.OrderType.BUY, "MKT: only for BUY orders");
uint256 maxCost = o.whAmount * o.pricePerWh / 1e18;
require(amount >= maxCost, "MKT: insufficient escrow");
require(stablecoin.transferFrom(msg.sender, address(this), amount), "MKT: transfer failed");
escrow[orderId] += amount;
emit EscrowDeposited(orderId, amount);
}
// =========== MATCHING ENGINE ===========
/**
* @notice Executes matching between a sell order and a buy order.
* Checks: compatible prices, same REC, DSO constraints, sufficient escrow.
*
* @dev In production, this is called by an off-chain keeper that monitors
* OrderPlaced events and finds compatible pairs.
*/
function matchOrders(
uint256 sellOrderId,
uint256 buyOrderId
) external nonReentrant {
EnergyOrderBook.Order memory sellOrder = orderBook.orders(sellOrderId);
EnergyOrderBook.Order memory buyOrder = orderBook.orders(buyOrderId);
// Basic validations
require(
sellOrder.status == EnergyOrderBook.OrderStatus.OPEN &&
buyOrder.status == EnergyOrderBook.OrderStatus.OPEN,
"MKT: orders not open"
);
require(
keccak256(bytes(sellOrder.cerCode)) == keccak256(bytes(buyOrder.cerCode)),
"MKT: different RECs, trade not permitted"
);
require(
block.timestamp <= sellOrder.expiresAt &&
block.timestamp <= buyOrder.expiresAt,
"MKT: order(s) expired"
);
// Price check: sell price <= buy price (positive spread)
require(
sellOrder.pricePerWh <= buyOrder.pricePerWh,
"MKT: incompatible prices"
);
// Calculate quantity to execute (minimum of the two remaining)
uint256 sellRemaining = sellOrder.whAmount - sellOrder.filledAmount;
uint256 buyRemaining = buyOrder.whAmount - buyOrder.filledAmount;
uint256 tradeWh = sellRemaining < buyRemaining ? sellRemaining : buyRemaining;
require(tradeWh >= MIN_TRADE_WH, "MKT: quantity below minimum");
// Check DSO grid capacity constraints
string memory cerCode = sellOrder.cerCode;
CERConfig storage cer = cerConfigs[cerCode];
require(cer.active, "MKT: REC not active");
uint256 availableCapacity = cer.maxCapacityWh > cer.currentLoadWh
? cer.maxCapacityWh - cer.currentLoadWh
: 0;
if (tradeWh > availableCapacity) {
emit GridConstraintViolation(cerCode, tradeWh, availableCapacity);
revert("MKT: insufficient grid capacity");
}
// Execution price = midpoint (or sell price for simplicity)
uint256 execPrice = sellOrder.pricePerWh;
// Calculate total value and fee
uint256 totalValue = tradeWh * execPrice / 1e18;
uint256 fee = totalValue * FEE_BPS / 10000;
uint256 sellerNet = totalValue - fee;
// Verify seller's EWT balance
require(
ewtToken.balanceOf(sellOrder.trader) >= tradeWh,
"MKT: seller has insufficient EWT"
);
// Verify buyer escrow
require(escrow[buyOrderId] >= totalValue, "MKT: buyer escrow insufficient");
// ======= EXECUTE SETTLEMENT =======
// 1. Transfer EWT from seller to buyer
require(
ewtToken.transferFrom(sellOrder.trader, buyOrder.trader, tradeWh),
"MKT: EWT transfer failed"
);
// 2. Transfer stablecoin from escrow to seller (net of fee)
escrow[buyOrderId] -= totalValue;
require(
stablecoin.transfer(sellOrder.trader, sellerNet),
"MKT: seller payment failed"
);
// 3. Fee to REC treasury
if (fee > 0) {
require(
stablecoin.transfer(cer.cerTreasury, fee),
"MKT: treasury fee transfer failed"
);
}
// 4. Record trade
tradeCounter++;
trades[tradeCounter] = Trade({
tradeId: tradeCounter,
sellOrderId: sellOrderId,
buyOrderId: buyOrderId,
seller: sellOrder.trader,
buyer: buyOrder.trader,
whAmount: tradeWh,
pricePerWh: execPrice,
totalValue: totalValue,
fee: fee,
timestamp: block.timestamp,
cerCode: cerCode
});
pendingDSOSettlement[tradeCounter] = true;
// 5. Update estimated REC load
cer.currentLoadWh += tradeWh;
emit TradeExecuted(
tradeCounter,
sellOrder.trader,
buyOrder.trader,
tradeWh,
execPrice,
cerCode
);
}
/**
* @notice Confirms physical settlement by the DSO.
* Burns EWT tokens after physical reconciliation.
*/
function confirmDSOSettlement(
uint256 tradeId
) external onlyRole(DSO_ORACLE_ROLE) {
require(pendingDSOSettlement[tradeId], "MKT: trade already confirmed");
Trade memory t = trades[tradeId];
// Burn buyer's EWT (energy physically "consumed")
ewtToken.burnSettled(t.buyer, t.whAmount);
pendingDSOSettlement[tradeId] = false;
emit DSOSettlementConfirmed(tradeId);
}
}
Note on Gas Cost and Optimizations
The EnergyMarketplace contract in the example uses unoptimized storage for didactic clarity.
In production, reducing the number of storage writes is essential. Techniques: use
calldata instead of memory where possible, pack variables into a single
uint256 with bit shifting, and especially use events instead of
storage for historical data (event logs cost ~20x less than storage). On Polygon zkEVM, the
estimated gas cost for a complete matchOrders() call is approximately 150,000 gas,
equal to about 0.03 EUR at the current MATIC price.
Oracle Design: Smart Meter Data On-Chain
The fundamental problem of the blockchain oracle in the energy context is the certification of actual production: how to ensure that the Wh minted as EWT actually correspond to physical energy produced and measured by a certified smart meter?
Multi-Layer Oracle Architecture
# oracle/smart_meter_oracle.py
# Python Oracle that reads DLMS/COSEM smart meters and sends data on-chain
import asyncio
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Optional
from dlms_cosem import DlmsConnection, Obis # Python DLMS library
from eth_account import Account
from web3 import AsyncWeb3
from web3.middleware import SignAndSendRawMiddlewareBuilder
# ======= CONFIGURATION =======
ORACLE_PRIVATE_KEY = "0x..." # in production: HashiCorp Vault / AWS KMS
ENERGY_TOKEN_ABI = json.load(open("abi/EnergyToken.json"))
ENERGY_TOKEN_ADDR = "0x..."
RPC_URL = "https://rpc.energyweb.org"
# DLMS parameters for Italian smart meter
# Standard: IEC 62056-21, OBIS codes for bidirectional meters
METER_IP = "192.168.1.100"
METER_PORT = 4059
METER_TIMEOUT = 10 # seconds
# Standard OBIS codes for imported/exported energy
OBIS_ACTIVE_EXPORT = Obis(1, 0, 2, 8, 0, 255) # total kWh exported
OBIS_ACTIVE_IMPORT = Obis(1, 0, 1, 8, 0, 255) # total kWh imported
@dataclass
class MeterReading:
meter_id: str
timestamp: int
export_kwh: float # exported energy (injected production)
import_kwh: float # imported energy (grid consumption)
net_wh: int # net in Wh (positive = excess production)
signature: bytes # ECDSA signature from meter (if available)
async def read_smart_meter(meter_id: str) -> MeterReading:
"""
Reads the smart meter via DLMS/COSEM and returns verified data.
The Italian e-distribuzione meter (Enel Hera Linea Group) supports
IEC 62056-21 mode E with HDLC framing.
"""
conn = DlmsConnection(
ip=METER_IP,
port=METER_PORT,
timeout=METER_TIMEOUT
)
try:
await conn.connect()
# DLMS authentication (HLS or LLS password as configured)
await conn.authenticate(level="lls", password="00000000")
export_kwh = await conn.get(OBIS_ACTIVE_EXPORT)
import_kwh = await conn.get(OBIS_ACTIVE_IMPORT)
net_wh = int((export_kwh - import_kwh) * 1000) # convert to Wh
# Meter signature (if the meter supports ECDSA P-256 signing)
# Alternatively, use the backend oracle signature
reading = MeterReading(
meter_id=meter_id,
timestamp=int(time.time()),
export_kwh=float(export_kwh),
import_kwh=float(import_kwh),
net_wh=max(0, net_wh), # only excess production
signature=b""
)
return reading
finally:
await conn.disconnect()
def sign_reading(reading: MeterReading, private_key: str) -> bytes:
"""
Signs the reading with the oracle's private key.
The contract verifies this signature to authenticate the source.
"""
account = Account.from_key(private_key)
message = hashlib.sha256(
f"{reading.meter_id}:{reading.timestamp}:{reading.net_wh}".encode()
).hexdigest()
signed = account.sign_message(
message_signable={"type": "string", "message": message}
)
return signed.signature
async def submit_to_blockchain(
reading: MeterReading,
w3: AsyncWeb3,
oracle_account: Account
) -> Optional[str]:
"""
Sends the verified reading to the EnergyToken contract for minting.
Uses EIP-1559 to optimize gas costs on L2.
"""
if reading.net_wh < 100:
# Not worth minting less than 100 Wh (below marketplace minimum)
print(f"Skip: reading {reading.net_wh}Wh below minimum threshold")
return None
contract = w3.eth.contract(
address=ENERGY_TOKEN_ADDR,
abi=ENERGY_TOKEN_ABI
)
# Verify the prosumer wallet associated with the meter ID
producer_address = await get_producer_address(reading.meter_id)
tx = await contract.functions.mintEnergy(
producer_address,
reading.net_wh,
"SOLAR",
"IT-CER-PG-001",
400 # grams CO2 per kWh (Italian grid average, ISPRA 2024 update)
).build_transaction({
"from": oracle_account.address,
"nonce": await w3.eth.get_transaction_count(oracle_account.address),
"maxFeePerGas": w3.to_wei("30", "gwei"),
"maxPriorityFeePerGas": w3.to_wei("2", "gwei"),
})
signed_tx = oracle_account.sign_transaction(tx)
tx_hash = await w3.eth.send_raw_transaction(signed_tx.raw_transaction)
receipt = await w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Minted {reading.net_wh} EWT for {producer_address} | TX: {tx_hash.hex()}")
return tx_hash.hex()
else:
print(f"Mint error: TX {tx_hash.hex()} reverted")
return None
async def oracle_loop(interval_seconds: int = 900):
"""
Main oracle loop: reads the meter every 15 minutes (quarter-hour interval
aligned with Italian electricity market energy settlement - MGP/MI IPEX).
"""
w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(RPC_URL))
oracle_account = Account.from_key(ORACLE_PRIVATE_KEY)
meter_ids = await load_certified_meters() # from GSE database
print(f"Oracle started. Monitoring {len(meter_ids)} smart meters every {interval_seconds}s")
while True:
start = time.time()
for meter_id in meter_ids:
try:
reading = await read_smart_meter(meter_id)
tx_hash = await submit_to_blockchain(reading, w3, oracle_account)
await log_reading(reading, tx_hash)
except Exception as e:
print(f"Meter error {meter_id}: {e}")
await alert_on_call(meter_id, str(e))
elapsed = time.time() - start
sleep_for = max(0, interval_seconds - elapsed)
await asyncio.sleep(sleep_for)
if __name__ == "__main__":
asyncio.run(oracle_loop())
Integration with the DSO: Grid Constraints and Grid Operator API
The critical point of any P2P energy trading system is the relationship with the DSO (Distribution System Operator) - in Italy, primarily e-distribuzione (Enel group) which manages 85% of the distribution network. Without DSO integration, on-chain trading is merely a financial abstraction disconnected from the real physics of the grid.
Why DSO Constraints Are Non-Negotiable
Physical Limits of the Distribution Network
- Primary substation capacity: each HV/MV substation has a maximum transformation capacity (typically 40-100 MVA). P2P trades occur within the perimeter of a single primary substation - this is also the geographic constraint of Italian RECs.
- Congestion management: during peak photovoltaic hours (11am-3pm in summer), some LV lines may become saturated. The DSO must be able to block or reduce trades to prevent local blackouts.
- Grid balancing: energy cannot physically flow from one POD to another without considering losses and grid balancing. P2P financial trading is a virtual netting, not a physical routing.
- Official metering: the certified smart meter (e-distribuzione or certified third party) is the only legally valid measurement for regulatory settlement with the GSE.
Python Client for DSO API
# integration/dso_client.py
# Client for the e-distribuzione REST API (simulated for development)
# In production: access via e-distribuzione OpenData portal or B2B API
import httpx
import asyncio
from dataclasses import dataclass
from typing import Optional
from datetime import datetime, timezone
DSO_API_BASE = "https://api.e-distribuzione.it/v2"
DSO_API_KEY = "..." # from environment variable
@dataclass
class GridCapacity:
"""Grid capacity available for a primary substation in a time interval."""
substation_id: str # primary substation ID (e.g. "IT-RM-CP-0042")
timestamp_from: datetime
timestamp_to: datetime
total_capacity_kw: float # total MV capacity
available_capacity_kw: float # available for new flows
congestion_level: str # "LOW", "MEDIUM", "HIGH", "CRITICAL"
@dataclass
class P2PTradeNotification:
"""Notification to the DSO of an on-chain P2P trade, for reconciliation."""
trade_id: str # on-chain transaction ID
tx_hash: str # blockchain hash
pod_seller: str # POD of the prosumer seller (e.g. "IT001E12345678XX")
pod_buyer: str # POD of the consumer buyer
wh_amount: int # Wh traded
timestamp: datetime # on-chain trade timestamp
cer_code: str # REC code for GSE reconciliation
class DSOClient:
"""
Asynchronous client to communicate with the Distribution System Operator.
Handles:
- Available capacity queries for the grid constraint oracle
- P2P trade notifications for physical reconciliation
- Congestion alerts receipt for preventive blocking
"""
def __init__(self, api_base: str = DSO_API_BASE, api_key: str = DSO_API_KEY):
self.api_base = api_base
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json"
}
async def get_grid_capacity(
self,
substation_id: str,
interval_minutes: int = 15
) -> GridCapacity:
"""
Queries the DSO for the current primary substation capacity.
This information feeds the on-chain DSO oracle.
"""
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.api_base}/substations/{substation_id}/capacity",
headers=self.headers,
params={
"interval_minutes": interval_minutes,
"timestamp": datetime.now(timezone.utc).isoformat()
}
)
response.raise_for_status()
data = response.json()
return GridCapacity(
substation_id=data["substation_id"],
timestamp_from=datetime.fromisoformat(data["from"]),
timestamp_to=datetime.fromisoformat(data["to"]),
total_capacity_kw=data["capacity"]["total_kw"],
available_capacity_kw=data["capacity"]["available_kw"],
congestion_level=data["congestion"]["level"]
)
async def notify_p2p_trade(self, trade: P2PTradeNotification) -> bool:
"""
Notifies the DSO of a completed on-chain P2P trade.
The DSO uses this information for:
1. Reconciliation with smart meter readings
2. Updating the CACER database (via GSE interface)
3. Potential cancellation request if constraints are violated ex-post
"""
async with httpx.AsyncClient(timeout=15.0) as client:
payload = {
"trade_id": trade.trade_id,
"tx_hash": trade.tx_hash,
"seller_pod": trade.pod_seller,
"buyer_pod": trade.pod_buyer,
"energy_wh": trade.wh_amount,
"timestamp": trade.timestamp.isoformat(),
"cer_code": trade.cer_code,
"source": "blockchain_p2p_marketplace"
}
response = await client.post(
f"{self.api_base}/p2p-settlements",
headers=self.headers,
json=payload
)
if response.status_code == 201:
print(f"Trade {trade.trade_id} successfully notified to DSO")
return True
else:
print(f"DSO error: {response.status_code} - {response.text}")
return False
async def subscribe_congestion_alerts(
self,
substation_id: str,
callback # async callable
):
"""
WebSocket subscription for real-time congestion alerts.
When the DSO signals a block, the keeper updates the contract
and blocks new trades for that REC.
"""
import websockets
ws_url = f"wss://api.e-distribuzione.it/v2/ws/congestion/{substation_id}"
async with websockets.connect(
ws_url,
extra_headers={"Authorization": f"Bearer {DSO_API_KEY}"}
) as ws:
async for message in ws:
alert = json.loads(message)
await callback(alert)
Testing with Hardhat: Complete Suite for the Marketplace
Energy contracts handle real value: reliability is critical. An exhaustive test suite with Hardhat is the only way to guarantee marketplace correctness before mainnet deployment.
// test/EnergyMarketplace.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
describe("EnergyMarketplace - Complete Suite", function () {
// ========= FIXTURE =========
async function deployMarketplaceFixture() {
const [admin, dsoOracle, cerAdmin, prosumerA, prosumerB, consumer1, consumer2]
= await ethers.getSigners();
// Deploy EnergyToken
const EnergyToken = await ethers.getContractFactory("EnergyToken");
const ewtToken = await EnergyToken.deploy();
// Deploy mock ERC-20 stablecoin (simulated EURC)
const MockEURC = await ethers.getContractFactory("MockERC20");
const eurc = await MockEURC.deploy("Euro Coin", "EURC", 6); // 6 decimals like USDC
// Deploy OrderBook
const OrderBook = await ethers.getContractFactory("EnergyOrderBook");
const orderBook = await OrderBook.deploy();
// Deploy Marketplace
const Marketplace = await ethers.getContractFactory("EnergyMarketplace");
const marketplace = await Marketplace.deploy(
await ewtToken.getAddress(),
await eurc.getAddress(),
await orderBook.getAddress()
);
// Setup roles
const MINTER_ROLE = await ewtToken.MINTER_ROLE();
await ewtToken.grantRole(MINTER_ROLE, admin.address); // oracle mints
await marketplace.grantRole(
await marketplace.DSO_ORACLE_ROLE(),
dsoOracle.address
);
await marketplace.grantRole(
await marketplace.CER_ADMIN_ROLE(),
cerAdmin.address
);
// Certify prosumers
await ewtToken.certifyProducer(prosumerA.address, "did:ewc:it:cer:prosumer-a");
await ewtToken.certifyProducer(prosumerB.address, "did:ewc:it:cer:prosumer-b");
// Configure REC
await marketplace.connect(cerAdmin).configureCER(
"IT-CER-PG-001",
admin.address, // REC treasury
1_000_000 // 1 MWh max capacity
);
// Mint EWT for prosumer A (oracle reading simulation)
await ewtToken.mintEnergy(
prosumerA.address,
5_000, // 5 kWh
"SOLAR",
"IT-CER-PG-001",
400
);
// Mint EURC for consumer1 (for escrow)
await eurc.mint(consumer1.address, ethers.parseUnits("100", 6)); // 100 EUR
return {
admin, dsoOracle, cerAdmin, prosumerA, prosumerB, consumer1, consumer2,
ewtToken, eurc, orderBook, marketplace
};
}
// ========= TEST: TOKEN MINT =========
describe("EnergyToken - Mint and Certify", function () {
it("should only mint EWT for certified prosumers", async function () {
const { ewtToken, consumer1 } = await loadFixture(deployMarketplaceFixture);
await expect(
ewtToken.mintEnergy(consumer1.address, 1000, "SOLAR", "IT-CER-PG-001", 400)
).to.be.revertedWith("EWT: producer not certified");
});
it("should correctly register batch metadata", async function () {
const { ewtToken, prosumerA } = await loadFixture(deployMarketplaceFixture);
const batchId = await ewtToken.producerBatch(prosumerA.address);
const batch = await ewtToken.batches(batchId);
expect(batch.sourceType).to.equal("SOLAR");
expect(batch.location).to.equal("IT-CER-PG-001");
expect(batch.co2Avoided).to.equal(5_000 * 400); // 5000 Wh * 400 gCO2/Wh
});
});
// ========= TEST: ORDERS AND MATCHING =========
describe("Marketplace - Matching and Settlement", function () {
it("should execute a complete trade with correct settlement", async function () {
const {
admin, dsoOracle, prosumerA, consumer1,
ewtToken, eurc, marketplace, orderBook
} = await loadFixture(deployMarketplaceFixture);
// ProsumerA approves EWT for the marketplace
await ewtToken.connect(prosumerA).approve(
await marketplace.getAddress(),
5_000
);
// Post SELL order: 2000 Wh @ 0.08 EUR/Wh
const pricePerWh = ethers.parseUnits("0.08", 18); // 18 decimals for compatibility
const sellTx = await orderBook.connect(prosumerA).placeOrder(
1, // SELL
2_000,
pricePerWh,
0, // no partial fill
3600, // TTL 1 hour
"IT-CER-PG-001"
);
const sellReceipt = await sellTx.wait();
const sellOrderId = 1n; // first order
// Consumer1 approves EURC for escrow and posts BUY
const buyTotalCost = 2_000n * pricePerWh / BigInt(1e18);
await eurc.connect(consumer1).approve(await marketplace.getAddress(), buyTotalCost);
const buyTx = await orderBook.connect(consumer1).placeOrder(
0, // BUY
2_000,
pricePerWh,
0,
3600,
"IT-CER-PG-001"
);
const buyOrderId = 2n;
// Deposit escrow
await marketplace.connect(consumer1).depositEscrow(buyOrderId, buyTotalCost);
// Update grid load (DSO oracle: available capacity)
await marketplace.connect(dsoOracle).updateGridLoad("IT-CER-PG-001", 0);
// Balance snapshot before trade
const prosumerABalanceBefore = await eurc.balanceOf(prosumerA.address);
const consumer1EWTBefore = await ewtToken.balanceOf(consumer1.address);
// Execute matching
await marketplace.connect(admin).matchOrders(sellOrderId, buyOrderId);
// Verify balances after trade
const prosumerABalanceAfter = await eurc.balanceOf(prosumerA.address);
const consumer1EWTAfter = await ewtToken.balanceOf(consumer1.address);
// ProsumerA received EURC (net of 0.5% fee)
const expectedNet = buyTotalCost - (buyTotalCost * 50n / 10000n);
expect(prosumerABalanceAfter - prosumerABalanceBefore).to.equal(expectedNet);
// Consumer1 received 2000 EWT
expect(consumer1EWTAfter - consumer1EWTBefore).to.equal(2_000n);
});
it("should reject trades between different RECs", async function () {
const { prosumerA, consumer1, orderBook, marketplace }
= await loadFixture(deployMarketplaceFixture);
await orderBook.connect(prosumerA).placeOrder(1, 1000, ethers.parseUnits("0.08", 18), 0, 3600, "IT-CER-PG-001");
await orderBook.connect(consumer1).placeOrder(0, 1000, ethers.parseUnits("0.09", 18), 0, 3600, "IT-CER-RM-002");
await expect(
marketplace.matchOrders(1n, 2n)
).to.be.revertedWith("MKT: different RECs, trade not permitted");
});
it("should block trades when grid capacity is insufficient", async function () {
const { admin, dsoOracle, prosumerA, consumer1, marketplace, orderBook, ewtToken, eurc }
= await loadFixture(deployMarketplaceFixture);
// Simulate grid at full capacity
await marketplace.connect(dsoOracle).updateGridLoad(
"IT-CER-PG-001",
999_000 // 999 kWh out of 1000 kWh available - only 1 kWh free
);
await ewtToken.connect(prosumerA).approve(await marketplace.getAddress(), 5000);
await orderBook.connect(prosumerA).placeOrder(1, 2000, ethers.parseUnits("0.08", 18), 0, 3600, "IT-CER-PG-001");
const pricePerWh = ethers.parseUnits("0.08", 18);
const escrowAmt = 2000n * pricePerWh / BigInt(1e18);
await eurc.connect(consumer1).approve(await marketplace.getAddress(), escrowAmt);
await orderBook.connect(consumer1).placeOrder(0, 2000, pricePerWh, 0, 3600, "IT-CER-PG-001");
await marketplace.connect(consumer1).depositEscrow(2n, escrowAmt);
await expect(
marketplace.connect(admin).matchOrders(1n, 2n)
).to.be.revertedWith("MKT: insufficient grid capacity");
});
});
});
Regulatory Framework: EU and Italy
P2P energy trading on blockchain operates at the intersection of two overlapping regulatory frameworks: the energy framework (EU directives and Italian transposition) and the digital financial markets framework (MiCA, GDPR). Understanding the regulatory constraints is essential to designing a legally compliant system.
Key European Directives
Clean Energy Package (2018-2021): The Legal Foundation for P2P
The European Union's Clean Energy Package (CEP) is the regulatory framework that legally enabled P2P energy trading throughout the EU. The relevant pillars:
- Electricity Directive (EU) 2019/944: Art. 15 - explicitly recognizes the right of "active prosumers" to sell self-produced energy, including through P2P agreements. Art. 16 - regulates citizens' energy communities (CEC).
- RED II Directive (EU) 2018/2001: Art. 22 - introduces "renewable energy communities" (RECs) as legal entities with the right to produce, consume, store and sell renewable energy.
- RED III Directive (EU) 2023/2413: In force since November 2023, targeting 42.5% renewables by 2030. Strengthens REC rights, simplifies permitting procedures, introduces the concept of facilitated "repowering". National transposition: by May 2025.
Italian Legislation: D.Lgs. 199/2021 and RECs
| Aspect | Regulatory Detail | Impact on P2P Blockchain |
|---|---|---|
| REC Definition | Legal entity with legal personality, max 1 MW installation, primary HV/MV substation perimeter | On-chain P2P trading is limited to PODs within the same primary substation - fixed constraint |
| GSE Incentive | Premium tariff ~110 EUR/MWh on shared energy + network tariff savings | Blockchain settlement must be reconciled with the GSE CACER portal |
| Metering | Certified e-distribuzione smart meter as the only valid measurement | The oracle must use data from certified meters - not self-reported |
| Eligible participants | Individuals, SMEs, public bodies, third sector entities, cooperatives (after D.L. MASE May 2025) | Blockchain wallets must be linked to verified legal identities (KYC) |
| PNRR incentives | 2.2 billion EUR for REC investments (CACER tender July-November 2025) | Opportunity to finance blockchain infrastructure + smart meter upgrades |
| GSE CACER Rules 2025 | Simplified retroactive expense eligibility, 30% advance on non-repayable grant | Greater liquidity for the initial investment in the P2P system |
MiCA and Energy Tokens: Not Securities
Legal Classification of the EnergyToken (EWT)
The MiCA Regulation (Markets in Crypto-Assets, EU 2023/1114) in full force since December 2024 classifies crypto-assets into three categories: ART (Asset-Referenced Tokens), EMT (E-Money Tokens), and "Other tokens".
The EnergyToken (EWT) described in this article is classifiable as a "utility token" in the "Other tokens" category: it represents a physical unit of energy (1 EWT = 1 Wh), not an expectation of profit. This classification exempts it from the more stringent MiCA obligations for ART/EMT, but still requires:
- Published whitepaper with technical token description
- Anti-money laundering measures (AMLD6) for participants (KYC/AML)
- Notification to the national competent authority (Banca d'Italia / Consob) if volume exceeds thresholds
The EURC stablecoin used for payments is an EMT issued by Circle with a MiCA license, and requires no further compliance from the marketplace operator.
GDPR and Privacy: Consumption Data On-Chain
Smart meter data - how many kWh I produce, when, with what hourly profile - are personal data under GDPR (CJEU ruling C-434/16, Nowak). This creates a fundamental paradox with blockchain: on-chain data is immutable, but GDPR guarantees the "right to erasure" (Art. 17). How is this resolved?
Privacy by Design: Off-Chain Architecture with ZK Proofs
# privacy/zkp_meter_proof.py
# Zero-Knowledge Proof for smart meter data: proves you produced X Wh
# without revealing the detailed production profile.
# Uses circom + snarkjs via Python binding
"""
ZK circuit schema (circom pseudocode):
template MeterProductionProof() {
// Private inputs (not revealed on-chain)
signal private input raw_readings[96]; // 15-min intervals for 24h
signal private input meter_secret; // meter secret (HMAC key)
// Public inputs (on-chain verifiable)
signal input total_wh_claimed; // Wh the prosumer claims
signal input meter_id_hash; // hash of meter ID (anonymized)
signal input day_timestamp; // day this refers to
// Output: proof that sum(raw_readings) == total_wh_claimed
signal output valid;
var sum = 0;
for (var i = 0; i < 96; i++) {
sum += raw_readings[i];
}
// Constraint: sum of private readings = declared total
sum === total_wh_claimed;
valid <== 1;
}
"""
from py_snark import Prover, Verifier
import hashlib
import json
class ZKMeterProver:
"""
Generates ZK proofs for smart meter data.
The prosumer proves to the contract that they produced X Wh
without revealing the temporal profile of their readings.
"""
def __init__(self, circuit_wasm: str, zkey: str):
self.prover = Prover(circuit_wasm, zkey)
self.verifier = Verifier()
def generate_proof(
self,
raw_readings: list[float], # readings every 15 minutes (private)
meter_id: str, # meter ID (private)
day_timestamp: int # day timestamp (public)
) -> dict:
"""
Generates a ZK proof attesting total production without
revealing individual readings.
"""
total_wh = int(sum(raw_readings))
meter_id_hash = hashlib.sha256(meter_id.encode()).hexdigest()
# Private inputs (do not leave the prosumer's device)
private_inputs = {
"raw_readings": [int(r) for r in raw_readings],
"meter_secret": int(hashlib.md5(meter_id.encode()).hexdigest(), 16) % (2**253)
}
# Public inputs (sent on-chain with the proof)
public_inputs = {
"total_wh_claimed": total_wh,
"meter_id_hash": int(meter_id_hash, 16) % (2**253),
"day_timestamp": day_timestamp
}
proof = self.prover.generate_proof(
private_inputs=private_inputs,
public_inputs=public_inputs
)
return {
"proof": proof.to_solidity(), # format for on-chain verification
"public_inputs": public_inputs,
"total_wh": total_wh
}
def verify_proof(self, proof: dict, public_inputs: dict) -> bool:
"""Verifies the proof (normally executed by the Verifier contract on-chain)."""
return self.verifier.verify(proof["proof"], public_inputs)
# ====== VERIFIER CONTRACT (Solidity, generated by snarkjs) ======
# The Groth16Verifier.sol contract is automatically generated by:
# $ snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol
#
# In the EnergyMarketplace contract, minting only happens if the proof is valid:
#
# function mintWithZKProof(
# uint[2] calldata _pA,
# uint[2][2] calldata _pB,
# uint[2] calldata _pC,
# uint[3] calldata _pubSignals // [total_wh, meter_hash, timestamp]
# ) external {
# require(groth16Verifier.verifyProof(_pA, _pB, _pC, _pubSignals), "ZK: invalid proof");
# uint256 totalWh = _pubSignals[0];
# address producer = meterHashToProducer[bytes32(_pubSignals[1])];
# _mintEnergy(producer, totalWh, ...);
# }
EDPB 2025 Guidelines on Blockchain and GDPR
On April 14, 2025, the European Data Protection Board (EDPB) published updated guidelines on blockchain and GDPR. Key points for P2P energy trading:
- Personal data on-chain: identity hashes, wallet addresses linked to individuals and consumption data are all personal data - they must never be written on-chain in plain text
- Pseudonymization: using Ethereum addresses as pseudonyms is acceptable if the wallet-identity mapping is kept off-chain with limited access
- Right to erasure: implementable with off-chain data deletion + on-chain token nullification (burn + certificate revocation) - the blockchain transaction remains immutable but meaningless
- Data controller: for a REC with blockchain, it is the entity managing the oracle and the wallet-identity mapping - must appoint a DPO if processing data at large scale
Case Studies: Brooklyn Microgrid and Italian REC
Brooklyn Microgrid - LO3 Energy (2016-2024)
The Brooklyn Microgrid, a pioneering project by LO3 Energy in partnership with Siemens, was the first P2P energy trading pilot on blockchain in history. Launched in 2016 in the Park Slope neighborhood of Brooklyn (New York), it initially involved fewer than 60 prosumers, growing to hundreds of virtual participants.
Lessons Learned from the Brooklyn Microgrid
- Fundamental regulatory problem: in the US, utilities have a legal monopoly on electricity sales. Even with the technology ready, participants could only exchange Renewable Energy Certificates (RECs), not physical energy. The "P2P" trading was purely financial.
- Exergy platform: LO3 developed an energy-specific permissioned blockchain layer. In Europe, Energy Web Foundation followed a similar approach with Energy Web Chain, optimized for regulated use cases.
- Smart meter integration: the main bottleneck was not the blockchain but interfacing with legacy smart meters. American meters did not support real-time readings - a limitation resolved in Europe with the EU smart meter directive.
- User experience: the complexity of the crypto wallet was a huge barrier for non-technical prosumers. The solution: platform-custodied wallets with a UI similar to a banking app.
Case Study: Pilot REC in Perugia - Hypothetical Scenario
Let us model a concrete use case for an Italian REC with P2P trading on blockchain, based on real parameters from the GSE 2025 framework:
# simulation/cer_p2p_simulation.py
# P2P trading simulation for an Italian REC with 50 prosumers and 200 consumers
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
import matplotlib.pyplot as plt # for result visualization
# ======= PERUGIA PILOT REC PARAMETERS =======
CER_CONFIG = {
"name": "REC Perugia Centro - Substation 042",
"cer_code": "IT-CER-PG-042",
"max_capacity_kw": 500, # 500 kW substation capacity
"n_prosumer": 50, # 50 prosumers with PV
"n_consumer": 200, # 200 consumers only
"avg_fv_kw": 4.5, # average 4.5 kWp per prosumer (typical residential)
"incentivo_gse": 0.110, # 110 EUR/MWh GSE premium tariff
"p2p_price_range": (0.07, 0.12), # P2P price range in EUR/kWh
"grid_price_sell": 0.05, # grid sell price (without P2P)
"grid_price_buy": 0.30, # grid buy price (consumers)
}
def simulate_solar_production(n_prosumer: int, avg_kw: float, hours: int = 24) -> np.ndarray:
"""Simulates PV production for a typical summer day (July, Perugia)."""
# Typical solar profile: Gaussian curve centered at 1pm
time_hours = np.linspace(0, 24, hours)
solar_curve = np.maximum(0, avg_kw * np.exp(-((time_hours - 13) ** 2) / (2 * 3**2)))
# Variability between prosumers (orientation, shading)
productions = np.random.normal(
loc=solar_curve,
scale=solar_curve * 0.15, # 15% variability
size=(n_prosumer, hours)
)
return np.maximum(0, productions)
def simulate_consumption(n_consumer: int, hours: int = 24) -> np.ndarray:
"""Simulates typical residential consumption (ARERA 2025 profile)."""
# Bimodal profile: morning peak (7-9am) and evening peak (6-10pm)
time_hours = np.linspace(0, 24, hours)
base_kw = 0.8
morning_peak = 0.4 * np.exp(-((time_hours - 8) ** 2) / (2 * 1.5**2))
evening_peak = 0.6 * np.exp(-((time_hours - 20) ** 2) / (2 * 2**2))
profile = base_kw + morning_peak + evening_peak
consumptions = np.random.normal(
loc=profile,
scale=profile * 0.20,
size=(n_consumer, hours)
)
return np.maximum(0, consumptions)
def run_p2p_market_simulation() -> dict:
"""Run P2P market simulation for 1 day."""
config = CER_CONFIG
hours = 24
productions = simulate_solar_production(config["n_prosumer"], config["avg_fv_kw"], hours)
consumptions = simulate_consumption(config["n_consumer"], hours)
# Aggregated prosumer surplus per hour
surplus_per_hour = productions.sum(axis=0) * 1000 # in Wh
# Aggregated consumer demand per hour
demand_per_hour = consumptions.sum(axis=0) * 1000 # in Wh
# Simulate P2P matching for each hour
results = []
total_p2p_wh = 0
total_p2p_value = 0
total_grid_wh = 0
for h in range(hours):
surplus = surplus_per_hour[h]
demand = demand_per_hour[h]
# P2P quantity = minimum between surplus and demand
p2p_wh = min(surplus, demand)
p2p_price = np.random.uniform(*config["p2p_price_range"]) / 1000 # EUR/Wh
# P2P transaction value
p2p_value = p2p_wh * p2p_price
# Remaining energy sold to grid or bought from grid
grid_sell_wh = max(0, surplus - p2p_wh) # excess not absorbed by P2P
grid_buy_wh = max(0, demand - p2p_wh) # deficit not covered by P2P
total_p2p_wh += p2p_wh
total_p2p_value += p2p_value
total_grid_wh += grid_buy_wh
results.append({
"hour": h,
"surplus_wh": surplus,
"demand_wh": demand,
"p2p_wh": p2p_wh,
"p2p_price": p2p_price * 1000, # EUR/kWh for output
"p2p_value_eur": p2p_value,
"grid_sell_wh": grid_sell_wh,
"grid_buy_wh": grid_buy_wh
})
# Calculate P2P savings vs. traditional scenario
# Traditional scenario: all sold to grid, all purchased from grid
traditional_value_sell = total_p2p_wh * config["grid_price_sell"] / 1000
traditional_value_buy = total_p2p_wh * config["grid_price_buy"] / 1000
p2p_benefit = total_p2p_value - traditional_value_sell # seller earns more
# Carbon credits: 400 gCO2/kWh Italian grid mix
co2_avoided_kg = total_p2p_wh * 400 / 1_000_000 # kg CO2
summary = {
"total_p2p_wh": total_p2p_wh,
"total_p2p_kwh": total_p2p_wh / 1000,
"total_p2p_value": total_p2p_value,
"average_p2p_price": total_p2p_value / total_p2p_wh * 1000 if total_p2p_wh > 0 else 0,
"n_transactions": len([r for r in results if r["p2p_wh"] > 0]),
"p2p_benefit_eur": p2p_benefit,
"co2_avoided_kg": co2_avoided_kg,
"gse_incentivo": total_p2p_wh * config["incentivo_gse"] / 1_000_000, # EUR
"hourly_data": results
}
return summary
# Run simulation
results = run_p2p_market_simulation()
print(f"=== REC Perugia Simulation - 1 summer day ===")
print(f"P2P energy traded: {results['total_p2p_kwh']:.1f} kWh")
print(f"P2P market value: {results['total_p2p_value']:.2f} EUR")
print(f"Average P2P price: {results['average_p2p_price']:.4f} EUR/kWh")
print(f"Benefit vs grid: {results['p2p_benefit_eur']:.2f} EUR")
print(f"CO2 avoided: {results['co2_avoided_kg']:.1f} kg")
print(f"GSE incentive: {results['gse_incentivo']:.2f} EUR")
print(f"Simulated transactions: {results['n_transactions']}")
Typical Simulation Results
| Metric | P2P Blockchain Scenario | Traditional Scenario (GSE) | P2P Benefit |
|---|---|---|---|
| Energy shared/day | 850-1,200 kWh | 850-1,200 kWh | Identical (physically) |
| Average price paid to prosumer | 90-100 EUR/MWh | 50 EUR/MWh (GSE prize only) | +40-50 EUR/MWh to prosumer |
| Average price paid by consumer | 90-100 EUR/MWh | 300+ EUR/MWh (retail tariff) | -200 EUR/MWh to consumer |
| Settlement speed | < 30 seconds | 30-90 days | Immediate liquidity |
| Intermediation costs | 0.5% (REC treasury fee) | 5-15% (utility + DSO) | 4-14% savings |
| Transparency | On-chain, verifiable | GSE portal, delayed | Real-time audit |
Tokenomics: Energy Tokens, Carbon Credits and Prosumer Incentives
A well-designed tokenomics system is fundamental to ensuring the economic sustainability of the P2P marketplace and the correct incentives for all participants.
Marketplace Token Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title CarbonCreditToken (CCT)
* @notice Token representing 1 kg of CO2 avoided from renewable production.
* Automatically issued on each EWT mint (proportional to source).
* Sellable on carbon credit marketplace (Toucan Protocol, Verrà Bridge).
*/
contract CarbonCreditToken is ERC20, AccessControl {
bytes32 public constant CCT_MINTER = keccak256("CCT_MINTER");
// Emission factors per source (gCO2 avoided per Wh)
mapping(string => uint256) public co2FactorPerWh;
constructor() ERC20("Carbon Credit Token", "CCT") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
// Updated ISPRA 2024 factors
co2FactorPerWh["SOLAR"] = 400; // gCO2/kWh avoided vs Italian grid mix
co2FactorPerWh["WIND"] = 400;
co2FactorPerWh["HYDRO"] = 350;
co2FactorPerWh["BIOMASS"] = 100; // net (carbon cycle)
}
/**
* @notice Mints CCT proportionally to renewable production.
* 1 CCT = 1 kg CO2 avoided = 0.001 TCO2 (tonne CO2)
*
* @param recipient Prosumer receiving the carbon credit
* @param whProduced Energy produced in Wh
* @param sourceType Source type for emission factor calculation
*/
function mintCredits(
address recipient,
uint256 whProduced,
string calldata sourceType
) external onlyRole(CCT_MINTER) {
uint256 factor = co2FactorPerWh[sourceType];
require(factor > 0, "CCT: unknown source");
// gCO2 / 1000 = kgCO2 = number of CCTs
uint256 cctAmount = whProduced * factor / 1_000_000; // gCO2 -> kgCO2
if (cctAmount > 0) {
_mint(recipient, cctAmount * 1e18); // 18 decimals for ERC-20 compatibility
}
}
}
// ======= TOKENOMICS SUMMARY =======
/*
Token flow for a prosumer with 4 kWp PV producing 20 kWh on a summer day:
1. PRODUCTION: Smart meter measures 20,000 Wh exported
2. EWT MINT: Oracle mints 20,000 EWT (1 EWT = 1 Wh)
3. CCT MINT: 20,000 Wh * 400 gCO2/kWh / 1,000,000 = 8 CCT (8 kg CO2 avoided)
4. P2P TRADE: Sells 15,000 EWT @ 0.095 EUR/kWh on marketplace
= 1.425 EUR gross
- 0.5% fee = 7.12 EUR to REC treasury
= 1,417.88 EUR net to prosumer
5. RESIDUAL: 5,000 EWT sold to grid via GSE
= 0.05 EUR/kWh * 5 kWh = 0.25 EUR + GSE incentive 0.11 EUR/kWh = 0.55 EUR
6. CARBON CREDITS: 8 CCT sold on Toucan Protocol (voluntary market ~$12 USD/tCO2)
= 8 kg * 0.012 USD/kg = 0.096 USD
DAILY TOTAL: 1,417.88 + 0.55 + 0.096 = ~1.96 EUR (vs 0.55 EUR without P2P)
P2P Benefit: +255% compared to GSE incentive alone
Note: illustrative prices based on 2025 market estimates
*/
Performance and Scaling: Throughput for a Real REC
How many transactions per second does a REC need? The numbers are surprisingly manageable: even a large REC with 1,000 prosumers generates at most a few dozen trades every 15 minutes. The real scalability requirement emerges when talking about regional aggregators with thousands of RECs.
Required Throughput Estimate
| Scenario | Participants | Trades/15min | Required TPS | Suitable Blockchain |
|---|---|---|---|---|
| Small REC | 50 prosumers + 200 consumers | 10-50 | < 0.1 | Any L2 |
| Large REC | 500 prosumers + 2,000 consumers | 100-500 | < 1 | Polygon, Arbitrum, EWC |
| Regional aggregator | 100 RECs, ~50,000 users | 10,000-50,000 | < 100 | EWC + dedicated Layer 2 |
| National platform | 10,000 RECs, 5M users | 1M+ | 1,000+ | Rollups + state channels |
Optimization: State Channels for Micro-Trades
// State Channel for high-frequency trading between two prosumers
// Only channel opening and closing go on-chain (2 transactions total)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title EnergyStateChannel
* @notice Bidirectional channel for high-frequency energy trading.
* Opening and closing on-chain, intermediate transactions off-chain.
* Suitable for fixed bilateral agreements (e.g. neighboring pair).
*/
contract EnergyStateChannel {
address public partyA; // prosumer (habitual seller)
address public partyB; // consumer (habitual buyer)
uint256 public channelId;
uint256 public expiresAt;
// Balances deposited in the channel
uint256 public depositA_EWT; // EWT locked by partyA
uint256 public depositB_EUR; // EUR (stablecoin) locked by partyB
bool public settled;
struct ChannelState {
uint256 nonce; // increasing sequence (anti-replay)
uint256 aBalance_EWT; // EWT owed to A
uint256 bBalance_EUR; // EUR owed to B
bytes sigA; // A's signature
bytes sigB; // B's signature
}
event ChannelOpened(address partyA, address partyB, uint256 channelId);
event ChannelClosed(uint256 channelId, uint256 finalNonce);
/**
* @notice Closes the channel with the final state agreed off-chain.
* Both parties have signed the final state.
* Execute on-chain only at the end of the session (daily/weekly).
*/
function closeChannel(ChannelState calldata finalState) external {
require(!settled, "SC: channel already closed");
require(block.timestamp <= expiresAt, "SC: channel expired");
// Verify both parties' signatures
bytes32 stateHash = keccak256(abi.encodePacked(
channelId,
finalState.nonce,
finalState.aBalance_EWT,
finalState.bBalance_EUR
));
require(_verifySignature(stateHash, finalState.sigA, partyA), "SC: invalid A signature");
require(_verifySignature(stateHash, finalState.sigB, partyB), "SC: invalid B signature");
settled = true;
// Distribute tokens according to agreed final state
// (EWT and EUR transfer from deposits to parties)
// ... implementation ...
emit ChannelClosed(channelId, finalState.nonce);
}
function _verifySignature(
bytes32 hash,
bytes memory sig,
address expectedSigner
) internal pure returns (bool) {
bytes32 prefixed = keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32", hash
));
(uint8 v, bytes32 r, bytes32 s) = _splitSignature(sig);
return ecrecover(prefixed, v, r, s) == expectedSigner;
}
function _splitSignature(bytes memory sig)
internal pure returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65, "SC: invalid signature");
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
}
}
The Future: DeFi for Energy and Transactive Energy
The applications we are describing today are just the beginning of a deeper convergence between decentralized finance (DeFi) and energy infrastructure. Here are the most promising directions for the next 2-5 years:
Emerging Trends 2025-2030
- Energy Futures on DeFi: futures contracts on tokenized PV production, with Uniswap v4 or Curve as AMM (Automated Market Maker). Prosumers can hedge weather risk by selling futures on their expected production.
- Carbon Credit DeFi (ReFi): protocols like Toucan, KlimaDAO and Flowcarbon are building on-chain markets for carbon credits. Direct integration with EWT will allow bundling energy + carbon credit into a single token.
- Transactive Energy: the next step beyond P2P trading - full demand/response automation with AI autonomously managing the prosumer's energy portfolio optimization. Smart contracts + ML for optimal marketplace bidding.
- V2G P2P: electric vehicles as active participants in the energy marketplace. The vehicle automatically sells battery energy when the P2P price exceeds the threshold set by the user.
- EU Energy Data Space: the Gaia-X project and the European Energy Data Space create the infrastructure for interoperable energy data sharing. Blockchain can serve as the trust and settlement layer.
Conclusions and Next Steps
P2P energy trading on blockchain represents one of the most concrete and mature applications of blockchain technology outside the purely financial sector. This is not speculation: the Solidity contracts we have seen in this article are deployable today on Energy Web Chain or Polygon zkEVM with gas costs below 0.05 EUR per transaction.
The Italian regulatory framework, with Legislative Decree 199/2021 and the 2.2 billion PNRR for RECs, creates a concrete opportunity for developers who want to build in this space. RECs are recognized legal entities, have access to public incentives, and the RED III directive further strengthens their rights in 2025-2026.
Constraints remain: DSO integration requires non-trivial B2B agreements, the MiCA classification of energy tokens must be handled with legal care, and GDPR imposes a privacy-first design with ZK proofs. But none of these obstacles is insurmountable for a team with blockchain expertise and energy domain knowledge.
With this article we conclude the EnergyTech Series. We have traversed the entire technology stack of the energy transition: from Smart Grids and IoT to energy tokenization on blockchain, passing through DERMS, BMS, Digital Twin, ML for renewable forecasting, V2G, MQTT/InfluxDB, IEC 61850 and carbon accounting.
Resources for Further Study
- Energy Web Foundation: EWC documentation and SDK - energyweb.org/technology/energy-web-chain
- OpenZeppelin Contracts: secure smart contract library - docs.openzeppelin.com/contracts
- GSE CACER Portal: regulations and Italian REC incentives - gse.it/servizi-per-te/autoconsumo/comunita-energetiche-rinnovabili
- Hardhat Documentation: testing and deployment framework - hardhat.org/docs
- Polygon zkEVM: L2 with ZK proofs for privacy - polygon.technology/polygon-zkevm
- EDPB Guidelines on Blockchain (2025): GDPR guidelines on blockchain - edpb.europa.eu/our-work-tools/documents/our-documents
- RED III Directive (EU 2023/2413): official text - eur-lex.europa.eu
Related Series on federicocalo.dev
- MLOps for Business: how to put ML models into production for energy forecasting and demand response
- Data & AI Business: data lakehouse architecture for energy data, ETL/ELT with dbt and Airbyte for energy time-series
- AI Engineering/RAG: LLMs for REC contract analysis and virtual assistants for prosumers







