Created
March 3, 2026 20:31
-
-
Save pkoch/b7dacd8c4cf6db843f7f5b32efe228a9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/script/initial-distribution/README.md b/script/initial-distribution/README.md | |
| index 7bb3e0a..7a27bd1 100644 | |
| --- a/script/initial-distribution/README.md | |
| +++ b/script/initial-distribution/README.md | |
| @@ -40,7 +40,7 @@ These scripts interact with several on-chain contracts: | |
| - **CCADisbursementTracker** -- Bookkeeping contract for the CCA flow. It | |
| bridges the gap between what the CCA thinks should be disbursed and what's | |
| actually disbursed by our scripts. It has a | |
| - `recordDisbursement(beneficiary, amount, txHash)` function that records the | |
| + `recordDisbursements(recipients, amounts, txHashes)` function that records | |
| disbursement on-chain for clear traceability of bonuses and vesting splits. | |
| - **BatchCaller** -- Plumbing. EIP-7702 delegation target that lets the | |
| disburser EOA execute multiple `disburse` calls in a single transaction. Used | |
| @@ -134,7 +134,7 @@ The CCA flow uses two separate disbursers: | |
| - **`TDE_DISBURSER_PRIVATE_KEY`** -- signs `TDEDisbursement.disburse()` calls | |
| and IDOS token approvals. | |
| - **`TRACKER_DISBURSER_PRIVATE_KEY`** -- signs | |
| - `CCADisbursementTracker.recordDisbursement()` calls and | |
| + `CCADisbursementTracker.recordDisbursements()` calls and | |
| `CCA.exitBid()`/`claimTokens()` calls. | |
| ```bash | |
| diff --git a/script/initial-distribution/fork-run.sh b/script/initial-distribution/fork-run.sh | |
| old mode 100644 | |
| new mode 100755 | |
| index fbd38ee..f51df69 | |
| --- a/script/initial-distribution/fork-run.sh | |
| +++ b/script/initial-distribution/fork-run.sh | |
| @@ -15,10 +15,10 @@ ANVIL_PORT="${ANVIL_PORT:-8545}" | |
| ANVIL_RPC="http://127.0.0.1:${ANVIL_PORT}" | |
| ARB1_RPC="https://arb1.arbitrum.io/rpc" | |
| -TDE_DISBURSEMENT=0xdf24F4Ca9984807577d13f5ef24eD26e5AFc7083 | |
| -CCA_TRACKER=0xb628B89067E8f7Dfc2cB528a72BcfF7d5cEDcE29 | |
| -CCA=0xc27F8a94Df88C4f57B09067e07EA6bC11CA47e11 | |
| -IDOS_TOKEN=0x68731d6F14B827bBCfFbEBb62b19Daa18de1d79c | |
| +TDE_DISBURSEMENT=0x4d428cecf85667e1cb90d24d1130683c78df48b5 | |
| +CCA_TRACKER=0xFbEf5ABeD8117f98988C5f694312b32b2A038dC2 | |
| +CCA=0x8Ad0b34755A333027Ca6cb03Cc96E9E7b6d74320 | |
| +IDOS_TOKEN=0x48fb081aeedb1a0a6143e716a839b94e927f82be | |
| # ── Start Anvil ────────────────────────────────────────────────────────────── | |
| diff --git a/script/initial-distribution/src/abis.ts b/script/initial-distribution/src/abis.ts | |
| index c8e96d6..4654d37 100644 | |
| --- a/script/initial-distribution/src/abis.ts | |
| +++ b/script/initial-distribution/src/abis.ts | |
| @@ -12,11 +12,11 @@ export const trackerAbi = [ | |
| }, | |
| { | |
| type: "function", | |
| - name: "recordDisbursement", | |
| + name: "recordDisbursements", | |
| inputs: [ | |
| - { name: "to", type: "address" }, | |
| - { name: "value", type: "uint256" }, | |
| - { name: "txHash", type: "bytes32" }, | |
| + { name: "recipients", type: "address[]" }, | |
| + { name: "values", type: "uint256[]" }, | |
| + { name: "txHashes", type: "bytes32[]" }, | |
| ], | |
| outputs: [], | |
| stateMutability: "nonpayable", | |
| diff --git a/script/initial-distribution/src/cca.ts b/script/initial-distribution/src/cca.ts | |
| index 61570a4..16b83fd 100644 | |
| --- a/script/initial-distribution/src/cca.ts | |
| +++ b/script/initial-distribution/src/cca.ts | |
| @@ -197,7 +197,7 @@ async function recordOnTracker(to: Address, ccaAmount: bigint, txHash: Hex): Pro | |
| return ( | |
| await receiptFor( | |
| publicClient, | |
| - await trackerContract.write.recordDisbursement([to, ccaAmount, txHash]), | |
| + await trackerContract.write.recordDisbursements([[to], [ccaAmount], [txHash]]), | |
| ) | |
| ).transactionHash; | |
| } | |
| 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. | |
| diff --git a/test/CCADisbursementTracker.invariant.sol b/test/CCADisbursementTracker.invariant.sol | |
| index 13afbdc..a342cb4 100644 | |
| --- a/test/CCADisbursementTracker.invariant.sol | |
| +++ b/test/CCADisbursementTracker.invariant.sol | |
| @@ -32,8 +32,15 @@ contract DisbursementHandler is Test { | |
| uint256 value = bound(valueSeed, 1, available); | |
| + address[] memory recipientsArr = new address[](1); | |
| + recipientsArr[0] = recipient; | |
| + uint256[] memory values = new uint256[](1); | |
| + values[0] = value; | |
| + bytes32[] memory txHashes = new bytes32[](1); | |
| + txHashes[0] = txHash; | |
| + | |
| vm.prank(disburser); | |
| - tracker.recordDisbursement(recipient, value, txHash); | |
| + tracker.recordDisbursements(recipientsArr, values, txHashes); | |
| ghostTotalDisbursed += value; | |
| ghostDisbursedTo[recipient] += value; | |
| diff --git a/test/CCADisbursementTracker.t.sol b/test/CCADisbursementTracker.t.sol | |
| index f7fc739..0246046 100644 | |
| --- a/test/CCADisbursementTracker.t.sol | |
| +++ b/test/CCADisbursementTracker.t.sol | |
| @@ -184,7 +184,13 @@ contract CCADisbursementTrackerUnitTest is Test { | |
| function _recordSingle(address to, uint256 value, bytes32 txHash) internal { | |
| vm.prank(disburser_); | |
| - tracker.recordDisbursement(to, value, txHash); | |
| + address[] memory recipients = new address[](1); | |
| + recipients[0] = to; | |
| + uint256[] memory values = new uint256[](1); | |
| + values[0] = value; | |
| + bytes32[] memory txHashes = new bytes32[](1); | |
| + txHashes[0] = txHash; | |
| + tracker.recordDisbursements(recipients, values, txHashes); | |
| } | |
| function test_RecordDisbursement_RevertsIfSaleNotFullyClaimed() public { | |
| @@ -197,9 +203,15 @@ contract CCADisbursementTrackerUnitTest is Test { | |
| function test_RecordDisbursement_RevertsIfNotDisburser() public { | |
| _completeSale(); | |
| + address[] memory r = new address[](1); | |
| + r[0] = holder1; | |
| + uint256[] memory v = new uint256[](1); | |
| + v[0] = 1; | |
| + bytes32[] memory h = new bytes32[](1); | |
| + h[0] = bytes32(uint256(1)); | |
| vm.prank(nobody); | |
| vm.expectRevert(CCADisbursementTracker.OnlyDisburserCanRecordDisbursements.selector); | |
| - tracker.recordDisbursement(holder1, 1, bytes32(uint256(1))); | |
| + tracker.recordDisbursements(r, v, h); | |
| } | |
| function test_RecordDisbursement_RevertsOnZeroAddress() public { | |
| @@ -661,6 +673,12 @@ contract CCADisbursementTrackerIntegrationTest is Test { | |
| function _recordSingle(address to, uint256 value, bytes32 txHash) internal { | |
| vm.prank(disburser_); | |
| - tracker.recordDisbursement(to, value, txHash); | |
| + address[] memory recipients = new address[](1); | |
| + recipients[0] = to; | |
| + uint256[] memory values = new uint256[](1); | |
| + values[0] = value; | |
| + bytes32[] memory txHashes = new bytes32[](1); | |
| + txHashes[0] = txHash; | |
| + tracker.recordDisbursements(recipients, values, txHashes); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment