blockchain · 5 min read
A Zilliqa allowance contract needs a withdrawal clock
Test the missing withdrawal interval in a Zilliqa allowance design with a JavaScript state model covering repeated calls, exact balances and integer limits.
An allowance contract checks that the caller is the beneficiary and that the balance covers the payment. Both checks pass. The beneficiary calls again, and both checks pass again. What made the allowance periodic?
Nothing, unless time is part of the contract's state transition. A method named Withdraw does not imply a schedule. Neither does a comment promising a weekly payment.
This note examines that missing rule in a Scilla-style allowance design for Zilliqa. The downloadable experiment is an executable JavaScript state model, not a compiled Scilla contract. It tests the intended behavior before translating it into a chain-specific implementation. No wallet is connected, no transaction is signed and no funds are involved.
Write the promise as state
Use one beneficiary, a fixed payment amount, a balance, an interval measured in blocks and the next eligible block. In this design, the first withdrawal is allowed at the configured starting block. Each successful withdrawal sets the next eligible block to the current block plus the interval.
That final choice matters. Suppose a beneficiary arrives much later than expected. They can take one payment, then wait another interval. Missed periods do not accumulate. A contract that accrues missed payments needs a different specification and different tests.
The model represents amounts with JavaScript BigInt, not Number. It restricts balances and payment amounts to the unsigned 128-bit range to match the intended Scilla amount domain. Using BigInt alone would not impose that upper limit. Block numbers remain nonnegative integers in the model; no wall-clock conversion is assumed.
The beneficiary address uses a deliberately narrow canonical form: lowercase 0x followed by forty hexadecimal characters. This is local input validation, not a complete Zilliqa address-conversion library. A UI accepting another representation needs to normalize and validate it before this boundary.
The transition has three rejection cases
The withdrawal function checks identity, time and available funds, then returns the new state and a proposed payment:
if (caller !== state.beneficiary) throw new Error('unauthorized');if (block < state.nextBlock) throw new Error('too early');if (state.balance < state.amount) throw new Error('insufficient balance');return { state: {...state, balance: state.balance - state.amount, nextBlock: block + state.interval}, payment: {recipient: state.beneficiary, amount: state.amount}};Two equalities are worth testing rather than leaving to intuition. A request at exactly nextBlock is allowed. A balance exactly equal to the allowance can be spent down to zero. A condition that insists the balance be strictly greater than the payment would strand the final payable amount.
Rejected calls leave the original state unchanged. In this local model, successful calls also return a new object instead of mutating the input. That makes assertions straightforward. It does not simulate transaction rollback, gas charging or message delivery.
The payment object describes an intended effect. It is not evidence that a transfer happened. That distinction becomes important when mapping the rule into an actual smart contract.
Try the second withdrawal
Save allowance.mjs and test_allowance.mjs together. Run node --test test_allowance.mjs; the recorded Node.js 22.22.2 run passed eight tests without external packages.
The central fixture starts with a balance of 20 units, a payment of 10, an interval of 10 blocks and first eligibility at block 100:
| Call | Expected result |
|---|---|
| Beneficiary at block 99 | Reject: too early |
| Beneficiary at block 100 | Pay 10; next eligible block becomes 110 |
| Same beneficiary again at block 100 | Reject: too early |
| Beneficiary at block 109 | Reject: too early |
| Beneficiary at block 110 | Pay remaining 10; balance becomes zero |
Other cases cover unauthorized callers, a late withdrawal, deposits, invalid integers, overflow and a balance above JavaScript's exact Number integer range. A deposit adds funds without resetting the withdrawal window. Otherwise a third party could change the beneficiary's schedule merely by sending money.
Remove the time check and the repeated-call assertions fail. Change the balance check to reject equality and the final-payment test fails. Those are useful tests because they distinguish the stated rule from plausible but incorrect implementations.
Translate the rule into Scilla carefully
Scilla distinguishes immutable contract parameters from mutable fields. The schedule needs a mutable field for the next eligible block. Its documentation shows reading the current block with x <- & BLOCKNUMBER; the implementation must compare that value with the stored eligibility value, then update the field only on the successful path. See Scilla's statements and communication model.
The same reference explains accept, which accepts an incoming amount, and send, which emits messages. A local JavaScript return value does not test either operation. It also does not exercise Scilla's type system, the chain's balance handling or the consequences of a failed recipient call.
Avoid translating the return object into a deployable contract by mechanical substitution. Build the actual transition, run the Scilla checker and interpreter, and replay these fixtures against that implementation. Add cases for the execution and transfer semantics that the local model intentionally omits. Passing the JavaScript tests is evidence for the specification, not a contract audit.
A block interval is also not a duration in seconds. If the product promise is “every week,” define what that means for the target network and its supported time source rather than assuming a fixed number of blocks always represents a week. Keep current network details separate from the contract rules; Zilliqa's developer portal is the starting point for the active environment.
The missing clock is easy to overlook because one withdrawal works. The second call is the one that reveals whether the code implements an allowance or merely an authorized way to drain a balance.
Found a mistake or tried a different approach?
Send Alex a note ↗