Settlement
Money moves twice in Sitowise: once in, when a transfer buys a node, and once out, when a node’s owner takes their balance. This page is the mechanism for both, at the level of the calls the contract actually makes.
Why it looks like this
An earlier design kept balances off chain and settled them with EIP-712 vouchers: the server signed a cumulative allowance and you spent it against the contract. That saves gas when gas is expensive. Gas on this chain is around 0.03 gwei, so it saved nothing worth having and cost something that mattered, because it put a server key in the path between a holder and their own money. The vouchers are gone. Balances are held on chain and the owner moves them directly.
A payment becomes a node
Payment happens entirely outside the contract. You send a plain 0.02 ETH transfer to the payments wallet. A watcher sees it and the relayer calls mintFor(to, paymentRef), paying that gas itself. The contract never holds purchase money, so there is no forwarding step that could fail and no treasury for it to sit in.
paymentRef is the payment transaction’s own hash. It is written into paymentRefUsed and emitted in NodeMinted, which is what makes the sale checkable rather than merely asserted: a node points at the exact transfer that paid for it, and that transfer can never point at a second node.
function mintFor(address to, bytes32 paymentRef) external onlyRelayer returns (uint256 id) {
if (paused) revert IsPaused();
if (to == address(0)) revert BadInput();
if (paymentRef == bytes32(0)) revert BadInput();
if (paymentRefUsed[paymentRef]) revert RefAlreadyUsed();
if (_owned[to].length >= maxPerWallet) revert WalletLimit();
paymentRefUsed[paymentRef] = true;
id = ++totalNodes;
// ...
emit NodeMinted(id, to, paymentRef, uint64(block.timestamp));
}Without that mapping the paymentRef in the log would prove nothing: one payment could back unlimited nodes and the explorer trail would be theatre. With it, the check anyone can run is simple. Take the node’s paymentRef, open that transaction, and confirm it paid the price to the payments wallet. Full steps are on Deploying a node.
ETH becomes a balance
Credits are decided off chain and settled on chain in one payable call. The distributor calls creditBatch(ids, amounts) and sends the money with it. If msg.value is not exactly the sum of amounts, the call reverts with ValueMismatch and nothing is recorded.
uint256 sum;
for (uint256 i; i < n; ++i) {
uint256 amt = amounts[i];
if (amt == 0) revert BadInput();
if (amt > type(uint128).max) revert AmountTooLarge();
sum += amt;
}
if (sum != msg.value) revert ValueMismatch();So a balance can never exist without the ETH behind it. There is no state in which the interface shows you a number that the contract cannot pay. The sum is validated before any storage is written, which also means a malformed batch costs the caller memory-only gas instead of a write per node.
The uint128 guard is not decoration either. Balances are uint128, and an explicit narrowing cast in Solidity truncates silently rather than reverting. Left unguarded, an oversized amount would credit a node less than the ETH backing it and break the accounting below permanently.
Each credited node emits Credited(id, amount, newBalance). The balance is withdrawable from that moment; there is no unlock, no vesting and no waiting period.
A balance becomes ETH in your wallet
The node’s owner calls withdraw(id, to), or withdrawAll(to) to sweep every node they hold in one transaction. The caller is checked against node.owner and against nothing else. It always moves the node’s whole balance, because a partial withdrawal would be an amount argument whose only purpose is to be got wrong.
function withdraw(uint256 id, address to) external nonReentrant {
Node storage node = _node[id];
if (node.owner != msg.sender) revert NotNodeOwner();
if (to == address(0)) revert BadInput();
uint256 amount = node.balance;
if (amount == 0) revert NothingToWithdraw();
node.balance = 0;
node.totalWithdrawn += uint128(amount);
outstanding -= amount;
totalWithdrawn += amount;
emit Withdrawn(id, to, amount);
(bool ok,) = payable(to).call{value: amount}("");
if (!ok) revert TransferFailed();
}The balance is zeroed and the totals are updated before the ETH is sent, so a destination that calls back into the contract finds nothing left to take. The nonReentrant guard on top of that is belt and braces rather than the primary defence. Failure modes and gas are on Withdrawing.
Pausing blocks mintFor and nothing else. There is no state of the contract, and no action available to the owner, that stops or delays a withdrawal.
The invariant that holds it together
outstanding is the sum of every node balance. It rises by the exact msg.value of every credit and falls by the exact amount of every withdrawal. Its job is to bound what the contract owner can take out.
| Read | Means |
|---|---|
outstanding() | Everything the contract owes to node balances |
freeBalance() | address(this).balance - outstanding, contract funds attached to no node |
isSolvent() | balance >= outstanding. False would mean balances are not fully backed |
rescue(to, amount) reverts with ExceedsFree for anything above freeBalance(). That is the whole guarantee: under every sequence of calls available to the owner, holder money is unreachable. It is enforced by a fuzz invariant in the test suite, and the invariant is verified by mutation, meaning the bound was deliberately changed to the full balance to confirm the suite fails when the property is broken. A test that cannot fail proves nothing. See Security model and Audits.
What used to be here
If you have read an older version of these docs, or a copy of them somewhere else, the following do not exist and never will in this deployment. None of them are functions on the contract at the published address.
| Old mechanism | What is true now |
|---|---|
| An EIP-712 voucher signed by a server | You call the contract yourself. There is no signer key and no signature. |
| A cumulative allowance and a deadline | A balance, which does not expire and cannot be replayed because it is spent. |
POST /api/withdraw/prepare and /confirm | Deleted. Withdrawing touches no API at all. |
| A treasury contract receiving mint payments | A plain payments wallet, outside the contract, holding no code. |