Розробка блокчейн-рішення для логістики
Логістика — один із небагатьох секторів, де блокчейн дає реальне преимущество над традиційними базами даних. Причина: у ланцюгу поставок беруть участь множество незалежних сторін (виробник, експедитор, таможня, порт, перевозник, одержувач), кожна ведеться власний облік, і рекончиляція між ними займає дні та вимагає дорогих посередників. TradeLens (Maersk + IBM), Morpheus.Network, CargoX — реальні приклади. Блокчейн тут не «технологія заради технології», а shared ledger для мультисторонної системи без єдиного доверенного центру.
Що конкретно вирішує блокчейн у логістиці
Три проблеми, які коштують реальних грошей:
Підлінність документів: Bill of Lading — ключовий документ у морській логістиці. Традиційно — паперовий, передається курʼєром. Електронний B/L (eBL) давно існує, але централізовані платформи (essDOCS, Bolero) вимагають довіри до оператора. CargoX реалізує B/L як NFT (ERC-721) на Ethereum — ownership трансферабелен on-chain без посередника.
Прозорість умов угоди: смарт-контракт-escrow: оплата висвобджується автоматично при підтвердженні доставки. Не потрібні банківські гарантії або аккредитиви для невеликих угод.
Трекинг та provenance: для фармацевтики, люкса, продовольства — критична верифікація origin та ланцюга зберігання (температура, вологість). IoT-сенсори + блокчейн = неізмінний audit trail.
Архітектура документообороту
Bill of Lading як NFT
contract ElectronicBillOfLading is ERC721, AccessControl {
bytes32 public constant CARRIER_ROLE = keccak256("CARRIER_ROLE");
bytes32 public constant CUSTOMS_ROLE = keccak256("CUSTOMS_ROLE");
struct ShipmentData {
string shipmentId; // зовнішній ID з TMS
address shipper;
address consignee;
string portOfLoading;
string portOfDischarge;
string cargoDescription;
uint256 quantity;
string unit; // TEU, tonnes, pallets
uint256 issuedAt;
ShipmentStatus status;
bytes32 dataHash; // хеш повного документу в IPFS
}
enum ShipmentStatus {
Issued,
InTransit,
ArrivedAtPort,
CustomsCleared,
Delivered,
Surrendered
}
mapping(uint256 => ShipmentData) public shipments;
mapping(uint256 => string[]) public statusHistory; // лог змін статусу
uint256 private _tokenIdCounter;
function issueBL(
address consignee,
string calldata shipmentId,
string calldata portOfLoading,
string calldata portOfDischarge,
string calldata cargoDescription,
uint256 quantity,
string calldata unit,
bytes32 dataHash
) external onlyRole(CARRIER_ROLE) returns (uint256) {
uint256 tokenId = ++_tokenIdCounter;
_mint(consignee, tokenId);
shipments[tokenId] = ShipmentData({
shipmentId: shipmentId,
shipper: msg.sender,
consignee: consignee,
portOfLoading: portOfLoading,
portOfDischarge: portOfDischarge,
cargoDescription: cargoDescription,
quantity: quantity,
unit: unit,
issuedAt: block.timestamp,
status: ShipmentStatus.Issued,
dataHash: dataHash
});
emit BLIssued(tokenId, consignee, shipmentId);
return tokenId;
}
function updateStatus(
uint256 tokenId,
ShipmentStatus newStatus,
string calldata note
) external {
ShipmentData storage shipment = shipments[tokenId];
// Перевірка ролей для кожного переходу
if (newStatus == ShipmentStatus.CustomsCleared) {
require(hasRole(CUSTOMS_ROLE, msg.sender), "Only customs");
} else if (newStatus == ShipmentStatus.Delivered) {
require(ownerOf(tokenId) == msg.sender, "Only consignee");
} else {
require(hasRole(CARRIER_ROLE, msg.sender), "Only carrier");
}
ShipmentStatus prevStatus = shipment.status;
shipment.status = newStatus;
statusHistory[tokenId].push(string(abi.encodePacked(
Strings.toString(block.timestamp), ":", note
)));
emit StatusUpdated(tokenId, prevStatus, newStatus, msg.sender);
}
// Override transfer — B/L може передаватися тільки при певних статусах
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal override
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if (from != address(0)) { // не mint
ShipmentStatus status = shipments[tokenId].status;
require(
status == ShipmentStatus.Issued || status == ShipmentStatus.InTransit,
"BL not transferable in current status"
);
}
}
}
Escrow для платежів
Оплата заморожена в смарт-контракті до підтвердження доставки:
contract ShipmentEscrow {
enum EscrowState { Created, Funded, Released, Disputed, Refunded }
struct Escrow {
address buyer;
address seller;
address carrier;
uint256 amount;
address token; // USDC або інший stablecoin
uint256 blTokenId; // ID B/L NFT
address blContract;
EscrowState state;
uint256 releaseDeadline; // якщо немає dispute до deadline — авто-release
}
mapping(bytes32 => Escrow) public escrows;
function createEscrow(
address seller,
address carrier,
uint256 amount,
address token,
uint256 blTokenId,
address blContract,
uint256 deliveryDeadline
) external returns (bytes32 escrowId) {
escrowId = keccak256(abi.encodePacked(msg.sender, seller, blTokenId, block.timestamp));
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
escrows[escrowId] = Escrow({
buyer: msg.sender,
seller: seller,
carrier: carrier,
amount: amount,
token: token,
blTokenId: blTokenId,
blContract: blContract,
state: EscrowState.Funded,
releaseDeadline: deliveryDeadline + 7 days // буфер для dispute
});
}
function confirmDelivery(bytes32 escrowId) external {
Escrow storage escrow = escrows[escrowId];
require(msg.sender == escrow.buyer, "Only buyer");
require(escrow.state == EscrowState.Funded, "Wrong state");
// Перевіряємо що B/L має статус Delivered
ElectronicBillOfLading bl = ElectronicBillOfLading(escrow.blContract);
require(
bl.shipments(escrow.blTokenId).status == ElectronicBillOfLading.ShipmentStatus.Delivered,
"Not delivered on-chain"
);
escrow.state = EscrowState.Released;
IERC20(escrow.token).safeTransfer(escrow.seller, escrow.amount);
}
}
IoT-інтеграція: дані з датчиків в блокчейн
Для cold chain (фармацевтика, продукти) важлива верифікація умов зберігання. IoT → блокчейн вимагає вирішення проблеми oracle: смарт-контракт не може отримати дані з фізичних датчиків напрямки.
Архітектура IoT oracle
IoT датчики (температура, вологість, GPS)
↓ MQTT / LoRaWAN
IoT шлюз (Raspberry Pi / промисловий гейт)
↓ підписані пакети даних
Oracle сервіс (Chainlink Functions або власний)
↓ on-chain транзакція
Смарт-контракт (запис телеметрії)
contract ShipmentTelemetry {
struct TelemetryRecord {
uint256 timestamp;
int16 temperature; // у десятих долях градуса (156 = 15.6°C)
uint16 humidity; // у десятих процента
int32 latitude; // у мікрастепенях
int32 longitude;
address oracle; // хто підписав дані
}
mapping(uint256 => TelemetryRecord[]) public telemetry; // tokenId => records
mapping(uint256 => bool) public conditionViolated; // були ли нарушення
// Допустимі діапазони для вантажу
struct ConditionRequirements {
int16 minTemp;
int16 maxTemp;
uint16 maxHumidity;
}
mapping(uint256 => ConditionRequirements) public requirements;
function submitTelemetry(
uint256 shipmentTokenId,
int16 temperature,
uint16 humidity,
int32 lat,
int32 lon,
bytes calldata oracleSignature
) external {
// Верифікуємо підпис oracle
bytes32 dataHash = keccak256(abi.encodePacked(
shipmentTokenId, temperature, humidity, lat, lon, block.timestamp / 300 // 5-мін окно
));
address signer = ECDSA.recover(dataHash.toEthSignedMessageHash(), oracleSignature);
require(isApprovedOracle(signer), "Unauthorized oracle");
telemetry[shipmentTokenId].push(TelemetryRecord({
timestamp: block.timestamp,
temperature: temperature,
humidity: humidity,
latitude: lat,
longitude: lon,
oracle: signer
}));
// Перевіряємо нарушення умов
ConditionRequirements memory req = requirements[shipmentTokenId];
if (temperature < req.minTemp || temperature > req.maxTemp || humidity > req.maxHumidity) {
conditionViolated[shipmentTokenId] = true;
emit ConditionViolation(shipmentTokenId, temperature, humidity, block.timestamp);
}
}
}
Вибір блокчейну для логістики
Публічний блокчейн (Polygon, Arbitrum): максимальна откритість, permissionless учасники, токенізація B/L з DeFi інтеграцією. Мінуси: gas, публічність транзакцій (конкуренти бачать обсяги).
Enterprise blockchain (Hyperledger Fabric, R3 Corda): приватні дані, permissioned учасники, немає газу. Мінуси: немає токенізації, немає DeFi composability, висока вартість нод.
Hybrid: приватні дані в Fabric, хеші в публічний блокчейн для notarization. Складніше в розробці, але поєднує переваги обох.
Для B2B логістичного проекту з відомими учасниками — Hyperledger Fabric або Polygon з приватними транзакціями (zk-based). Для відкритого протоколу з токенізацією — Polygon або Arbitrum.
Інтеграція з існуючими системами
Логістичні TMS (Transportation Management Systems), ERP (SAP, Oracle TMS) мають REST/SOAP API. Інтеграційний шар:
class LogisticsIntegration {
private web3Provider: Provider;
private blContract: ElectronicBillOfLading;
// Webhook від TMS при зміні статусу вантажу
async handleTMSStatusUpdate(event: TMSEvent) {
const { shipmentId, newStatus, timestamp, operator } = event;
const tokenId = await this.getTokenIdByShipmentId(shipmentId);
const onChainStatus = this.mapTMSStatusToOnChain(newStatus);
// Відправляємо транзакцію
const tx = await this.blContract.updateStatus(
tokenId,
onChainStatus,
`TMS update: ${newStatus} at ${timestamp}`
);
await tx.wait();
// Оновлюємо локальну БД
await this.db.shipments.update({
where: { shipmentId },
data: { lastTxHash: tx.hash, onChainStatus },
});
}
}
Стек розробки
| Компонент | Технологія |
|---|---|
| B/L контракти | Solidity + ERC-721 + Foundry |
| Escrow | Solidity + OpenZeppelin |
| IoT oracle | Chainlink Functions або власний oracle з HSM підпис |
| Backend API | Node.js/TypeScript + Fastify |
| Документи | IPFS + AES encryption для приватних документів |
| TMS інтеграція | REST webhooks + message queue (RabbitMQ) |
| Frontend | Next.js + wagmi + shadcn |
Графік
MVP (B/L NFT + базовий трекинг статусів + простий escrow): 6–8 тижнів.
Production з IoT-інтеграцією, мультистороним workflow, TMS інтеграцією та audit reporting: 4–6 місяців.
Ключова складність не технічна, а організаційна: всі сторони ланцюга поставок повинні прийняти систему. Технічна інтеграція з кожним учасником додає 2–4 тижні роботи.







