Measured Behaviour

Does obfuscation break royalty rate tiers?

A tiered royalty looks like a lookup table, and lookup tables have a well-understood failure: a missed key becomes a fallback rather than an error. A royalty schedule has a second half that a lookup table does not - the rule for which units the stepped rate is actually paid on. Both halves are names. We measured what happens when a rename reaches each of them.

A schedule has two halves and only one of them is a table

We built a royalty period the way a real one is shaped: eight titles, a schedule that pays ten per cent to twenty-five thousand units, twelve per cent to a hundred thousand and fifteen per cent above that, tiers measured on lifetime units rather than the period alone, and three titles already sitting at ninety-two thousand lifetime units so that the escalators are genuinely engaged. The label collects seventy per cent of list from its distributor and takes a twenty per cent packaging deduction before the rate applies; fifteen per cent of what is earned is reserved against returns.

The correct run reports gross sales of 2,129,805.00, earns 142,489.67 and pays 121,116.22 after the reserve. The licensor's own rule - no title may be paid above the ceiling its contract allows, and the statement must foot - passes with 121,116.22 payable under a ceiling of 532,451.25.

We then protected the file with default settings on five profiles: the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile. All five produced byte-identical output. That is the first result and it deserves stating plainly - protection on its own did not move a unit or a cent.

The failures start when member renaming is pointed at the names on the deal object or on the sales records, which is what a regexp like ^(applyMarginally)$ or a broad RenameMembers pattern over a configuration literal actually does. If you have not yet decided how far your member pattern reaches, the mechanics are documented in Protect Members.

Marginal or retroactive: one boolean, 10,961.21 dollars

Escalator clauses come in two shapes and both are real contracts. A MARGINAL schedule pays each slice of units at the rate for that slice: the first twenty-five thousand at ten per cent whatever the artist goes on to sell. A RETROACTIVE schedule reprices every unit in the period at the highest tier the artist reached. The deal we measured is marginal. The library defaults to retroactive.

That default is not a bug and it is not laziness. A library shown a rate schedule and no contract cannot choose the stingier reading of it, so it chooses the generous one. Renaming applyMarginally does not produce an error, a warning or a refusal - it hands the calculation to a different, entirely defensible contract.

Measured: earnings went from 142,489.67 to 155,385.22 and the payable figure from 121,116.22 to 132,077.43. The label paid 10,961.21 more than the contract required, on a statement where gross sales were identical to the cent, every title appeared, and the ceiling check passed exactly as before.

There is one visible tell and it is easy to miss. The correct run reports top-rate=n/a, because a marginal calculation has no single rate to report; the broken run reports top-rate=0.15. A statement that suddenly names a headline rate where it used to name none is the whole signal, and it is a field most royalty analysts have never been asked to look at.

Lose the boundaries instead and the money goes the other way

The other half of the schedule is the list of unit counts at which the rate steps. The library ships with no escalators at all - an empty boundary list and a single base rate - because a library cannot invent a deal it has not been shown.

Renaming tierBoundaries takes earnings from 142,489.67 to 119,269.08 and the payable figure to 101,378.72. The artist is short 19,737.50 on one statement. Gross sales are again identical to the cent, every title is on the statement, and the ceiling check passes - it asks whether too much was paid, and too little is not too much.

So the two halves of one clause fail in opposite directions: lose the application rule and the label overpays, lose the boundaries and the artist is underpaid. Neither produces an exception and neither changes a single input figure.

The union is quieter than one of its halves

Testing both names together is the pattern a realistic broad regexp actually produces, and it is where this measurement got interesting. With ^(tierBoundaries|applyMarginally)$ the statement reports earnings of 119,269.08 and a payable figure of 101,378.72 - identical, to the cent, to losing the boundaries alone.

The reason is structural rather than lucky. With no boundaries there is exactly one tier, and inside one tier marginal and retroactive are the same calculation. The application rule has nothing left to apply. So a test that exercises only the broad pattern reports one failure where there are two, and the 10,961.21 overpayment never appears at all.

This is the shape our standing notes call the masked half, and it has now been measured in enough areas to be worth a rule of thumb: test the halves, not just the union. The dangerous cells are not at the wide end of the pattern.

