We give a synthetic sale 37 payment units of capacity and accept contributions totaling 31 units. A new payment of seven units satisfies the ten unit transaction maximum but exceeds the remaining sale capacity. The payment must fail without changing any balance or consuming the participant's allowance.
The independent JavaScript ledger models three constraints using integer units. It separates a transaction maximum from a cumulative participant allowance and a global cap. These quantities are invented for the example and represent neither a token price nor funds raised by a project.
Choose the scope of each maximum
A transaction maximum constrains one call. A participant allowance constrains the sum of accepted contributions under one identity. A global cap constrains every accepted contribution across the sale. Naming all three values maximum deposit makes it easy for an interface and a contract to enforce different promises.
Our fixture allows at most ten units per call and twenty units per participant. The global capacity is 37 units. We reject zero and negative quantities before calculating a candidate participant total so the ledger never records a payment that has no positive contribution.
function contribute(who, amount) {
const next = (ledger.byParticipant.get(who) ?? 0n) + amount;
if (amount <= 0n || amount > policy.perCall) throw new Error("call limit");
if (next > policy.perParticipant) throw new Error("participant limit");
if (amount > policy.capacity - ledger.total) throw new Error("capacity");
ledger.byParticipant.set(who, next);
ledger.total += amount;
}All predicates are evaluated before mutation. JavaScript exceptions do not reverse earlier assignments so this ordering is necessary in the fixture. Solidity supplies transaction rollback on an uncaught revert but doing the checks before interactions also reduces the amount of intermediate state an external recipient can observe.
Exact fills need an equality case
With 31 units accepted the final six units fit exactly. A guard using a strict comparison against capacity rejects this legitimate final fill. A guard that checks only whether the existing total exceeds capacity accepts the next payment after the sale is full.
We calculate remaining capacity and reject a payment larger than that quantity. This calculation assumes the existing total already satisfies the cap. Administrative changes therefore need their own invariant because lowering capacity below accepted deposits makes the subtraction invalid under Solidity 0.8 arithmetic.
When the product permits a partial final fill the accounting becomes a different operation. The contract has to split the submitted amount into an accepted quantity and a refund and then explain which amount counts against the participant allowance. Our fixture chooses complete acceptance or complete rejection to keep that promise explicit.
Identity and eligibility remain separate questions
A per address allowance does not establish a per person allowance. An actor can use several addresses unless the eligibility mechanism binds them to some other entitlement. A whitelist entry or signed allocation can establish a particular authorization rule but that rule must be stated without claiming that address counting measures people.
We preserve the participant allowance as its own predicate because a sale can satisfy the global cap while violating an individual entitlement and it can satisfy every individual entitlement while exceeding its aggregate capacity if the two checks are collapsed into one counter that has no unambiguous owner.
For ERC20 payments the recorded contribution may need to use the quantity actually received. A transfer fee can make the requested amount differ from the sale's balance increase. A supported asset policy should define the payment unit and reject token behaviors that invalidate the chosen balance measurement.
31 accepted; 7 rejected; final 6 accepted; capacity 37
10 assertions passedRejections are accounting tests
We snapshot the accepted total and the number of participants before a rejected payment. Afterward both values must match. We then accept an exact fill and verify that a further payment fails. A separate case exceeds a participant allowance while ample global capacity remains.
The test is intentionally small enough to inspect every contribution. In a Solidity suite we add multiple callers and arbitrary payment sequences. After each accepted call the sum of participant contributions must equal the global total and every failed call must preserve the previous accepted state.
For a native currency sale a forced balance change means the contract's raw currency balance can exceed tracked contributions. That surplus should not silently become another participant's contribution or inflate their allocation. Accounting state and asset custody need a reconciliation policy even when ordinary payments use the intended entry point.
Cancellation changes what the capacity counters mean. Decide whether a refund restores participant allowance and global capacity and then test a partial refund between two contributions before exposing that operation in the sale interface.