Measured Behaviour
Published
A dividend is a fixed amount of money. The issuer pays a rate times a share count, once, and nothing in the software that distributes it can create or destroy a cent. Every name in that software decides only which account receives which part of a total that was settled before the first line ran - which is exactly why the check an operations desk actually runs cannot see any of the failures below.
A total that cannot move, and a recipient that can
We built a corporate-actions distribution the way one is really shaped: an issue of 4,200,000 shares paying 62 cents, an ex-day and a record day one day apart, a blotter of 101 trades in the fortnight around them, and five accounts - a pension fund, an index fund, a hedge fund, a retail book and a sovereign wealth fund. The market settles the next business day. Everything not sitting in a traded account is paid to the street name in one lump.
The correct run pays 2,604,000.00 in total, of which 191,657.50 reaches the five named accounts and 5,905.50 is withheld from the non-resident one. The pension fund holds 115,850 shares on the record date because the block it bought two days before settled in time; the hedge fund holds 6,400 because the block it sold settled too.
Then we protected the file with default settings on five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - and ran it. All five produced byte-identical output. That is the first result and it is worth stating plainly: protection on its own did not move a share or a cent.
The failures start when member renaming is pointed at the names on the options object or on the trade records, which is what a regexp like ^(settlementDays)$ or a broad RenameMembers pattern over a config literal actually does.
One day of settlement cycle, and 25,000 shares change hands
The distribution library in this measurement defaults settlementDays to 2. That is not a placeholder and not an error value: it is the cycle the library shipped with, before the market moved to next-day settlement. The caller overrides it to 1.
Rename that one member and the override stops being found. Every date arithmetic in the file is then performed correctly for a market that settles in two days. The blocks struck on the day before the record date no longer count as settled positions, so the buyer is not paid on them and the seller is.
The hedge fund's recorded position went from 6,400 shares to 38,900, and the pension fund's from 115,850 to 90,850. Twenty-five thousand shares of dividend - 15,500.00 at this rate - was paid to the party that had already sold them. The total distributed was 2,604,000.00 in both runs, to the cent.
The registrar's own rule in this fixture checks two things: that the distribution foots against the issue, and that no account is paid on a short position. It passed. It was always going to pass: the amount paid and the amount expected are computed from the same rate and the same share count, so the comparison is closed by construction, and the shares that moved moved between two accounts that both stayed long.
The arm that paid every holder nothing, and still footed
Renaming recordDay reverts it to the library's zero. No trade in the blotter has settled by day zero, so no account holds anything on the record date, and every share in the company is treated as sitting in the street name.
The five named accounts received 0.00 instead of 191,657.50. The withholding fell to zero with them. And the reconciliation still read foots=true, because the whole issue was paid - just to a single residual line rather than to the holders who own it.
The sharper version of the same shape is ^(sharesOutstanding)$. The issue then reads zero shares. The street-name residue is computed as the issue minus what the accounts hold, so it goes to minus 318,650 shares, gross distribution reads 0.00 - and 191,657.50 still leaves the account and reaches the five holders, correctly. The registrar's rule compares 0.00 against 0.00 and reports that the distribution foots.
This is worth separating from the usual reading of a conservation check. The check is not blind here in the sense of looking at the wrong number. Both of its numbers moved to zero together, which is a valid comparison of two quantities that are both wrong, while the payment file it was meant to certify is unchanged and real.
Under-withheld tax, by two independent routes
The sovereign wealth account is non-resident and 15 per cent is withheld from it. Two different renames removed that.
^(withholdingPct)$ is the option, and it reverts to the library's zero - the only default a library that does not know your treaty position can pick, and the one that under-withholds. ^(residency)$ is a field on the trade records, and losing it means no account can be identified as non-resident at all. Both produced WITHHELD=0.00 against 5,905.50, and both raised the amount actually paid out to 197,563.00.
An option key and a record field are different failure modes - an option is replaced by the vendor's value and a record field genuinely goes missing - and here they arrive at the same place. Withholding tax that was not withheld is a liability of the withholding agent, not of the holder who received it, so this is a shortfall the firm discovers from a revenue authority rather than from its own books.
Three arms that were caught, and what caught them
Not everything here was silent, and the pattern in what was loud is the useful part.
^(entitlementBasis)$, ^(dueBillThresholdPct)$ and the record field ^(tradeDay)$ were all refused outright. Each pushed an account into a negative position, and the registrar's rule has a second clause that refuses to pay a short position.
That clause is doing real work. It is defined over something the renamed names do not control - whether a computed position is below zero - rather than over the engine's own arithmetic. The first clause, the one that checks the distribution foots, caught nothing in the entire matrix.
If you take one design point from this page, that is it: in a two-clause control, the clause that compares the engine's output against the engine's own inputs is decoration, and the clause that asserts a property no configuration can make true is the one that earns its place.
What failed closed, and one honest negative
^(qty)$ on the trade records produced NaN through every position and every amount, and the finite check refused the distribution before it paid anybody. The value-fails-closed, meaning-fails-open contrast this site has now measured across two dozen domains held again: losing the number is loud, losing what the number means is silent.
^(account)$ collapsed all five accounts into one bucket holding 318,650 shares, which is the grouping-key failure documented on other pages here; ^(side)$ counted every sale as a purchase.
And one arm did nothing at all. ^(exDay)$ is inert in this measurement, because the dividend is an ordinary one: no due bills attach, so the ex-day never enters the calculation. That is a property of the data, not of the engine. Change the rate to a special dividend and the same rename moves money.
Whether you can see this depends on the fortnight
The same file also runs a quiet distribution: the same issue, the same rate, the same accounts, but with no trading in the window around the record date. On that run every arm above is inert. Nothing is unsettled across the boundary, so the settlement cycle, the entitlement basis and the due-bill threshold have nothing to decide.
Most dividends in a year look like the quiet one. A test suite built from a representative sample of distributions is structurally unable to see this whole class, and would report green while the arms above were live.
What to do about it
None of this is an argument against obfuscating a corporate actions service. Protection with default settings changed nothing measurable on any of the five profiles.
It is an argument about RenameMembers. If you use it, scope MemberRegexp so that it cannot reach the keys of an options object handed to an installed library, or the field names of records that library reads. Those names are part of a contract with code that is not being rebuilt with your bundle. See Protect Members for how the regexp is applied.
Two changes to the code are worth making regardless of obfuscation, because a dropped key over a serialisation hop or a library upgrade produces the same missing name. Assert the settlement cycle you expect rather than relying on the library's default, and write the reconciliation over something a configuration cannot move - a position that cannot be negative, a payment file checked against the custodian's own statement - rather than over the engine's own arithmetic.
The wider point is the one this site keeps arriving at from different directions: a total that always adds up is an invariant of your code, not evidence about your answer. The related pages below reach it from allocation, from effective-dated rate tables and from coordination of benefits, and SOX and GLBA for financial application JavaScript covers the control environment this sits inside.
Frequently asked questions
Does obfuscating a corporate actions system cause wrong dividend payments?
Not on its own. In this measurement, protection with default settings produced byte-identical behaviour on all five profiles tested. The failures appeared only when member renaming was pointed at the option keys or record field names that an installed distribution library reads.
Why did the total stay correct when the recipients were wrong?
Because the total is fixed by the issue. The rate times the share count is computed before any entitlement question is asked, and every share is attached to exactly one account or to the street name. A reconciliation that compares what was paid against the issue is therefore closed by construction and passes in every arm, including the ones that paid the wrong parties.
Which single rename moved the most money?
Renaming the settlement cycle. It reverted from the caller's T+1 to the library's older T+2 default, which moved 25,000 shares of entitlement - 15,500.00 at a 62 cent rate - from the fund that bought the block to the fund that sold it, with no figure on the distribution report changing.
Would a conservation check have caught any of this?
No. The clause that checks the distribution foots against the issue caught nothing in the whole matrix, including an arm where the issue read zero shares and the street-name position went to minus 318,650. The clause that caught three arms was the one asserting no account is paid on a short position, because that property is not derived from the configuration that moved.
Is withholding tax affected?
Yes, by two independent routes. Renaming the withholding percentage reverts it to the library's zero, and renaming the residency field on the trade records means no account can be identified as non-resident. Both produced zero withheld against 5,905.50, which is a liability of the withholding agent rather than of the holder.
How do I use RenameMembers safely on this kind of code?
Scope the regexp to names your own bundle owns on both sides. Any key on an options object passed to an installed library, and any field name on a record that library reads, is part of an external contract and must be excluded. Rebuilding the library with the same mapping is not an option when it is an installed dependency.
Does this apply to the ES5 target as well as the modern target?
Both were measured and both agreed on every arm. The base column - protection with no member renaming - was identical to the unprotected run on the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile.
Related reading