Skip to main content

Recording Holding Periods Without a Hard Fork: A Design Review of SecurityToken.sol

· 12 min read
Bankware Global Engineering

In the previous article, we explored whether the history of ownership over a period—not merely the balance at a single moment—could be reflected in income rights and voting power. The idea was to checkpoint only the points at which a balance changed and add up the area between them, producing an address's balance × number of blocks held.

After writing that article, we reopened the code from 2023. The implementation had existed before the formula was ever written down.

SecurityToken.sol in nigo-protocol-v0 is a prototype that stores each address's change history alongside its ERC-20 balance, then uses that history to let holders claim income allocated over a specified block range. This time, the question is much more concrete.

How was proof of holding period represented as smart-contract state, and what did that code leave for the next design?

The source is a historical prototype written in May 2023. This article reviews SecurityToken.sol at commit af4cb29. The repository contains neither tests dedicated to this contract nor a migration, and we found no public deployment address or security-audit materials. It should therefore not be treated as production-verified code. Nor does this article describe a feature currently available in Nigo Protocol. The contract's name, SecurityToken, also says nothing by itself about whether the token is legally classified as a security.

Record the Blocks Where Change Occurs, Not Every Block

It would be inaccurate to summarize this implementation as “storing the balance at every block.” What it actually stores are the points at which the balance changes.

The external mint and transfer paths first update the balance, then record the current block height and the resulting balance for that address. The internal _burn path invokes the same recording function, although the current source does not expose an external function that calls it. In pseudocode, the original state model looks like this:

onBalanceChanged(holder, newBalance):
balances[holder] = newBalance
balanceAt[holder][currentBlock] = newBalance
changedBlocks[holder].append(currentBlock)

If address A has checkpoints (10, 100) and (20, 40), its history can be reconstructed as holding 100 units from block 10 through block 19, and 40 units from block 20 until the next change. There is no need to create a copy at every block. Storage grows with the number of balance changes for an address, rather than with the total number of blocks in the chain.

mint / transfer / internal _burn
|
v
balance update
|
v
(block height, new balance) checkpoint
|
v
intersect with income period
|
v
holder claim

This choice embeds a semantic rule. A checkpoint contains the balance after the transaction has been processed. If an address's balance changes several times in the same block, the system must decide which value represents its holdings for that block. Whether to use only the end-of-block state or distinguish transaction order is not merely a data-structure question; it is part of the rules for calculating rights. A block height is also a ledger position, not wall-clock time. To distribute income accrued by day or by month, a system must define how the chain's variable block interval maps to real time.

Intersecting an Income Period with Holding Segments

When income is deposited, the prototype records four values:

Income
- startBlock
- endBlock
- amountPerBlock
- totalSupply

It divides the deposit by the number of blocks, including both endpoints, to determine the income per block.

amountPerBlock
= floor(depositedAmount / (endBlock - startBlock + 1))

When a holder submits a claim, the contract turns that holder's checkpoints into segments with constant balances. It counts the blocks in which each segment overlaps the income campaign, then adds the following amount:

segmentIncome
= floor(
balanceAtSegment
* amountPerBlock
* overlappingBlockCount
/ campaignTotalSupply
)

Suppose an address holds 100 units beginning at block 10, then holds only 40 beginning at block 20. An income campaign runs from block 15 through block 24, with income of 10 per block and a reference total supply of 100.

15–19 100 * 10 * 5 / 100 = 50
20–24 40 * 10 * 5 / 100 = 20
total 70

The original source uses closed intervals that include both endpoints, as in [startBlock, endBlock]. The previous article used half-open intervals because their boundaries compose more easily. The two notations can describe the same period as follows:

source [S, E] == previous article [S, E + 1)

Neither notation is inherently more correct. What matters is that storage and calculation follow one boundary rule consistently.

Income distribution follows a pull model. Instead of having the payer enumerate every address and send all payments in one transaction, each holder later makes a claim based on their own history. The payout asset is also not fixed to the SecurityToken itself; the prototype can accept a separate ERC-20.

The companion contract, SecurityTokenPublisher.sol, deploys the token after confirmation by designated approvers and assigns the initial supply according to subscription ratios. The core holding-period calculation, however, lives in SecurityToken.sol. The time-weighted voting rights and contribution ledger proposed in the previous article were not features implemented by this code.

How Far Could We Go Without a Hard Fork?

The greatest advantage of this prototype was that it required no special consensus algorithm.

It used ordinary EVM storage, block.number, and ERC-20 calls. A token could checkpoint transfers and calculate time-weighted income without introducing a new opcode or changing the consensus rules of an existing chain. The same state model could be tested through a new smart contract on any EVM-compatible chain.

This is an important design advantage of experimenting at the application layer. A team can build a rights model and test its behavior and demand on a small scale before persuading every chain operator and consensus participant. It is also easier to vary the income period and calculation policy for each product.

But “the existing chain does not need a hard fork” is not the same as “this works immediately with every existing token.” These checkpoints accumulate naturally only in newly issued tokens that include the logic. An already deployed ERC-20 would require an upgrade, migration, or wrapper, and this contract cannot retrospectively create a trustworthy history for the period before integration.

The checkpoints also represent balances in this particular contract's address ledger. They do not automatically prove the beneficial interests divided among customers on a custodian's internal ledger, legal ownership, or economic exposure aggregated across collateral and derivatives. Here, proof does not mean a new cryptographic proof. It means that the quantity-weighted holding time can be reconstructed and audited from checkpoints recorded onchain.

From Expressing an Idea to Protecting Real Money

