# Pin.sol Whitepaper v1 > A Decentralized IPFS pinning protocol built for EVM **Published by:** [ink blots](https://paragraph.com/@jonbray/) **Published on:** 2025-03-22 **Categories:** ipfs, ethereum, storage, decentralization, dao **URL:** https://paragraph.com/@jonbray/pinsol-v1 ## Content AbstractPin.sol is a decentralized protocol designed to incentivize persistent file storage on the InterPlanetary File System (IPFS) through direct ETH payments on EVM networks. By leveraging a decentralized network of storage providers, a fair node rotation queue system, and a reputation-based reward mechanism, Pin.sol ensures reliable file availability and contract-level access to IPFS nodes for increased composability.IntroductionIPFS has emerged as a powerful distributed file system, but it lacks native economic incentives to ensure long-term file persistence. When a file is uploaded to IPFS, there's no guarantee it will remain accessible unless someone actively "pins" it to their node. While solutions to IPFS file availability do exist, none of them are directly composable with the Ethereum ecosystem or smart contracts. Current solutions fall into three categories:Centralized pinning services that create single points of failure and go against the ethos of decentralizationCustom blockchain networks like Filecoin and Arweave, which require migration to separate ecosystemsSelf-hosted infrastructure that is resource-intensive and technically complexPin.sol introduces a simple, Ethereum-native solution where IPFS node operators are directly rewarded in ETH for providing reliable pinning services, while users can prioritize their content through market-based mechanisms, all without requiring a separate token economy.Core Mechanisms Wallet-Linked IPFS NodesEach participating IPFS node connects an Ethereum wallet and stake ETH as collateral to participate in the protocol. Nodes receive ETH payments for pinning files from users, with smart contracts tracking node participation, staking, and ensuring service delivery through community verification of pinned files.Fair Queue SystemParticipating nodes are placed in a rotation queue to receive pinning assignments. After completing a job, the node moves to the back of the queue to ensure all participating nodes have fair access to earning opportunities. The base ETH cost per file is calculated based on file size and requested duration.Reputation-Based Reward DistributionNodes build reputation based on reliability and service quality which determines the percentage of payment received from pin requests:Highest reputation nodes receive 99% of paymentLower reputation nodes receive proportionally less (down to 50%)Remaining percentage goes to the DAO treasury, with the DAO taking a base 1% of protocol revenue.Dual Pricing ModelsPin.sol supports two complementary pricing models:1. Standard Queue-Based SystemUsers submit pinning requests with required ETH payment based on file size and duration. The protocol automatically assigns files to the next node(s) in the queue based on the specified replication factor.2. Direct Node PaymentsUsers can bypass the queue and pay nodes directly in ETH for long-term pinning, creating a decentralized marketplace where users and nodes negotiate terms. In direct agreements, node reputation does not affect payment percentage.Escrow & Protection MechanismFunds are held in escrow and are vested linearly for the duration that pinning service is provided for. If a node unpins content prematurely, the remaining escrowed ETH is returned to the payer with 5% of returned funds being awarded to users who report violations, creating community incentives to report bad actors.Node Banning MechanismNodes that consistently fail to meet protocol requirements face progressive penalties to reputation and earnings: First offense: Short-term ban. First offenses do not incur a reputation strike. Repeated offenses: Increasingly longer ban and reputation hit. The more a node misbehaves, the lower it's reputation gets (down to 50%). Severe violations: Perma-ban and slashing of staked ETH. Bans are enforced transparently onchain, and appeals can be made through a decentralized governance process. Slashed ETH goes to the DAO treasuryVerifier Services Pin.sol creates economic opportunities for Verifiers who maintain network integrity by checking IPFS for content availability promised by nodes and verifying that pinning commitments are being honored. Verifiers: Reporting nodes that fail to meet obligations Earning ETH rewards (5% of remaining escrow) for maintaining network healthHow Verifier Services WorkAutomated Monitoring: Verifiers scan the network, checking actual availability of pinned contentReputation Enforcement: Trigger the protocol's reputation mechanisms for failing nodesEconomic Incentives: Earn a percentage of returned funds when reporting violationsThis creates a market for developers to build services around Pin.sol, such as IPFS availability monitors, node reputation tracking dashboards, and analytics tools,.DAO Treasury and FeesPin.sol collects fees that go directly to the DAO treasury:Queue-Based Assignments: A percentage of each payment based on node reputation:1% from highest reputation nodes (99% to node)Up to 50% from lowest reputation nodes (50% to node)Direct Payments: Flat 1% fee on all direct agreements between users and nodesComparison with Other Storage SolutionsFeaturePin.solFilecoinArweaveCentralized PinningNative CurrencyETHFILARVariousSmart Contract IntegrationDirectLimitedLimitedNoneBarrier to EntryLow (standard IPFS node)High (specialized hardware)MediumN/APayment ModelQueue-based + direct paymentLong-term contractsOne-time perpetualSubscriptionNode SelectionFair queue rotationAuction/manual dealsMiners competeCentralizedStorage VerificationCommunity-drivenProof-of-SpacetimeProof-of-AccessCentralizedContent AddressingIPFS CIDsIPFS CIDsTransaction-basedProprietaryNode EconomicsReputation-based rewardsComplex deal structureEndowment-basedFixed pricingCensorship ResistanceHighHighVery HighLowWhy Choose Pin.sol?Ethereum Native: Uses ETH directly, no need for token swaps or bridgesFair Distribution: Queue-based system ensures all nodes get opportunitiesIncentive Alignment: Reputation affects rewards, not job accessLow Barrier: Anyone with an IPFS node can participate and earnTransparent: All commitments and payments are visible on-chainSustainable: DAO treasury ensures long-term protocol developmentSecurity & SustainabilityStake-Based Participation: Required ETH staking ensures nodes have skin in the gameLinear Vesting Model: Payments release over time, aligning incentives for long-term storageCommunity Policing: The 5% bounty for reporting violations creates distributed oversightTransparent Queue: Fair job distribution prevents centralizationReputation Economics: Better performance leads to higher earnings percentageProgressive Banning: Repeat offenders face escalating penaltiesSustainable Treasury: Protocol fees fund ongoing development and maintenanceSmart Contract DesignBelow is an outline of proposed smart contract design for Pin.sol. This code is not tested and should not be used in a production environment.Node Registration & Queue ManagementStructure of an onchain IPFS node bool) activePins; // Currently pinned content } ">struct Node { address payable wallet; // Wallet address of node operator string ipfsPeerId; // Unique identifier for IPFS node uint256 reputation; // Score representing node reliability (0-100) uint256 stakedAmount; // Total ETH locked as collateral uint256 lastActive; // Timestamp of most recent activity bool isActive; // Whether node is currently eligible for assignments uint256 bannedUntil; // Timestamp when temporary ban expires (0 if not banned) uint256 banCount; // Number of times node has been banned uint256 totalEarned; // Total ETH earned through the protocol mapping(bytes32 => bool) activePins; // Currently pinned content } Implementation of the node queue system. uint256) private nodeQueuePosition; ">address[] private nodeQueue; mapping(address => uint256) private nodeQueuePosition; New nodes register with their peerId by staking ETH and are placed at the end of the queue.= MINIMUM_STAKE, "Insufficient stake"); require(nodeQueuePosition[msg.sender] == 0, "Already registered"); nodes[msg.sender] = Node({ wallet: payable(msg.sender), ipfsPeerId: ipfsPeerId, reputation: INITIAL_REPUTATION, stakedAmount: msg.value, lastActive: block.timestamp, isActive: true, bannedUntil: 0, banCount: 0, totalEarned: 0 }); // Add node to the end of the queue nodeQueue.push(msg.sender); nodeQueuePosition[msg.sender] = nodeQueue.length; emit NodeRegistered(msg.sender, ipfsPeerId, msg.value); } ">function registerNode(string calldata ipfsPeerId) external payable { require(msg.value >= MINIMUM_STAKE, "Insufficient stake"); require(nodeQueuePosition[msg.sender] == 0, "Already registered"); nodes[msg.sender] = Node({ wallet: payable(msg.sender), ipfsPeerId: ipfsPeerId, reputation: INITIAL_REPUTATION, stakedAmount: msg.value, lastActive: block.timestamp, isActive: true, bannedUntil: 0, banCount: 0, totalEarned: 0 }); // Add node to the end of the queue nodeQueue.push(msg.sender); nodeQueuePosition[msg.sender] = nodeQueue.length; emit NodeRegistered(msg.sender, ipfsPeerId, msg.value); } Nodes are rotated to the back of the queue after being assigned a pin request. 0, "Node not in queue"); // Remove node from current position for (uint i = pos; i function _rotateNodeInQueue(address node) internal { uint256 pos = nodeQueuePosition[node]; require(pos > 0, "Node not in queue"); // Remove node from current position for (uint i = pos; i < nodeQueue.length; i++) { nodeQueue[i-1] = nodeQueue[i]; nodeQueuePosition[nodeQueue[i]] = i; } // Put node at end of queue nodeQueue[nodeQueue.length - 1] = node; nodeQueuePosition[node] = nodeQueue.length; } Contract gets the next available nodes from the queuefunction _getNextAvailableNodes(uint8 count) internal view returns (address[] memory) { address[] memory availableNodes = new address[](count); uint256 found = 0; for (uint i = 0; i < nodeQueue.length && found < count; i++) { address nodeAddr = nodeQueue[i]; if (nodes[nodeAddr].isActive && nodes[nodeAddr].bannedUntil < block.timestamp) { availableNodes[found] = nodeAddr; found++; } } require(found == count, "Not enough available nodes"); return availableNodes; } File Pinning Requests & PricingStructure of an onchain pin requeststruct PinRequest { bytes32 cid; // Content Identifier (CID) of file address payable user; // Address of requestor uint256 duration; // Time period (in seconds) for pinning uint256 fileSize; // Size of file in bytes uint256 payment; // Total ETH payment uint256 createdAt; // Timestamp when request was submitted bool fulfilled; // Whether request has been assigned to nodes uint8 replicationFactor; // Number of nodes to pin the content } The price of a pin request is determined based on file size and duration the pin is requested for. DAO fees can be updated by governance proposals but is initialized at 1%.function calculateBasePrice(uint256 fileSize, uint256 duration) public pure returns (uint256) { // Example pricing formula // Base price = (fileSize in MB * 0.0001 ETH) * (duration in days / 30) uint256 fileSizeInMB = fileSize / (1024 * 1024); uint256 durationInDays = duration / 86400; return (fileSizeInMB * 1e14 * durationInDays) / 30; } Pin requests are submitted with a replicationFactor that determines how many nodes should pin the file. Payment cost is (fileSize * duration) * replicationFactor and paid at the time of submission, after which the payment is split between the number of participating nodes and their respective fees (modified by reputation) are vested to them.= totalPrice, "Insufficient payment"); require(duration >= MIN_DURATION, "Duration too short"); require(replicationFactor > 0, "Must request at least one node"); uint256 requestId = _nextRequestId++; pinRequests[requestId] = PinRequest({ cid: cid, user: payable(msg.sender), duration: duration, fileSize: fileSize, payment: msg.value, createdAt: block.timestamp, fulfilled: false, replicationFactor: replicationFactor }); // Process the request immediately _processRequest(requestId); emit PinRequestSubmitted(requestId, cid, msg.sender, msg.value, duration); } ">function submitPinRequest( bytes32 cid, uint256 fileSize, uint256 duration, uint8 replicationFactor ) external payable { // Calculate minimum required payment uint256 basePrice = calculateBasePrice(fileSize, duration); uint256 totalPrice = basePrice * replicationFactor; require(msg.value >= totalPrice, "Insufficient payment"); require(duration >= MIN_DURATION, "Duration too short"); require(replicationFactor > 0, "Must request at least one node"); uint256 requestId = _nextRequestId++; pinRequests[requestId] = PinRequest({ cid: cid, user: payable(msg.sender), duration: duration, fileSize: fileSize, payment: msg.value, createdAt: block.timestamp, fulfilled: false, replicationFactor: replicationFactor }); // Process the request immediately _processRequest(requestId); emit PinRequestSubmitted(requestId, cid, msg.sender, msg.value, duration); } Node Assignments & Reputation-Based PaymentsStructure of successful pin assignment to a node.struct PinAssignment { bytes32 cid; // CID of pinned file address node; // Address of assigned node address payable user; // User who requested the pinning uint256 startTime; // When pinning began uint256 endTime; // When pinning should end uint256 totalPayment; // Total ETH allocated for this assignment uint256 claimedAmount; // ETH already claimed through vesting bool active; // Whether assignment is currently active } Payment percentage is calculated based on node reputation. At max reputation (100) a node receives 99% of fees. At 0 reputation, a node gets 50% of the fees, with a linear scale between. Nodes with 0 reputation who receive another strike are banned from participation.function _calculatePaymentPercentage(uint256 reputation) internal pure returns (uint256) { uint256 basePercentage = 50; // Multiply reputation (0-100) by 49 (difference between min & max payment received) to get the variable portion uint256 variablePercentage = (reputation * 49) / 100; return basePercentage + variablePercentage; } Requests are processed from the queue, selecting next available nodes and processing their reputation-based payment into a linear vesting schedule. 0) { (bool success, ) = daoTreasury.call{value: daoFee}(""); require(success, "DAO fee transfer failed"); emit DAOFeeCollected(requestId, daoFee); } request.fulfilled = true; } ">function _processRequest(uint256 requestId) internal { PinRequest storage request = pinRequests[requestId]; require(!request.fulfilled, "Request already fulfilled"); // Get the next available nodes address[] memory selectedNodes = _getNextAvailableNodes(request.replicationFactor); uint256 paymentPerNode = request.payment / request.replicationFactor; uint256 daoFee = 0; for (uint i = 0; i < selectedNodes.length; i++) { address node = selectedNodes[i]; // Calculate node's payment based on reputation uint256 paymentPercentage = _calculatePaymentPercentage(nodes[node].reputation); uint256 nodePayment = (paymentPerNode * paymentPercentage) / 100; // Track DAO fee (remainder) daoFee += paymentPerNode - nodePayment; // Create assignment with vesting schedule uint256 assignmentId = _nextAssignmentId++; pinAssignments[assignmentId] = PinAssignment({ cid: request.cid, node: node, user: request.user, startTime: block.timestamp, endTime: block.timestamp + request.duration, totalPayment: nodePayment, claimedAmount: 0, active: true }); // Update node's active pins nodes[node].activePins[request.cid] = true; // Rotate node to back of queue _rotateNodeInQueue(node); emit PinAssigned(assignmentId, request.cid, node, nodePayment, request.duration); } // Send DAO fee to treasury if (daoFee > 0) { (bool success, ) = daoTreasury.call{value: daoFee}(""); require(success, "DAO fee transfer failed"); emit DAOFeeCollected(requestId, daoFee); } request.fulfilled = true; } Direct PaymentsIn addition to the queue system, users can select and pay a node directly.struct DirectPayment { bytes32 cid; // Content Identifier address payable node; // Node providing service address payable user; // User paying for service uint256 amount; // Total ETH paid to node // DAO fee initialized at 1% but can be modified by governance proposals uint256 daoFee; // Fee collected by DAO uint256 startTime; // When agreement began uint256 endTime; // When service should end uint256 claimedAmount; // ETH already claimed bool active; // Current status } Direct payments are negotiated between both parties and bypasses reputation, meaning that nodes paid directly receive the full fee (minus DAO percentage). 0, "Payment required"); require(nodes[node].isActive, "Node not active"); require(nodes[node].bannedUntil function payNodeDirectly( address payable node, bytes32 cid, uint256 duration ) external payable { require(msg.value > 0, "Payment required"); require(nodes[node].isActive, "Node not active"); require(nodes[node].bannedUntil < block.timestamp, "Node is banned"); // Calculate basis points for DAO fee (100 = 1%) uint256 daoFee = (msg.value * daoFeePercentage) / 10000; uint256 nodePayment = msg.value - daoFee; uint256 paymentId = _nextDirectPaymentId++; directPayments[paymentId] = DirectPayment({ cid: cid, node: node, user: payable(msg.sender), amount: nodePayment, daoFee: daoFee, startTime: block.timestamp, endTime: block.timestamp + duration, claimedAmount: 0, active: true }); // Update node's active pins nodes[node].activePins[cid] = true; // Send DAO fee (bool success, ) = daoTreasury.call{value: daoFee}(""); require(success, "DAO fee transfer failed"); emit DirectPaymentCreated(paymentId, cid, node, msg.sender, nodePayment, daoFee, duration); } Claiming Vested PaymentsAt any time during an active assignment, a node can claim the ETH currently being vested to them.= assignment.endTime) { vestedAmount = assignment.totalPayment; } else { vestedAmount = (assignment.totalPayment * elapsed) / totalDuration; } uint256 claimableAmount = vestedAmount - assignment.claimedAmount; require(claimableAmount > 0, "No funds to claim"); // Update claimed amount assignment.claimedAmount += claimableAmount; // Transfer ETH to node (bool success, ) = assignment.node.call{value: claimableAmount}(""); require(success, "Transfer failed"); // Update node's total earned nodes[assignment.node].totalEarned += claimableAmount; emit PaymentClaimed(assignmentId, assignment.node, claimableAmount); } ">function claimVestedPayment(uint256 assignmentId) external { PinAssignment storage assignment = pinAssignments[assignmentId]; require(msg.sender == assignment.node, "Not authorized"); require(assignment.active, "Assignment not active"); // Calculate vested amount uint256 totalDuration = assignment.endTime - assignment.startTime; uint256 elapsed = block.timestamp - assignment.startTime; uint256 vestedAmount; if (block.timestamp >= assignment.endTime) { vestedAmount = assignment.totalPayment; } else { vestedAmount = (assignment.totalPayment * elapsed) / totalDuration; } uint256 claimableAmount = vestedAmount - assignment.claimedAmount; require(claimableAmount > 0, "No funds to claim"); // Update claimed amount assignment.claimedAmount += claimableAmount; // Transfer ETH to node (bool success, ) = assignment.node.call{value: claimableAmount}(""); require(success, "Transfer failed"); // Update node's total earned nodes[assignment.node].totalEarned += claimableAmount; emit PaymentClaimed(assignmentId, assignment.node, claimableAmount); } Verification & SlashingIf a node is found to have prematurely unpinned an item, or is offline long enough for IPFS garbage collection to remove the pin, verifiers can report them to the protocol. NOTE: The verification procedure needs a lot of work to ensure that it's reliable and doesn't maliciously target nodes who are behaving. 0) { // Return 95% to user uint256 userRefund = (remainingFunds * 95) / 100; (bool successUser, ) = assignment.user.call{value: userRefund}(""); require(successUser, "User refund failed"); // Award 5% to reporter uint256 reporterReward = remainingFunds - userRefund; (bool successReporter, ) = payable(msg.sender).call{value: reporterReward}(""); require(successReporter, "Reporter reward failed"); emit SlashingExecuted(assignmentId, node, userRefund, msg.sender, reporterReward); } // Mark assignment as inactive assignment.active = false; // Remove from node's active pins nodes[node].activePins[cid] = false; // Decrease node reputation _decreaseReputation(node, SLASHING_REPUTATION_PENALTY); // Check if ban is warranted _evaluateBanning(node); } ">function reportUnpinnedContent(bytes32 cid, address node, uint256 assignmentId) external { PinAssignment storage assignment = pinAssignments[assignmentId]; require(assignment.cid == cid, "CID mismatch"); require(assignment.node == node, "Node mismatch"); require(assignment.active, "Assignment not active"); // Verification logic to confirm content is indeed not pinned bool contentUnpinned = _verifyContentIsUnpinned(cid, node); require(contentUnpinned, "Content is still pinned"); // Calculate remaining payment uint256 remainingFunds = assignment.totalPayment - assignment.claimedAmount; if (remainingFunds > 0) { // Return 95% to user uint256 userRefund = (remainingFunds * 95) / 100; (bool successUser, ) = assignment.user.call{value: userRefund}(""); require(successUser, "User refund failed"); // Award 5% to reporter uint256 reporterReward = remainingFunds - userRefund; (bool successReporter, ) = payable(msg.sender).call{value: reporterReward}(""); require(successReporter, "Reporter reward failed"); emit SlashingExecuted(assignmentId, node, userRefund, msg.sender, reporterReward); } // Mark assignment as inactive assignment.active = false; // Remove from node's active pins nodes[node].activePins[cid] = false; // Decrease node reputation _decreaseReputation(node, SLASHING_REPUTATION_PENALTY); // Check if ban is warranted _evaluateBanning(node); } Ideally, verifiers would interact with reputation-altering functions through an oracle or other interface that can atomically check IPFS pin status against contract storage. For the actual implementation, this would check IPFS network.function _verifyContentIsUnpinned(bytes32 cid, address node) internal returns (bool) { // // Returns true if content is confirmed to be unpinned // return true; } Challenges & ConsiderationsAs with any novel application there are a number of challenges and additional considerations that will need to be worked on as Pin.sol is developed. Some of these will have to be addressed before a mainnet launch, while others can be addressed later through community proposals. You can find a non-exhaustive list of development challenges and considerations here.ConclusionThis document outlines the vision for Pin.sol, an ETH-based decentralized storage incentivization layer for IPFS. The protocol leverages Ethereum's security, liquidity, and composability to create reliable distributed storage with fair access for all participating nodes. By providing direct economic incentives for IPFS pinning through a balanced queue system, Pin.sol helps bridge the gap between decentralized storage technology and practical applications that require persistence guarantees.ContactIf you have any questions, open an issue or discussio thread, or reach out on Warpcast. ## Publication Information - [ink blots](https://paragraph.com/@jonbray/): Publication homepage - [All Posts](https://paragraph.com/@jonbray/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@jonbray): Subscribe to updates - [Twitter](https://twitter.com/heyjonbray): Follow on Twitter ## Optional - [Collect as NFT](https://paragraph.com/@jonbray/pinsol-v1): Support the author by collecting this post - [View Collectors](https://paragraph.com/@jonbray/pinsol-v1/collectors): See who has collected this post