The deductions are where the largest single number moved

The rate is only half of a royalty. The other half is the base it is applied to, and every deduction in this deal reverts to the most generous possible value when its name moves.

Renaming netReceiptsPct - the seventy per cent of list the label actually collects from its distributor - takes the payable figure from 121,116.22 to 173,023.17. The label pays royalties on 100 per cent of a list price it never received: 51,906.95 on one statement, on eight titles, in one accounting period.

Renaming packagingDeductionPct is worth another 30,279.05 in the same direction. Renaming returnsReservePct releases the entire returns reserve. None of the three throws, none of them changes a unit count, and all three leave the ceiling check passing.

The basis is measured over a population, not a number

rateBasis decides whether the tiers are measured over lifetime units or this period's units alone. It is not a rate and not a threshold: it says which population the comparison is taken over. Renaming it takes the payable figure to 103,728.21 - the three titles already deep into the top tier restart at zero and are paid at the base rate for the whole period.

The same 17,388.01 move is produced by renaming the openingUnits field on the sales records instead. That matters because the two names sit on opposite sides of the contract: one is a library option, one is your own data. A pattern that reaches either produces the identical wrong statement, so an audit that checks only your option object has covered half of it.

By contrast, renaming units or listPriceUsd - the numbers themselves - is refused loudly: the engine's finite check rejects the sale before computing anything. That contrast has now held in every area we have measured. Losing a VALUE fails closed and noisily; losing the MEANING of a value fails open and silently.

What we are not claiming

None of this is a defect in the obfuscator. Protection with default settings produced byte-identical behaviour on all five profiles, in this area and in the four others measured alongside it. Every failure above needed member renaming to be pointed at names that cross a boundary into an unprotected library or into data.

Nor is it an argument against renaming members. It is an argument for knowing which names your pattern reaches. A royalty engine's configuration object is a contract written in property names, and the library on the other side of it was compiled without your bundle.

The mitigation is the same one that helps against a mistyped config file or a dropped key over a serialisation hop, and it is worth having regardless of obfuscation: scope MemberRegexp so it cannot reach a third-party options literal, and validate the schedule you loaded before you pay on it. A statement that reports a headline rate where the contract is marginal has told you something, if anybody is reading that line.

Frequently asked questions

Does obfuscation change how a tiered royalty is calculated?

Not on its own. We protected the same royalty engine with default settings on five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - and all five produced byte-identical statements. The failures we measured all required member renaming to reach names that cross into an unprotected library.

What actually broke in your measurement?

Renaming the flag that says whether the schedule is applied marginally or retroactively moved 10,961.21 dollars onto the artist's statement; renaming the tier boundaries moved 19,737.50 the other way. Gross sales, unit counts and the contract-ceiling check were unchanged in both arms.

Why did the wider rename pattern look safer than the narrow one?

Because with no tier boundaries there is exactly one tier, and inside one tier marginal and retroactive are the same calculation. Renaming both names together reproduces the boundary failure exactly and hides the application failure completely. Test the halves, not just the union.

Which single option cost the most?

The net receipts percentage. Renaming it took the payable figure from 121,116.22 to 173,023.17, because the library assumes the label collects 100 per cent of list. That is 51,906.95 paid on money that was never received, on one statement.

Does a statement that reconciles prove the rates were right?

No. The licensor's rule in our measurement asks whether more was paid than the contract ceiling allows and whether the statement foots. Both are true in every arm we ran, including the ones that underpaid by nearly twenty thousand dollars, because a ceiling test is satisfied by any figure below it.

Are the record fields as dangerous as the options?

Yes, and in this area one of each produced the identical wrong answer. Renaming the rateBasis option and renaming the openingUnits field on the sales records both took the payable figure to 103,728.21. An audit that inspects only your options object has covered half the surface.

What should we actually do about it?

Scope MemberRegexp so it cannot reach a third-party options literal, keep your own record field names out of the same pattern, and validate the schedule you loaded before paying on it. See Protect Members for the mechanics, and SOX and GLBA for why a financial calculation deserves the stricter treatment.

Related reading