ESH

ESH

ERC-20 Token built special for distribute dividends

  • Publish Date

    Nov 16, 2025

  • Licenses

    MIT, Apache-2.0, Copyright (c) 2024 ultimatedeal.net Osher Haim Glick.

What ESH Is

ESH is an ERC-20–compatible token (via your custom ERCUltra) with three big extras:

  1. 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.

  2. 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”.

  3. 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.


Inheritance & External Interfaces

  • 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.

Token Bootstrapping

  • Constructor mints:

    • 1e27 ESH to _contractOwner
    • 3% (3e25 ESH) to a fixed address 0xfb31…f67
  • Both addresses are added to the holders set (_addHolder), ensuring the owners array starts populated.


Roles

  • 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.

Key Storage

  • 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, isCompleted
    • distributionInitialized & distributionInitializationStarted
    • Snapshotted mirrors for UI: distributionHolders, distributionBalances, distributionTotalBalance
  • Governance

    • _delegates[account]
    • _checkpoints[delegate][index] => { fromBlock, votes }
    • _numCheckpoints[delegate]

Core Flows

1) Creating a distribution

Goal: Distribute a fixed amount of paymentToken across current ESH holders proportionally to their ESH balances.

Steps

  1. createDistribution(paymentToken, amount) → returns id

    • Allowed: Distributors only.
    • Records payment token, amount, and detects decimals via IERC20Metadata.
    • Initializes state; does not build the recipient snapshot here.
  2. initializeDistribution(id)

    • Allowed: Distributors only.

    • Snapshot pass over owners (from ERCUltra) in INIT_BATCH_SIZE chunks:

      • Push each holder and their current ESH balance into recipients[] and balances[].
      • Accumulate 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/distributeMulticall before manual initialization, they auto-initialize once.

  1. 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.

  2. 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)
        • Tracks the share in holderTokenBalances[recipient][symbol]
        • Performs a transferFrom(msg.sender, recipient, adjustedShare)
      • Advances startIndex; emits DistributionExecuted.

      • When finished, marks isCompleted = true and emits DistributionCompleted.

    • distributeMulticall(id, maxCalls)

      • Convenience loop that runs up to maxCalls inner batches of size MAX_BATCH_SIZE.
      • Stops early if all recipients processed; same events as above.

Rounding behavior

  • Per-recipient shares use integer division, so tiny dust may remain undistributed. There’s no explicit handling of leftovers.

Approvals Weirdness The function calls:

paymentToken.approve(recipient, adjustedShare); paymentToken.approve(msg.sender, adjustedShare); paymentToken.approve(address(this), adjustedShare);

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.


2) Governance (vote delegation)

  • delegate(delegatee) → sets _delegates[msg.sender] and moves voting power matching current balance.
  • getVotes(account) → latest votes.
  • getPastVotes(account, blockNumber) → binary-searches checkpoints to return votes at a past block (must be < current block).

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).

Public Views & Utilities

  • 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.

Admin Functions

  • 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).

Events

  • 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)

Typical Usage (End-to-End)

  1. Setup (owner)
  • Deploy; owner receives supply, 3% minted to the fixed address.
  • Optionally setSellerStoreContract and/or setRentingStoreContract.
  1. Run a distribution (distributor)
  • Decide paymentToken (e.g., USDC) and amount.
  • Call createDistribution(paymentToken, amount)id.
  • (Optional) Call initializeDistribution(id) now; otherwise it will auto-run.
  • Approve the ESH contract to spend amount of paymentToken from the distributor’s address.
  • Call distributeMulticall(id, maxCalls) until it returns true (or loop distributeBatch).
  • UI can show per-holder received totals via getAllHolderTokenBalances(holder).
  1. Governance
  • Token holders call delegate(...).
  • Off-chain governance tools can read getVotes(address) and getPastVotes(address, blockNumber).

Design Notes, Trade-offs & Risks

  • 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:

    • The distributor to hold funds, and
    • The distributor to 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:

    • Restrict accepted paymentToken set, or
    • Add nonReentrant 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.)


Quick Reference (Function by Function)

Admin / Config

  • changeOwner(address): owner → owner
  • adjustBATCHSize(uint256): owner
  • adjustInitBATCHSize(uint256): owner
  • setSellerStoreContract(address): owner
  • setRentingStoreContract(address): owner

Metadata / Fees (thirdweb)

  • _canSetContractURI() → owner only
  • _canSetPlatformFeeInfo() → owner only

Holders

  • 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) → uint256
  • getAllHolderTokenBalances(holder)TokenBalance[]

Governance

  • delegate(delegatee)
  • delegates(account) → address
  • getVotes(account) → uint256
  • getPastVotes(account, blockNumber) → uint256

Supply

  • burn(amount)
  • burnFrom(account, amount) (caller must be account)

Suggested Improvements (Optional)

  • Remove internal approve() calls inside distributions.
  • nonReentrant on distributeBatch / distributeMulticall.
  • Address-keyed ledger for received balances; store both token and symbol in TokenBalance.
  • Remainder handling (send leftover to owner/treasury or the largest holder, or keep a carry-over).
  • Event for each credit (optional, may be noisy); or emit per-batch totals per token.
  • Snapshot at block (advanced): if you want immutable “balance at a block” semantics, integrate a snapshot mechanism.

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.

INIT_BATCH_SIZE

view

No inputs required