
ERC-20 Token built special for distribute dividends
Nov 16, 2025
MIT, Apache-2.0, Copyright (c) 2024 ultimatedeal.net Osher Haim Glick.
ESH is an ERC-20–compatible token (via your custom ERCUltra) with three big extras:
On-chain profit/dividend distribution engine Pays out any ERC-20 (“payment token”) to all current ESH holders pro-rata to their ESH balance, in gas-bounded batches.
Per-holder external balance ledger Internally tracks how much of each payment token was attributed to every holder (by symbol), so UIs can show “lifetime distributions received”.
Governance-style vote delegation with checkpoints Classic “delegate your votes” flow (à la ERC20Votes): live vote balances, historical vote queries, checkpoints, and automatic vote moves on mint/burn/transfer.
It also adopts Thirdweb’s ContractMetadata and PlatformFee patterns, adds ReentrancyGuard, and exposes owner/distributor role controls.
ERCUltra (custom): provides ERC-20 core plus holder indexing (owners array) and _addHolder. ESH relies on this for iterating holders.ReentrancyGuard: protects selected state-changing functions (presently used on changeOwner).ContractMetadata (thirdweb): owner-gated contractURI.PlatformFee (thirdweb): owner-gated platform fee info.IERC20 / IERC20Metadata: for paying distributions in arbitrary ERC-20 tokens and reading decimals/symbol.Constructor mints:
_contractOwner0xfb31…f67Both addresses are added to the holders set (_addHolder), ensuring the owners array starts populated.
Owner (contractOwner)
Can change owner, set platform fee & metadata, and tune batch sizes.
Can set two distributor addresses:
distributor1 (e.g., sales store)distributor2 (e.g., rentals store)Distributors (OnlyDistributor)
msg.sender must be owner, distributor1, or distributor2 to run any distribution function.Batch tuning
MAX_BATCH_SIZE (default 500): max recipients processed per inner batch.INIT_BATCH_SIZE (default 400): chunk size used when building the snapshot arrays.Per-holder “received balances” ledger
holderTokenBalances[holder][symbol] => { balance, symbol }holderTokenSymbols[holder] => string[] list of payment token symbols that holder has ever received.Distribution objects (per bytes32 id)
Distribution: recipients[], balances[], paymentToken, amount, decimals, startIndex, totalBalance, isCompleteddistributionInitialized & distributionInitializationStarteddistributionHolders, distributionBalances, distributionTotalBalanceGovernance
_delegates[account]_checkpoints[delegate][index] => { fromBlock, votes }_numCheckpoints[delegate]Goal: Distribute a fixed amount of paymentToken across current ESH holders proportionally to their ESH balances.
Steps
createDistribution(paymentToken, amount) → returns id
IERC20Metadata.initializeDistribution(id)
Allowed: Distributors only.
Snapshot pass over owners (from ERCUltra) in INIT_BATCH_SIZE chunks:
recipients[] and balances[].totalBalance.Copies snapshot arrays to public mirrors for easy off-chain reads (distributionHolders, distributionBalances, distributionTotalBalance).
This is not a historical “block snapshot”; it’s “balance at initialization time”.
Note: If you call
distributeBatch/distributeMulticallbefore manual initialization, they auto-initialize once.
Funding requirement (off-chain step)
The distributor (msg.sender) must hold amount of the payment token and must approve allowance to this ESH contract, because ESH executes:
paymentToken.transferFrom(msg.sender, recipient, adjustedShare)
Without approval, transfers will revert.
Distributing
distributeBatch(id, batchSize)
Processes recipients from startIndex up to endIndex = min(startIndex + batchSize, recipients.length).
For each recipient:
adjustedShare = amount * balances[i] / totalBalance (integer division; truncates remainder)holderTokenBalances[recipient][symbol]transferFrom(msg.sender, recipient, adjustedShare)Advances startIndex; emits DistributionExecuted.
When finished, marks isCompleted = true and emits DistributionCompleted.
distributeMulticall(id, maxCalls)
maxCalls inner batches of size MAX_BATCH_SIZE.Rounding behavior
Approvals Weirdness The function calls:
inside the contract before each transferFrom. These approvals set the contract’s allowance to others, which is unusual and generally unnecessary for the contract to pull from msg.sender. The actual transfer uses transferFrom(msg.sender, recipient, adjustedShare), which depends on msg.sender’s approval to this contract, not vice-versa. Consider removing these approve calls—they don’t help the transfer and may add gas + confusion.
_delegates[msg.sender] and moves voting power matching current balance.Votes track token supply changes automatically:
_afterTokenTransfer: moves votes from from’s delegate to to’s delegate._mint: mints votes to delegate(account)._burn: removes votes from delegate(account).getHolders() → returns owners (from ERCUltra).
getHolderTokenBalance(holder, symbol) → lifetime sum credited in that payment token symbol.
getAllHolderTokenBalances(holder) → array of { balance, symbol } for all symbols seen.
delegates(account) → current delegate.
Platform/Metadata guards
_canSetContractURI() and _canSetPlatformFeeInfo() are owner-only.changeOwner(newOwner) (nonReentrant)
adjustBATCHSize(uint256) → sets MAX_BATCH_SIZE
adjustInitBATCHSize(uint256) → sets INIT_BATCH_SIZE
setSellerStoreContract(address) → sets distributor1
setRentingStoreContract(address) → sets distributor2
burn(amount) and burnFrom(account, amount)
burnFrom is restricted: only the account itself can call it (not a spender-allowance pattern).BalanceDistributed(uint256 totalBalance) (declared but not emitted in current code)DistributionCreated(bytes32 id, address creator)DistributionExecuted(bytes32 id, uint256 startIndex, uint256 endIndex)DistributionCompleted(bytes32 id)DelegateChanged(delegator, fromDelegate, toDelegate)DelegateVotesChanged(delegate, previousBalance, newBalance)setSellerStoreContract and/or setRentingStoreContract.paymentToken (e.g., USDC) and amount.createDistribution(paymentToken, amount) → id.initializeDistribution(id) now; otherwise it will auto-run.amount of paymentToken from the distributor’s address.distributeMulticall(id, maxCalls) until it returns true (or loop distributeBatch).getAllHolderTokenBalances(holder).delegate(...).getVotes(address) and getPastVotes(address, blockNumber).Snapshot timing Balances are captured at initialization time, not at a historical block. If holders trade ESH after initialization but before distribution completes, payouts still reflect the snapshot, by design.
Rounding dust Integer division may leave small undistributed remainders. If precise full distribution is required, add a “sweep remainder” step.
Allowance model (important)
Transfers use transferFrom(msg.sender, recipient, amount). This requires:
approve(ESH, amount).
The internal approve() calls made by the ESH contract do not grant the allowance needed. Consider removing them to save gas and avoid confusion.Reentrancy surface
Distribution functions perform external calls (transferFrom) in loops and are not marked nonReentrant. With standard ERC-20s (like USDC) this is safe, but if a malicious token were used, reentrancy could be attempted. Mitigations:
paymentToken set, ornonReentrant to distributeBatch and distributeMulticall.Gas & batching
INIT_BATCH_SIZE and MAX_BATCH_SIZE are tunable. Very large holder sets are handled over multiple transactions. UIs should show progress using startIndex.
Holders index source
Iteration relies on owners from ERCUltra. Ensure _addHolder is called on first receipt (as you already do in _mint and likely in _afterTokenTransfer inside ERCUltra).
Symbol-keyed ledger
The “received balance” ledger is keyed by token symbol (string); two different tokens could share the same symbol on different chains or wrappers. Using the token address as the ledger key would be more robust. (You can still display symbol in the struct for UI.)
Admin / Config
changeOwner(address): owner → owneradjustBATCHSize(uint256): owneradjustInitBATCHSize(uint256): ownersetSellerStoreContract(address): ownersetRentingStoreContract(address): ownerMetadata / Fees (thirdweb)
_canSetContractURI() → owner only_canSetPlatformFeeInfo() → owner onlyHolders
getHolders() → address[] owners (from ERCUltra)Distributions
createDistribution(paymentToken, amount) → bytes32 id (distributor)initializeDistribution(id) (distributor)distributeBatch(id, batchSize) → bool completed (distributor)distributeMulticall(id, maxCalls) → bool completed (distributor)Per-holder received ledger
getHolderTokenBalance(holder, symbol) → uint256getAllHolderTokenBalances(holder) → TokenBalance[]Governance
delegate(delegatee)delegates(account) → addressgetVotes(account) → uint256getPastVotes(account, blockNumber) → uint256Supply
burn(amount)burnFrom(account, amount) (caller must be account)approve() calls inside distributions.distributeBatch / distributeMulticall.token and symbol in TokenBalance.If you want, I can turn this into a Markdown README.md with a “How to integrate in a dApp” section and sample scripts (approve + distribute loops) tailored for your current toolchain.
No inputs required