Development of Blockchain Solution for Logistics
Logistics is one of the few sectors where blockchain gives real advantage over traditional databases. Reason: supply chain involves many independent parties (manufacturer, freight forwarder, customs, port, carrier, recipient), each keeps own records, and reconciliation between them takes days and requires expensive intermediaries. TradeLens (Maersk + IBM), Morpheus.Network, CargoX are real examples. Blockchain here is not "technology for technology's sake" but shared ledger for multi-party system without single trusted center.
What Blockchain Specifically Solves in Logistics
Three problems worth real money:
Document authenticity: Bill of Lading is key document in maritime logistics. Traditionally paper, transferred by courier. Electronic B/L (eBL) existed long, but centralized platforms (essDOCS, Bolero) require trust in operator. CargoX implements B/L as NFT (ERC-721) on Ethereum — ownership is transferable on-chain without intermediary.
Transparency of transaction terms: smart contract escrow: payment released automatically upon delivery confirmation. No need for bank guarantees or letters of credit for small deals.
Tracking and provenance: for pharmaceuticals, luxury, food — verification of origin and storage chain (temperature, humidity) critical. IoT sensors + blockchain = immutable audit trail.
Document Workflow Architecture
Bill of Lading as 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; // external ID from TMS
address shipper;
address consignee;
string portOfLoading;
string portOfDischarge;
string cargoDescription;
uint256 quantity;
string unit; // TEU, tonnes, pallets
uint256 issuedAt;
ShipmentStatus status;
bytes32 dataHash; // hash of full document in IPFS
}
enum ShipmentStatus {
Issued,
InTransit,
ArrivedAtPort,
CustomsCleared,
Delivered,
Surrendered
}
mapping(uint256 => ShipmentData) public shipments;
mapping(uint256 => string[]) public statusHistory; // log of status changes
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];
// Role checks for each transition
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 can only be transferred in certain statuses
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal override
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if (from != address(0)) { // not mint
ShipmentStatus status = shipments[tokenId].status;
require(
status == ShipmentStatus.Issued || status == ShipmentStatus.InTransit,
"BL not transferable in current status"
);
}
}
}
Escrow for Payments
Payment frozen in smart contract until delivery confirmation:
contract ShipmentEscrow {
enum EscrowState { Created, Funded, Released, Disputed, Refunded }
struct Escrow {
address buyer;
address seller;
address carrier;
uint256 amount;
address token; // USDC or other stablecoin
uint256 blTokenId; // B/L NFT ID
address blContract;
EscrowState state;
uint256 releaseDeadline; // if no dispute before deadline — auto-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 // buffer for 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");
// Check that B/L has Delivered status
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 Integration: Sensor Data to Blockchain
For cold chain (pharmaceuticals, products), verification of storage conditions critical. IoT → blockchain requires solving oracle problem: smart contract can't get data from physical sensors directly.
IoT Oracle Architecture
IoT sensors (temperature, humidity, GPS)
↓ MQTT / LoRaWAN
IoT gateway (Raspberry Pi / industrial gate)
↓ signed data packages
Oracle service (Chainlink Functions or custom)
↓ on-chain transaction
Smart contract (recording telemetry)
contract ShipmentTelemetry {
struct TelemetryRecord {
uint256 timestamp;
int16 temperature; // in tenths of degree (156 = 15.6°C)
uint16 humidity; // in tenths of percent
int32 latitude; // in microdegrees
int32 longitude;
address oracle; // who signed the data
}
mapping(uint256 => TelemetryRecord[]) public telemetry; // tokenId => records
mapping(uint256 => bool) public conditionViolated; // were there violations
// Permissible ranges for cargo
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 {
// Verify oracle signature
bytes32 dataHash = keccak256(abi.encodePacked(
shipmentTokenId, temperature, humidity, lat, lon, block.timestamp / 300 // 5-min window
));
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
}));
// Check for condition violations
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);
}
}
}
Blockchain Choice for Logistics
Public blockchain (Polygon, Arbitrum): maximum openness, permissionless participants, B/L tokenization with DeFi integration. Downsides: gas, transaction publicity (competitors see volumes).
Enterprise blockchain (Hyperledger Fabric, R3 Corda): private data, permissioned participants, no gas. Downsides: no tokenization, no DeFi composability, high node costs.
Hybrid: private data in Fabric, hashes in public blockchain for notarization. More complex to develop, but combines advantages of both.
For B2B logistics project with known participants — Hyperledger Fabric or Polygon with private transactions (zk-based). For open protocol with tokenization — Polygon or Arbitrum.
Integration with Existing Systems
Logistics TMS (Transportation Management Systems), ERP (SAP, Oracle TMS) have REST/SOAP APIs. Integration layer:
class LogisticsIntegration {
private web3Provider: Provider;
private blContract: ElectronicBillOfLading;
// Webhook from TMS on cargo status change
async handleTMSStatusUpdate(event: TMSEvent) {
const { shipmentId, newStatus, timestamp, operator } = event;
const tokenId = await this.getTokenIdByShipmentId(shipmentId);
const onChainStatus = this.mapTMSStatusToOnChain(newStatus);
// Send transaction
const tx = await this.blContract.updateStatus(
tokenId,
onChainStatus,
`TMS update: ${newStatus} at ${timestamp}`
);
await tx.wait();
// Update local database
await this.db.shipments.update({
where: { shipmentId },
data: { lastTxHash: tx.hash, onChainStatus },
});
}
}
Development Stack
| Component | Technology |
|---|---|
| B/L contracts | Solidity + ERC-721 + Foundry |
| Escrow | Solidity + OpenZeppelin |
| IoT oracle | Chainlink Functions or custom oracle with HSM signature |
| Backend API | Node.js/TypeScript + Fastify |
| Documents | IPFS + AES encryption for private docs |
| TMS integration | REST webhooks + message queue (RabbitMQ) |
| Frontend | Next.js + wagmi + shadcn |
Timeline
MVP (B/L NFT + basic status tracking + simple escrow): 6–8 weeks.
Production with IoT integration, multi-party workflow, TMS integration and audit reporting: 4–6 months.
Key complexity not technical but organizational: all supply chain parties must adopt system. Technical integration with each participant adds 2–4 weeks of work.







