<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ankur Ghai</title>
    <description>The latest articles on DEV Community by Ankur Ghai (@ankurghai).</description>
    <link>https://dev.to/ankurghai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3980518%2Fdf17110f-0127-4494-a471-2cc278cbefc7.png</url>
      <title>DEV Community: Ankur Ghai</title>
      <link>https://dev.to/ankurghai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ankurghai"/>
    <language>en</language>
    <item>
      <title>KYC-Gated Dividend Distribution Contracts on Redbelly</title>
      <dc:creator>Ankur Ghai</dc:creator>
      <pubDate>Fri, 12 Jun 2026 05:29:29 +0000</pubDate>
      <link>https://dev.to/ankurghai/kyc-gated-dividend-distribution-contracts-on-redbelly-2co2</link>
      <guid>https://dev.to/ankurghai/kyc-gated-dividend-distribution-contracts-on-redbelly-2co2</guid>
      <description>&lt;h2&gt;
  
  
  KYC-Gated Dividend Distribution Contracts on Redbelly
&lt;/h2&gt;

&lt;p&gt;Tokenized real-world assets such as REITs and bonds must verify that each dividend recipient holds valid KYC &lt;strong&gt;at the moment of payment&lt;/strong&gt;. Snapshot ownership alone is not enough: eligibility must be checked when funds move.&lt;/p&gt;

&lt;p&gt;This article describes the Solidity contracts in the Redbelly KYC dividend template. The design separates two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who is owed a dividend?&lt;/strong&gt; Balance at record date, captured by OpenZeppelin &lt;code&gt;ERC20Snapshot&lt;/code&gt; on &lt;code&gt;RWAToken&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can the issuer pay them?&lt;/strong&gt; KYC status at settlement time, read from Redbelly's on-chain registry via &lt;code&gt;IKYCRegistry.isAllowed(address)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RWAToken.snapshot()
       |
       v
DividendDistributor.createEpoch(totalPool)   -- pulls dividend ERC-20, stores epoch
       |
       +-- distribute(epoch, recipients[])     (OPERATOR_ROLE)
       +-- claim(epoch)                          (holder)
       |
       v
_settle(epoch, account)
       |
       +-- isAllowed(account) == true  --&amp;gt; Paid (direct transfer)
       +-- isAllowed(account) == false --&amp;gt; Escrowed (claimEscrow after KYC)
       |
       v
After reclaim window expires --&amp;gt; reclaim(epoch, treasury) sweeps remainder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three on-chain components:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contract&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RWAToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ERC-20 security token with snapshot, optional cap, pause, optional transfer KYC hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DividendDistributor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Epochs, pro-rata settlement, escrow, reclaim, admin controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IKYCRegistry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External registry; single view function &lt;code&gt;isAllowed(address)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dividend payment token is a separate ERC-20 (for example a stablecoin). Its address is fixed at &lt;code&gt;DividendDistributor&lt;/code&gt; deploy time.&lt;/p&gt;

