Measured Behaviour
Published
A stake limit engine has obvious failures everybody tests for: a cap that will not save, a bet that will not settle, a limit that never fires. It has one almost nobody tests for, because the limit and the window are both unchanged - the point the window is measured FROM. We measured what a rename does to it.
One phrase, two different limits
"GBP150 per 24 hours" is the phrasing every operator uses and it names two different controls. Read as a rolling window it means no twenty-four hours may cost you more than GBP150. Read as a calendar window it means each named day gets its own GBP150.
The screen the player sees is identical either way. The difference only appears for the player who stops near midnight and starts again shortly after it, and that is not a rare player - it is the ordinary shape of a late session.
We built exactly that. Ten bets from one player: three losing GBP50 each at hours 18, 20 and 22, one slots bet over its GBP2 product cap at hour 23, then four more across the boundary at hours 25, 27, 29 and 31, a bonus-funded bet at hour 33 and a final bet at hour 46. Per-product stake caps of GBP2 on slots, GBP5 on roulette and GBP50 on sports, a GBP150 loss limit, a rolling 24-hour window, and bonus-funded play counting towards the limit.
The correct run accepts four bets. The player reaches GBP150 by hour 22, the oversized slots bet is refused on its cap, and every bet in the following twenty-four hours is refused on the loss limit until hour 46 clears the window. Total lost: GBP200.00, of which the worst rolling twenty-four hours cost GBP150.00 - exactly the limit.
Protected on five profiles with default settings, behaviour was identical every time, and the base column was measured twice and diffed identical.
An anchor is not a length
The library's default window is calendar, and that is the reasonable default rather than a bug. A calendar window is a counter and a reset. A rolling window needs the history kept and re-summed on every bet. A library that has to work for callers who configured nothing picks the one that always terminates.
So renaming the option key carrying the anchor does not corrupt a value. The string rolling is still in the bundle. The library reads its own default and gets calendar, which is a perfectly good window type that a great many systems legitimately use.
Note what does NOT move. The loss limit is GBP150 in both runs. The window length is 24 hours in both runs. The stake caps are unchanged and the oversized slots bet is refused in both. Every bet is recorded at the penny, every loss really occurred, and every sum is exact. The only thing that moved is where the twenty-four hours is measured from.
What renaming the anchor actually did
Accepted bets went from four to six. Bets refused on the loss limit went from five to three. The money lost went from GBP200.00 to GBP300.00.
The counter that tells the story is the worst rolling twenty-four hours the player actually sustained, computed from the bets each run accepted. In the correct run it is GBP150.00, which is the limit exactly - a rolling limit cannot be exceeded by a run that enforces it. In the defective run it is GBP300.00: two hundred per cent of a limit that was still printed as GBP150 on the configuration line and still enforced on every single bet.
The player lost GBP300 in eleven hours, in six bets, on a GBP150 daily limit, and at no point did the system take a bet it considered over the limit. The operator's own check passed in both runs, and its wording is worth quoting because it is precisely true: none past the limit in its window. The window is the thing that moved.
Both renaming presets produced byte-identical output.
The narrower loss is not the milder one
We measured the anchor together with the window length, and the result matters for how anyone reads a matrix like this.
Losing both takes the window to a calendar hour. Accepted bets go from four to nine, only the stake-cap refusal survives, the money lost goes to GBP420.00 and the worst rolling twenty-four hours reaches GBP370.00 - 247% of the limit.
So the wider pattern is worse here, which is the ordinary case. The point is the opposite one: the NARROW pattern, the one that moves a single option key nobody would flag in review, still doubled the player's worst day while leaving every number a reviewer would look at unchanged. A finding that moves only rolling to calendar reads as cosmetic in a diff. It is the whole control.
Why a test suite would not have caught it
We re-ran the same ten bets moved inside a single calendar day, one an hour from hour 2. Every stake, every loss, every product and every funding source identical; only the clock times changed.
In that fixture the arm is inert on every counter. A rolling window and a calendar window cover the same bets when no boundary is crossed, so both readings accept the same three bets, refuse the same seven, and report the same GBP150.00 worst day; only the reported window type moves.
That is the fixture most people build, and it is built that way for a sensible reason: test data for a daily limit is written as a day's play, and a day's play does not cross midnight. The single property that makes this defect visible - a session spanning the boundary - is the one property nobody puts in test data on purpose.
What to do about it
Assert the window type, not just its length. Have the library report the anchor it actually used and compare it against your licence conditions at start-up. This is a one-line check and it is worth having regardless of obfuscation, because a library upgrade that changes a default produces the identical outcome.
Compute and report the worst rolling window independently of the control that enforces it. The counter that separated our two runs is not a dashboard number and it is trivial to produce: for each accepted bet, sum the losses in the preceding twenty-four hours and keep the maximum. A control that enforces a rolling limit can never make that figure exceed the limit, so any value above it is a finding by construction.
Put at least one boundary-crossing session in the fixture. Every window control in this article behaves identically on data that sits inside one block, which means data inside one block tests the arithmetic and not the window.
On the build side the fix is scoping. Member renaming is opt-in and takes a regular expression, so a pattern that excludes the option keys you pass to an installed library keeps everything else renamed - the mechanics are on the RenameMembers documentation. For a client-side control shipped inside a mobile gambling app, the resilience expectations are the ones our OWASP MASVS guidance works through.
Frequently asked questions
Does obfuscation break stake and loss limits by default?
No. We protected the same limit engine on five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - and every one produced behaviour identical to the unprotected run, measured twice and diffed identical. The failure comes from member renaming, which is opt-in and takes a pattern you choose.
What changed when the window anchor was renamed?
The library read its own default and used a calendar window instead of a rolling one. The loss limit stayed at GBP150, the window stayed at 24 hours, the stake caps stayed in force and every bet was recorded at the penny. Only the point the twenty-four hours is measured from moved.
How much did the player lose?
GBP300.00 against GBP200.00 in the correct run, across eleven hours. The worst rolling twenty-four hours went from GBP150.00 - exactly the limit - to GBP300.00, or 200% of it. Accepted bets went from four to six and loss-limit refusals from five to three.
Did the system report a breach?
No. Its own check passed in both runs, and the wording is precisely true: nobody passed the limit in its window. The window is what moved. The configuration line still printed a GBP150 loss limit and a 24-hour window in both runs.
Is losing the window length as well worse or better?
Worse. Losing the anchor and the length together takes the window to a calendar hour, accepted bets to nine, money lost to GBP420.00 and the worst rolling twenty-four hours to GBP370.00, or 247% of the limit. The narrow single-key loss is the one that hides, not the one that is mild.
Why did our test data not show this?
We re-ran the same ten bets moved inside a single calendar day, one an hour. The arm is inert on every counter there, because a rolling and a calendar window cover the same bets when no boundary is crossed. Test data for a daily limit is normally written as a day's play, and a day's play does not cross midnight.
How do we keep the protection and avoid the defect?
Assert the window type as well as its length at start-up, compute the worst rolling window independently of the control that enforces it, and put a boundary-crossing session in the fixture. On the build side, scope RenameMembers so option keys handed to installed libraries are excluded - see the RenameMembers documentation.
Related reading