Turning an idea into state does not complete a settlement protocol. A system handling real money also needs invariants for the lifecycle and costs surrounding the formula.

The total paid for a campaign never exceeds the amount actually deposited.
Claim state always points to the next unprocessed position.
Claims before the end are either prohibited or preserve partial-claim progress.
Multiple balance changes in one block are combined under one explicit rule.
A failed external-token call is never recorded as a successful settlement.
The work processed by one claim has a defined upper bound.

Reading the 2023 source against these criteria reveals several areas that would need to be redesigned before production use.

First, the claim cursor should clearly point to the “next campaign to process,” not the “last campaign observed.” The system can allow each completed campaign to be claimed exactly once or, if partial claims are supported, record how far each campaign has been paid. The amount deposited, cumulative payouts, and remaining balance must be kept in a single accounting model to prevent duplication and omission.

Second, the design needs a rule for repeated block numbers in one address's history. If end-of-block balance is the policy, overwriting that block's checkpoint with the last change is natural. If finer-grained time is relevant to the right being calculated, transaction order or another unit of time must become part of the state model.

Third, an income campaign's denominator should not be an arbitrary input. If minting and burning are possible during a campaign, total supply must also be checkpointed so that the share can be calculated over the same segments as the balance. The policy must also specify who receives the dust left by per-block and per-holder integer division, or whether it rolls into the next campaign.

Fourth, a payout token is an external contract. The protocol must check both the success of a call and the amount actually received, commit its own state before making interactions, and guard against reentrancy. Its authority model must define who may create an income campaign and how an erroneous campaign can be stopped.

Finally, there is cost. The amount of work performed by a claim in the current design is roughly proportional to:

claim work ~= unprocessed income campaigns * holder checkpoints

As a holding history grows and campaigns accumulate, a single claim may exceed the block gas limit. A pull model frees the payer from enumerating every holder, but it does not automatically bound the calculation cost for each holder.

This assessment is not an attempt to judge the 2023 code as though it were a finished product today. The prototype's role was to reduce the idea to its smallest state model, establish feasibility, and expose the problems the next implementation must solve.

If We Were to Make the Same Idea More Robust in a Contract

The previous article compared direct checkpoint iteration, cumulative-area checkpoints, and offchain calculation. The 2023 source used the simplest of those approaches: iterate through each holder's checkpoints at claim time. Much can still be improved using only smart contracts before building a new chain.

If each checkpoint stores not only the balance but also the cumulative balance-blocks through the previous change, a fixed-supply campaign can be calculated as the difference between cumulative values at its two endpoints instead of rescanning the entire interval. Sorted checkpoints can be searched with binary search, and multiple changes in the same block can be consolidated.

If supply changes, separately dividing an address's cumulative holdings by cumulative total supply does not produce a time-weighted ownership share. The balance and total-supply change segments must be merged so that balance / eligibleSupply can be integrated segment by segment, or the design must update a global cumulative reward-per-unit index while rewards accrue.

Claim state should be defined as the next unprocessed position, such as nextClaimIndex, and the number of campaigns processed in one call should be capped. Claims can be restricted to completed campaigns, or the height of a partial claim can be preserved separately. Safe ERC-20 transfers, accounting based on the amount actually received, checks-effects-interactions, deposit/payout/dust invariants, and reproducible tests are also necessary.

An offchain indexer with a Merkle distribution is another option. Whether the complexity sits in the contract, an indexer, or the ledger, it does not disappear. It moves into other responsibilities, such as reproducible computation, dispute resolution, or operating shared infrastructure.

What the Contract-Layer Prototype Revealed

The smart-contract prototype let us test the idea without a hard fork. At the same time, once ownership history is treated as a shared input that every asset may continually use, the repetition at the application layer becomes visible.

DimensionContract-only implementationNative ledger functionality
AdoptionDeployable as a new token on an existing EVMRequires a protocol change or a new network
PolicyCan change quickly for each assetRequires general and conservative rules
StorageEach token repeats the same checkpoint structureMay be optimized as a shared primitive
QueriesCalculation and indexing are designed per contractCan provide standard historical queries and proofs
Failure scopeLargely confined to the contractCan affect consensus and every asset

Every transfer grows the token contract's persistent storage, and as the number of tokens increases, the same history structure is repeated. Each application must also decide which point within a block serves as the reference for rights calculations, whether only finalized blocks count, how long old history is retained, and how historical queries work after pruning.

What, then, might the ledger provide as a native capability? Candidates include historical balance and total-supply checkpoints for each asset, cumulative time-weighted values, queries at finalized heights, and proofs that remain verifiable after history has been pruned. Whether multiple transfers in one block resolve to end-of-block state, and who bears the storage cost of long-term history, would become part of the consensus rules and APIs.

Of course, this feature alone does not inevitably require a new blockchain. A stronger contract, an indexer with a Merkle distribution, a precompile, or a purpose-specific appchain are all alternatives. Native support replaces some application complexity with the larger responsibilities of chain implementation, validation, upgrades, and long-term compatibility.

One Contract Led to the Next Question

It began with a small state model: whenever a token balance changed, record the block height and the new balance together. Once a contract could express a time-weighted calculation, the question moved from feasibility to shared infrastructure. What would change if every token no longer repeated the same history and the ledger provided verifiable historical-balance queries as a common capability?

That line of inquiry led to the idea of building a blockchain that could support application histories such as holding period at the ledger layer. It later became one of several motivations behind the conception of Nigo Protocol. This article does not describe Nigo's current implementation scope or product roadmap; it records the starting point of that design question.

If time itself is an input to ownership rights, what would a ledger that understands it look like?