&lt;h2&gt;
  
  
  IKYCRegistry
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface IKYCRegistry {
    function isAllowed(address account) external view returns (bool);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DividendDistributor&lt;/code&gt; calls &lt;code&gt;isAllowed&lt;/code&gt; inside &lt;code&gt;_settle&lt;/code&gt; and &lt;code&gt;claimEscrow&lt;/code&gt;. The registry address is set in the constructor and can be updated by admin via &lt;code&gt;setKycRegistry(address)&lt;/code&gt; (with a &lt;code&gt;KycRegistryUpdated&lt;/code&gt; event). Operational guidance: rotate the registry only between epochs when possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  RWAToken
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RWAToken&lt;/code&gt; extends OpenZeppelin &lt;code&gt;ERC20&lt;/code&gt;, &lt;code&gt;ERC20Snapshot&lt;/code&gt;, &lt;code&gt;ERC20Burnable&lt;/code&gt;, &lt;code&gt;Pausable&lt;/code&gt;, and &lt;code&gt;AccessControlDefaultAdminRules&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roles
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT_ADMIN_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Admin transfer (two-step with delay), KYC registry updates, transfer-hook toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MINTER_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mint new supply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SNAPSHOT_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Call &lt;code&gt;snapshot()&lt;/code&gt; (granted to &lt;code&gt;DividendDistributor&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PAUSER_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pause()&lt;/code&gt; / &lt;code&gt;unpause()&lt;/code&gt; transfers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The constructor takes an explicit &lt;code&gt;admin&lt;/code&gt; address and &lt;code&gt;initialDelay&lt;/code&gt; for admin transfer safety. The deploy key does not need to remain admin if roles are wired to a multisig after deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supply and transfers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap:&lt;/strong&gt; Optional &lt;code&gt;cap_&lt;/code&gt; (0 = uncapped). &lt;code&gt;mint&lt;/code&gt; reverts with &lt;code&gt;CapExceeded&lt;/code&gt; if cap would be exceeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn:&lt;/strong&gt; Holders can &lt;code&gt;burn&lt;/code&gt; / &lt;code&gt;burnFrom&lt;/code&gt; via &lt;code&gt;ERC20Burnable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause:&lt;/strong&gt; &lt;code&gt;_beforeTokenTransfer&lt;/code&gt; uses &lt;code&gt;whenNotPaused&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional KYC on transfers:&lt;/strong&gt; &lt;code&gt;kycTransfersEnabled&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;. When enabled, both &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; (excluding mint/burn) must pass &lt;code&gt;kycRegistry.isAllowed&lt;/code&gt;. Dividend compliance does not require this hook; it is an issuer policy option.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DividendDistributor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  State
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Immutables:&lt;/strong&gt; &lt;code&gt;token&lt;/code&gt; (&lt;code&gt;RWAToken&lt;/code&gt;), &lt;code&gt;dividendToken&lt;/code&gt; (&lt;code&gt;IERC20&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configurable:&lt;/strong&gt; &lt;code&gt;kycRegistry&lt;/code&gt;, global &lt;code&gt;reclaimWindow&lt;/code&gt; (default 90 days in the template).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per epoch (&lt;code&gt;Epoch&lt;/code&gt; struct):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapshotId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Snapshot taken at epoch creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;totalPool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dividend tokens received (smallest units)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;supplyAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RWA total supply at snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createdAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block timestamp at creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;distributed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Amount paid out (direct + escrow claims)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escrowedTotal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Amount still in escrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reclaimWindow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Window frozen at creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reclaimed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether admin swept remainder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Mappings:&lt;/strong&gt; &lt;code&gt;hasClaimed[epoch][account]&lt;/code&gt;, &lt;code&gt;escrow[epoch][account]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounting:&lt;/strong&gt; &lt;code&gt;accountedDividendBalance&lt;/code&gt; tracks total dividend-token obligations across non-reclaimed epochs. Updated on epoch create, pay, escrow claim, and reclaim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating an epoch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 balanceBefore = dividendToken.balanceOf(address(this));
dividendToken.safeTransferFrom(msg.sender, address(this), totalPool);
uint256 received = dividendToken.balanceOf(address(this)) - balanceBefore;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The epoch stores &lt;code&gt;received&lt;/code&gt;, not the requested &lt;code&gt;totalPool&lt;/code&gt;. Inbound fee-on-transfer tokens therefore cannot undercollateralize the pool.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createEpoch&lt;/code&gt; also calls &lt;code&gt;token.snapshot()&lt;/code&gt; and reverts with &lt;code&gt;ZeroSupply&lt;/code&gt; if supply at snapshot is zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settlement (&lt;code&gt;_settle&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Both &lt;code&gt;distribute&lt;/code&gt; and &lt;code&gt;claim&lt;/code&gt; call the same internal path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _settle(uint256 epoch, address account) internal {
    hasClaimed[epoch][account] = true;

    uint256 amount = _entitlement(epoch, account);
    if (amount == 0) {
        emit Skipped(epoch, account, "zero balance");
        return;
    }

    Epoch storage e = epochs[epoch];

    if (kycRegistry.isAllowed(account)) {
        e.distributed += amount;
        accountedDividendBalance -= amount;
        dividendToken.safeTransfer(account, amount);
        emit Paid(epoch, account, amount);
    } else {
        escrow[epoch][account] = amount;
        e.escrowedTotal += amount;
        emit Escrowed(epoch, account, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Batch behavior:&lt;/strong&gt; &lt;code&gt;distribute&lt;/code&gt; skips addresses already in &lt;code&gt;hasClaimed&lt;/code&gt; and emits &lt;code&gt;Skipped(..., "already claimed")&lt;/code&gt; instead of reverting. This prevents a holder from DoS-ing a large batch by front-running with &lt;code&gt;claim()&lt;/code&gt;. Duplicate addresses in one batch are skipped the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-claim:&lt;/strong&gt; &lt;code&gt;claim&lt;/code&gt; reverts with &lt;code&gt;AlreadyClaimed&lt;/code&gt; on a second attempt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Escrow and reclaim
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;claimEscrow(epoch)&lt;/code&gt;:&lt;/strong&gt; Holder withdraws escrow after becoming KYC-eligible. Requires non-zero escrow, &lt;code&gt;isAllowed(msg.sender)&lt;/code&gt;, and &lt;code&gt;block.timestamp &amp;lt;= createdAt + reclaimWindow&lt;/code&gt;. Moves amount from &lt;code&gt;escrowedTotal&lt;/code&gt; to &lt;code&gt;distributed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reclaim(epoch, to)&lt;/code&gt;:&lt;/strong&gt; Admin-only, after the per-epoch window closes. Sends &lt;code&gt;totalPool - distributed&lt;/code&gt; to treasury (includes unclaimed escrow). Sets &lt;code&gt;reclaimed = true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each epoch captures its own &lt;code&gt;reclaimWindow&lt;/code&gt; at creation. &lt;code&gt;setReclaimWindow&lt;/code&gt; only affects &lt;strong&gt;future&lt;/strong&gt; epochs, so an admin cannot shorten an open epoch's escrow deadline retroactively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dividend math
&lt;/h3&gt;

&lt;p&gt;Entitlement for account &lt;code&gt;a&lt;/code&gt; in epoch &lt;code&gt;e&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amount = balanceOfAt(a, snapshotId) * totalPool / supplyAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Floored by integer division, then capped to the epoch's unallocated remainder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending = totalPool - distributed - escrowedTotal
amount = min(amount, pending)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invariant: &lt;code&gt;distributed + escrowedTotal &amp;lt;= totalPool&lt;/code&gt; on every path. After full settlement of all holders, tests assert &lt;code&gt;distributed + escrowedTotal == totalPool&lt;/code&gt;. Rounding dust may remain until &lt;code&gt;reclaim&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decimal-agnostic dividend token
&lt;/h3&gt;

&lt;p&gt;The contract never calls &lt;code&gt;decimals()&lt;/code&gt;. All pool and payout values are raw ERC-20 smallest units. A 6-decimal stablecoin and an 18-decimal token both work. RWA token decimals and dividend token decimals may differ; pro-rata math uses ratios of raw balances.&lt;/p&gt;

&lt;p&gt;Example: 1,000 USDC (6 decimals) as pool size means &lt;code&gt;totalPool = 1_000_000_000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended:&lt;/strong&gt; standard non-rebasing ERC-20 dividend tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbound fee-on-transfer:&lt;/strong&gt; supported via balance-delta accounting on deposit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; rebasing tokens and outbound fee-on-transfer tokens (outbound fees can desync recorded &lt;code&gt;distributed&lt;/code&gt; from tokens actually delivered).&lt;/p&gt;

&lt;h2&gt;
  
  
  Access control and security
&lt;/h2&gt;

&lt;p&gt;Both contracts use &lt;code&gt;AccessControlDefaultAdminRules&lt;/code&gt; (two-step admin transfer with configurable delay).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Contract&lt;/th&gt;
&lt;th&gt;Capabilities&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPERATOR_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distributor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createEpoch&lt;/code&gt;, &lt;code&gt;distribute&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PAUSER_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pause&lt;/code&gt; / &lt;code&gt;unpause&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT_ADMIN_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distributor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reclaim&lt;/code&gt;, &lt;code&gt;setReclaimWindow&lt;/code&gt;, &lt;code&gt;setKycRegistry&lt;/code&gt;, &lt;code&gt;rescueToken&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pause symmetry:&lt;/strong&gt; &lt;code&gt;whenNotPaused&lt;/code&gt; applies to &lt;code&gt;distribute&lt;/code&gt;, &lt;code&gt;claim&lt;/code&gt;, &lt;code&gt;claimEscrow&lt;/code&gt;, and &lt;code&gt;reclaim&lt;/code&gt;. Without this, a pauser could block escrow claims until the window expired and then sweep via &lt;code&gt;reclaim&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ReentrancyGuard:&lt;/strong&gt; on all external mutators that move tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;rescueToken(token, amount, to)&lt;/code&gt;:&lt;/strong&gt; Admin can sweep stray ERC-20 balances. For the dividend token, only amounts above &lt;code&gt;accountedDividendBalance&lt;/code&gt; are rescuable; committed epoch funds cannot be drained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom errors&lt;/strong&gt; include &lt;code&gt;AlreadyClaimed&lt;/code&gt;, &lt;code&gt;NotKycAllowed&lt;/code&gt;, &lt;code&gt;EscrowClaimWindowExpired&lt;/code&gt;, &lt;code&gt;ReclaimWindowActive&lt;/code&gt;, &lt;code&gt;InsufficientRescuableBalance&lt;/code&gt;, and others for clear revert reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gas benchmarks
&lt;/h2&gt;

&lt;p&gt;Per-recipient gas during &lt;code&gt;distribute&lt;/code&gt; (verified holders, Solidity 0.8.24, optimizer 200 runs):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Holders&lt;/th&gt;
&lt;th&gt;distribute gas&lt;/th&gt;
&lt;th&gt;gas / recipient&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;1,326,382&lt;/td&gt;
&lt;td&gt;66,319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;3,207,027&lt;/td&gt;
&lt;td&gt;64,141&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;6,342,100&lt;/td&gt;
&lt;td&gt;63,421&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;31,680,464&lt;/td&gt;
&lt;td&gt;63,361&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cost is dominated by &lt;code&gt;balanceOfAt&lt;/code&gt; on the snapshot, &lt;code&gt;isAllowed&lt;/code&gt; on the registry, a &lt;code&gt;hasClaimed&lt;/code&gt; storage write, and the ERC-20 transfer. Reentrancy guards and pause checks are included in these figures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The Hardhat suite includes 34 unit tests with approximately 95% line coverage. Cases cover pro-rata payout, escrow lifecycle, batch skip behavior, front-run resistance, per-epoch reclaim windows, pause interactions, &lt;code&gt;rescueToken&lt;/code&gt; accounting, registry updates, and explicit admin wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;MIT-licensed template: contracts, tests, and deployment scripts for Redbelly Network (testnet chain ID 153, mainnet 151).&lt;/p&gt;

&lt;p&gt;Stack: Solidity 0.8.24, OpenZeppelin 4.9.6, Hardhat, TypeScript.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/ankurghai/kyc-dividend-token" rel="noopener noreferrer"&gt;https://github.com/ankurghai/kyc-dividend-token&lt;/a&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>react</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
