Skip to content

Instantly share code, notes, and snippets.

@pkoch
Last active March 3, 2026 20:12
Show Gist options
  • Select an option

  • Save pkoch/a690b142b78e588cd188483aaac3742d to your computer and use it in GitHub Desktop.

Select an option

Save pkoch/a690b142b78e588cd188483aaac3742d to your computer and use it in GitHub Desktop.
diff --git a/src/CCADisbursementTracker.sol b/src/CCADisbursementTracker.sol
index 84c97ba..3522370 100644
--- a/src/CCADisbursementTracker.sol
+++ b/src/CCADisbursementTracker.sol
@@ -7,7 +7,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// @dev As per CCA's own tests, the CCA can leave up to 1e18 wei of dust in the
// contract after sweep+claim.
-uint256 constant MAX_ALLOWABLE_DUST_WEI = 1e18;
+uint256 constant MAX_ALLOWABLE_DUST_WEI = 1;
/**
* @title CCADisbursementTracker
@@ -44,9 +44,9 @@ uint256 constant MAX_ALLOWABLE_DUST_WEI = 1e18;
* Note well: `claimTokensBatch` makes things harder to track 1:1 with this contract, since that can make a single
* transfer that contains bids from different phases of the sale.
*
- * The disburser should call the `recordDisbursement` function to record each disbursement, providing the txHash where
- * the actual disbursement took place and the amount disbursed, so that the original token distribution can be verified.
- * A disburser might choose to split disbursements in tranches, either because the allocation was done in different
+ * The disburser should call the `recordDisbursements` function to record disbursements, providing the txHash where the
+ * actual disbursement took place and the amount disbursed, so that the original token distribution can be verified. A
+ * disburser might choose to split disbursements in tranches, either because the allocation was done in different
* phases of the sales, or for any other reason. The uniqueness and non-zero-ness of txHash isn't enforced
* by design to allow for flexibility in the disburser's strategy.
*/
@@ -149,7 +149,7 @@ contract CCADisbursementTracker is ERC20 {
}
/// @notice Returns true when the sale is fully disbursed (total supply is zero and all missing disbursements recorded).
- /// @dev Expects sweepUnsoldTokens to have been called, all bid tokens claimed, and all disbursements recorded via recordDisbursement.
+ /// @dev Expects sweepUnsoldTokens to have been called, all bid tokens claimed, and all disbursements recorded via recordDisbursements.
function saleFullyDisbursed() public view returns (bool) {
return saleFullyClaimed() && _totalMissingDisbursements == 0;
}
@@ -174,6 +174,7 @@ contract CCADisbursementTracker is ERC20 {
error OnlyDisburserCanRecordDisbursements();
error OverdisbursementDetected();
error SaleNotFullyClaimed();
+ error ArrayLengthMismatch();
event MissingDisbursementRecorded(address indexed to, uint256 value);
event DisbursementCompleted(address indexed to, uint256 value, bytes32 txHash);
@@ -241,26 +242,39 @@ contract CCADisbursementTracker is ERC20 {
return result;
}
- /// @notice Records a single disbursement made off-chain, reducing the missing disbursement balance.
+ /// @notice Records disbursements made off-chain, reducing the missing disbursement balances.
/// @dev Only callable by the disburser after the sale is fully claimed. Reverts on overdisbursement or zero amounts.
- /// @param to Address the disbursement was made to
- /// @param value Amount disbursed
- /// @param txHash Transaction hash where the on-chain disbursement occurred
- function recordDisbursement(address to, uint256 value, bytes32 txHash) external {
- if (msg.sender != _DISBURSER) revert OnlyDisburserCanRecordDisbursements();
+ /// @param recipients Addresses to record disbursements for
+ /// @param values Amounts disbursed to each recipient
+ /// @param txHashes Transaction hashes where the on-chain disbursements occurred
+ function recordDisbursements(address[] calldata recipients, uint256[] calldata values, bytes32[] calldata txHashes)
+ external
+ {
if (!saleFullyClaimed()) revert SaleNotFullyClaimed();
- if (to == address(0)) revert NoZeroAddressRecipientAllowed();
- if (value == 0) revert NoZeroDisbursementsAllowed();
- if (_missingDisbursements[to] < value) revert OverdisbursementDetected();
- // txHash is intentionally not checked for 0x0 or uniqueness to allow for flexibility.
+ if (msg.sender != _DISBURSER) revert OnlyDisburserCanRecordDisbursements();
+ uint256 len = recipients.length;
+ if (len != values.length || len != txHashes.length) revert ArrayLengthMismatch();
- unchecked {
- _missingDisbursements[to] -= value;
+ for (uint256 i; i < len;) {
+ address to = recipients[i];
+ uint256 value = values[i];
+ if (to == address(0)) revert NoZeroAddressRecipientAllowed();
+ if (value == 0) revert NoZeroDisbursementsAllowed();
+ if (_missingDisbursements[to] < value) revert OverdisbursementDetected();
+ // txHash is intentionally not checked for 0x0 or uniqueness to allow for flexibility.
+
+ unchecked {
+ _missingDisbursements[to] -= value;
+ }
_totalMissingDisbursements -= value;
- }
- _disbursements[to].push(Disbursement({value: value, txHash: txHash}));
+ _disbursements[to].push(Disbursement({value: value, txHash: txHashes[i]}));
- emit DisbursementCompleted(to, value, txHash);
+ emit DisbursementCompleted(to, value, txHashes[i]);
+
+ unchecked {
+ ++i;
+ }
+ }
}
/// @dev Reverts; this contract does not accept ETH.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment