<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vaultion]]></title><description><![CDATA[Vaultion]]></description><link>https://vaultion.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aca98bef2d69c040c23477e/ab1edcb7-0b64-4b26-a60e-1668da8123c5.png</url><title>Vaultion</title><link>https://vaultion.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 16:25:32 GMT</lastBuildDate><atom:link href="https://vaultion.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Who gets to stall? Designing timeouts and dispute windows for a P2P escrow contract]]></title><description><![CDATA[Most escrow contract tutorials stop at three functions: deposit, release, refund. That version works perfectly as long as both people behave. The interesting design work is everything that happens whe]]></description><link>https://vaultion.hashnode.dev/who-gets-to-stall-designing-timeouts-and-dispute-windows-for-a-p2p-escrow-contract</link><guid isPermaLink="true">https://vaultion.hashnode.dev/who-gets-to-stall-designing-timeouts-and-dispute-windows-for-a-p2p-escrow-contract</guid><category><![CDATA[Solidity]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Blockchain]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[Vaultion]]></dc:creator><pubDate>Sat, 10 Oct 2026 20:09:57 GMT</pubDate><content:encoded><![CDATA[<p>Most escrow contract tutorials stop at three functions: <code>deposit</code>, <code>release</code>, <code>refund</code>. That version works perfectly as long as both people behave. The interesting design work is everything that happens when one of them doesn't, and almost all of it comes down to <strong>time</strong>.</p>
<p>In a peer-to-peer deal between strangers, every clock in the contract is a lever. Set it wrong and you have built a contract where one side can win simply by going quiet. This post walks through the four timing decisions that matter and the failure each one prevents.</p>
<h2>The naive contract and how it fails</h2>
<pre><code class="language-solidity">function deposit() external payable onlyBuyer { funded = true; }
function release() external onlyBuyer { _pay(seller); }
function refund() external onlySeller { _pay(buyer); }
</code></pre>
<p>Read it from each side's point of view:</p>
<ul>
<li><p><strong>The seller ships and the buyer disappears.</strong> Only the buyer can release. The money is stuck forever.</p>
</li>
<li><p><strong>The buyer pays and the seller never ships.</strong> Only the seller can refund. Same problem in reverse.</p>
</li>
</ul>
<p>Neither party can force an outcome, so the person who is happy with a deadlock (usually the one who already has what they wanted) holds all the leverage. Every fix below is about removing that leverage.</p>
<h2>Decision 1: a review window that ends in the seller's favour</h2>
<p>The buyer needs time to inspect what they received. The seller needs to know the money will not be held hostage. A review window resolves both:</p>
<pre><code class="language-solidity">uint64 public reviewDeadline; // set when the seller marks the deal delivered

function claimAfterReview() external onlySeller {
    require(state == State.Delivered, "not delivered");
    require(block.timestamp &gt;= reviewDeadline, "review window open");
    _pay(seller);
}
</code></pre>
<p>The key choice is <strong>what happens when the window closes with no action</strong>. If silence means "pay the seller", a buyer who received the goods cannot block payment by ignoring the deal. A buyer with a real problem still has the whole window to object.</p>
<p><strong>How long?</strong> Long enough to cover shipping plus a genuine inspection. A digital file needs hours; a graded collectible shipped internationally needs days. Fix it per deal at creation, and never let either party change it afterwards.</p>
<h2>Decision 2: a delivery deadline that ends in the buyer's favour</h2>
<p>The mirror image covers the seller who takes the deal and never delivers:</p>
<pre><code class="language-solidity">uint64 public deliveryDeadline; // set at creation

function refundAfterNoDelivery() external onlyBuyer {
    require(state == State.Funded, "already delivered");
    require(block.timestamp &gt;= deliveryDeadline, "seller still has time");
    _pay(buyer);
}
</code></pre>
<p>Now silence costs each side something. A seller who does nothing loses the sale; a buyer who does nothing after delivery loses the ability to object. That symmetry is the whole point.</p>
<h2>Decision 3: disputes must freeze the clock</h2>
<p>A buyer who receives the wrong item needs a way to stop the review window from running out. Opening a dispute should move the deal into a state where neither timeout path works:</p>
<pre><code class="language-solidity">function openDispute() external onlyParty {
    require(state == State.Funded || state == State.Delivered, "cannot dispute");
    require(state != State.Delivered || block.timestamp &lt; reviewDeadline, "window closed");
    state = State.Disputed; // claimAfterReview and refundAfterNoDelivery now revert
}
</code></pre>
<p>Two details matter here:</p>
<ul>
<li><p><strong>The dispute has to be opened inside the window.</strong> Otherwise a buyer could wait a month and then dispute a deal that was already settled in practice.</p>
</li>
<li><p><strong>A dispute should cost the disputer something, or be ruled quickly.</strong> If opening a dispute is free and resolution is slow, disputes become a stalling tool of their own. Decentralized courts such as Kleros handle this with arbitration fees paid by the party raising the dispute; a reviewed model handles it with a clear ruling deadline.</p>
</li>
</ul>
<h2>Decision 4: what the arbitrator is allowed to do</h2>
<p>The last lever is the one people forget to constrain. Whoever rules on a dispute should be able to pick between exactly two outcomes and nothing else:</p>
<pre><code class="language-solidity">function rule(uint8 ruling) external onlyArbitrator {
    require(state == State.Disputed, "no dispute");
    state = State.Resolved;
    if (ruling == 1) _pay(seller);
    else _pay(buyer);
}
</code></pre>
<p>No <code>sweep()</code>, no <code>setRecipient()</code>, no admin path to move funds anywhere except to one of the two parties. If you want an emergency control, make it a <strong>pause</strong> that can delay a ruling for review, not a key that can redirect money. "Can delay, can never redirect" is a property users can actually check on a block explorer.</p>
<h2>Bonus: settle in a stablecoin</h2>
<p>Every clock above interacts with price. If the escrow holds a volatile asset, a review window becomes an option: a counterparty can stall while they watch the price, and act only when it moves in their favour. Escrowing USDC or USDT removes that incentive entirely, which is why stablecoins are the sensible default for anything that takes more than a few hours.</p>
<h2>Checklist</h2>
<p>When you write (or audit) a P2P escrow contract, check that:</p>
<ol>
<li><p>A silent buyer cannot block payment after delivery (review window ends in the seller's favour).</p>
</li>
<li><p>A silent seller cannot keep the money without delivering (delivery deadline ends in the buyer's favour).</p>
</li>
<li><p>Opening a dispute freezes both timeout paths, and can only happen inside the window.</p>
</li>
<li><p>Disputes have a cost or a deadline, so they are not a free stalling tool.</p>
</li>
<li><p>The arbitrator can only pay the buyer or the seller.</p>
</li>
<li><p>No party can change the deadlines after funding.</p>
</li>
</ol>
<p>If you would rather see these choices made in a deployed product than build them yourself, we run a non-custodial <a href="https://vaultion.org/p2p-crypto-escrow">P2P crypto escrow</a> built around the same ideas, with open-source contracts you can read on the block explorer.</p>
<hr />
<p><em>Disclosure: I work on</em> <a href="https://vaultion.org"><em>Vaultion</em></a><em>. The snippets above are simplified for illustration and are not Vaultion's production contract; a real contract also needs reentrancy protection, a pull-payment ledger and careful fee handling. This post was drafted with AI assistance.</em></p>
]]></content:encoded></item></channel></rss>