Patungan: One-Transaction Group Payments on QIE
Resumo
Pixel’s announcement introduces Patungan, a QIE mainnet escrow contract for group bookings. Each member pays for their own seat, and the final seat triggers payment to the seller in the same transaction; if the deadline passes first, members can reclaim their funds. The live contract is verified on Blockscout, with a demo, app, and judge path available.
DEV Community
The last seat pays the seller: one transaction that swaps, seats and pays on QIE mainnet
Every group that books something together has one person who fronts the money. Picture Rina booking the Thursday 5-a-side football pitch: she pays the court, shares the cost in the group chat, then spends a week chasing people. If half the group drops out, she is the one who is short, and getting refunds back depends on her. Patungan is Indonesian for chipping in together. I built a contract that removes the person in the middle. Everyone pays their own seat into an escrow. The last seat pays the seller in that same transaction. If the deadline passes first, every member takes their money back. It is live on QIE mainnet: 0x2f4646D52a5393E239f5b880411dC2CA2D945F31, with source verified on Blockscout. Live app: patungan.edycu.dev · repo: github.com/edycutjong/patungan · no-wallet judge path: /judge The 2:41 demo, recorded against the live mainnet contract: This post is about the one transaction that makes it work, and the two walls I hit getting it onto mainnet. The rule: whoever fills the pool pays the seller The escrow logic is short. Every join ends in _seat. When the seat count reaches the target, the same call flips the pool to Paid and transfers the whole pot to the payee: function _seat(uint256 id, Pool storage p, address member, uint16 n, uint256 amount) private {
if (seatsOf[id][member] == 0) members[id].push(member);
seatsOf[id][member] += n;
paid[id][member] += amount;
p.filled += n;
p.held += uint128(amount);
totalHeld += amount;
if (p.filled == p.seats) {
uint256 total = p.held;
p.status = Status.Paid;
p.held = 0;
totalHeld -= total;
emit PoolPaid(id, p.payee, total);
qusdc.safeTransfer(p.payee, total); // the last seat pays the payee in the same transaction
}
}
There is no "release" button and no keeper. Nobody has to come back later to trigger the payout. The payee can be a .qie name from QIE's Zone naming contract. The contract resolves it when the pool is created and stores the owner address it found. If the name points somewhere other than the address the form showed, creation reverts with PayeeChanged. The hard part: the member holds QIE, the seller wants a stablecoin Pools are priced in QUSDC, so the seller knows exactly what they receive. But a member may hold only the native QIE token. Making them buy QUSDC on a DEX first, then approve, then join, is three steps before they can even take a seat. So joinWithQIE takes native QIE, buys exactly the QUSDC the seats cost (n × price) on QIEDEX, seats the member, and returns the unspent QIE, all in one call. If that member takes the last seat, the payout above runs in the same transaction too. The catch is that the QIEDEX router's source is not verified on the explorer. I am sending users' funds through a contract I cannot read. So the contract does not trust the router's quote. It checks it against the QIE Oracle: function _swapIn(uint256 amount) private returns (uint256 qieSpent, uint256 oraclePrice) {
uint256 fairQie;
(fairQie, oraclePrice) = _fairQie(amount);
uint256 ceiling = fairQie * (10_000 + maxDevBps) / 10_000;
address[] memory path = _path();
uint256 qieIn = router.getAmountsIn(amount, path)[0];
if (qieIn > ceiling) revert PriceDeviation(fairQie, qieIn);
if (msg.value < qieIn) revert ValueTooLow(qieIn, msg.value);
uint256 nativeBefore = address(this).balance;
uint256 qusdcBefore = qusdc.balanceOf(address(this));
router.swapETHForExactTokens{value: Math.min(msg.value, ceiling)}(amount, path, address(this), block.timestamp);
if (qusdc.balanceOf(address(this)) - qusdcBefore < amount) revert SwapShort();
qieSpent = nativeBefore - address(this).balance;
if (qieSpent > ceiling) revert PriceDeviation(fairQie, qieSpent);
}
Each check is there because of a specific way an untrusted router could cheat. Each one has a test with a hostile mock router: Before the swap: if the router's quote is above the Oracle's fair value plus th...

Powered by Readybot.io