Skip to content

fea: add distributor blacklist to ListaVault - #108

Merged
razww merged 5 commits into
masterfrom
feat/distributor-blacklist
May 27, 2026
Merged

fea: add distributor blacklist to ListaVault#108
razww merged 5 commits into
masterfrom
feat/distributor-blacklist

Conversation

@ricklista

@ricklista ricklista commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a MANAGER-controlled blacklist on ListaVault so that specific distributorIds can be cut off from new emissions without un-registering them. Already-allocated balances and unclaimed historical entitlements remain claimable. Includes one independent test-only fix that unblocks EmissionVoting.t.sol under the deployed VeLista's new freePenaltyPeriodNotStart modifier. Also folds in event-level cleanups surfaced by the external audit (I04/I05).

Change type

  • New contract
  • Upgrade (existing proxy)
  • Bug fix (test fix only — test/EmissionVoting.t.sol)
  • Gas optimization
  • Configuration change
  • Migration / deploy script
  • Test
  • Dependency update

Contracts changed

Contract File Type
ListaVault contracts/dao/ListaVault.sol modified
ListaVaultTest test/dao/ListaVault.t.sol new (fork-based)
ListaVaultUnitTest test/dao/ListaVaultUnit.t.sol new (pure unit)
EmissionVotingTest test/EmissionVoting.t.sol modified (test fix)

Interface changes

New external function (MANAGER-only, batch):

function batchSetDistributorBlacklist(uint16[] memory ids, bool blacklisted) external onlyRole(MANAGER);
  • reverts "ids is empty" if ids.length == 0
  • reverts "distributor not registered" if any idToDistributor[ids[i]] == address(0) (also rejects id=0)
  • ids already in the target state are skipped silently (no event) — call is idempotent

New event:

event DistributorBlacklistUpdated(uint16 indexed distributorId, bool blacklisted);

Emitted once per id whose state actually changed.

New public storage getter: distributorBlacklist(uint16) → bool.

Modified behavior:

  • setWeeklyDistributorPercent(...) — additionally reverts with "distributor blacklisted" if any id in the input array is blacklisted. This is the only enforcement gate — it stops new percents from being recorded for blacklisted ids.
  • getDistributorWeeklyEmissions(...) and allocateNewEmissions(...)unchanged. Historical weeklyDistributorPercent[week][id] entries set before a blacklist remain claimable; the distributor can still call allocateNewEmissions and receive past-week amounts. (Voting-path emissions are also preserved — voter-driven allocations are a governance concern, not blocked here.)

Event changes (audit follow-up — I04 / I05)

  • event Withdraw(address indexed, uint256)removed (was declared but never emitted; I04).
  • event Deposit(address indexed account, uint256 amount)event Deposit(address indexed account, uint16 indexed week, uint256 amount) — week added, indexed for filtering (I05). ABI breaking for off-chain consumers: topic0 hash changes; downstream subgraph / indexers / dashboards / frontends must redeploy the new ABI in the same window as the on-chain upgrade.
  • event WeeklyDistributorPercentSet(uint16 indexed week, uint16[] ids, uint256[] percents)new, emitted at the end of setWeeklyDistributorPercent so weekly percent updates are observable on-chain (I05).

No function signatures, modifiers, or other events were changed.

Storage layout

Confirmed via forge inspect contracts/dao/ListaVault.sol:ListaVault storage-layout:

Slot Name Type
4294906062 emissionVoting (existing tail) contract IEmissionVoting
4294906063 distributorBlacklist (new) mapping(uint16 => bool)

All pre-existing variable slots are unchanged. New variable is appended at the end of contract storage — safe for the live transparent proxy upgrade. No __gap was used in the original contract; no gap accounting needed. The event-level changes (I04/I05) do not touch storage.

Access control

Function Role Change
batchSetDistributorBlacklist MANAGER new — reuses existing role constant keccak256("MANAGER")
setWeeklyDistributorPercent OPERATOR unchanged (added blacklist precondition inside body)
registerDistributor MANAGER unchanged

No new roles introduced. No changes to _authorizeUpgrade, DEFAULT_ADMIN_ROLE, PAUSER, or OPERATOR semantics.

Risk assessment

Area Risk Note
Storage collision 🟢 None New mapping appended at slot 4294906063; prior layout untouched (verified via forge inspect).
Fund safety 🟢 None Blacklist is an input-side gate only; never moves tokens, never erases past entitlements. Distributors retain access to emissions earned before being blacklisted.
Access control 🟢 None New write surface is gated by the existing MANAGER role. Validation rejects empty arrays and unregistered ids; idempotent on no-ops.
External call safety 🟢 None No new external calls, no callbacks, no token transfers in the new code path.
Voting-path coverage 🟡 Low If EmissionVoting is active and voters allocate weight to a blacklisted id, that id will still receive emissions for the voted week. By design — voter governance is the source of truth for the voting path. Document for ops if a stronger policy is needed.
Off-chain ABI sync 🟡 Medium Deposit event signature changed (I05). Existing indexers stop receiving Deposit until they pick up the new ABI. Coordinate downstream redeploy with the on-chain upgrade window.

Deployment

Transparent proxy upgrade on BSC mainnet via the standard TimeLock SOP:

  • TimeLock: 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253
  • ListaVault proxy: 0x307d13267f360f78005f476Fa913F8848F30292A
  • ProxyAdmin (OZ v5, admin of the proxy): 0xd6cd036133cbf6a275b7700ff7b41887a9d5fcae
  • New implementation (already deployed): 0x29202d64986097a099575807ed8284b0fd457167

TimeLock target = ProxyAdmin; calldata = upgradeAndCall(proxy, newImpl, ""). Schedule → 1-day delay → execute. No new env vars. No reinitializer — new state defaults to zero/false, no migration call. End-to-end flow simulated on an Anvil BSC mainnet fork (schedule → time-skip → execute → impl slot confirmed at the new address).

Audit

External audit on this branch (PR #108) returned Informational overall, 5 findings, all on ListaVault.sol:

ID Status
I01 — setEmissionVoting may erase historical voting-based emissions if called with unsettled weeks Accepted, address via runbook / ops
I02 — setWeeklyDistributorPercent totalPercent has no lower bound Accepted, ops policy
I03 — ReentrancyGuardUpgradeable inherited but not initialized / applied Accepted, address in a follow-up
I04 — unused Withdraw event declaration Fixed in ceeede1
I05 — missing / incomplete events for key state changes Fixed in ceeede1

Test plan

  • forge build — no errors
  • forge test --match-path 'test/dao/ListaVault.t.sol' — 12/12 pass (BSC fork)
  • forge test --match-path 'test/dao/ListaVaultUnit.t.sol' — 18/18 pass (pure unit, ~2ms)
  • forge test --match-path 'test/EmissionVoting.t.sol' — 3/3 pass (was failing on master with free penalty period start; fix included)
  • Storage layout check — new var appended, no slot moves
  • Historical-claim preservation verified for both percent and voting paths
  • Timelock propose → execute simulated on Anvil BSC fork; proxy impl slot updates to new impl address

🤖 Generated with Claude Code

ricklista and others added 5 commits May 6, 2026 17:46
The deployed VeLista impl introduced a freePenaltyPeriodNotStart modifier
on lock(); on a current-block fork the period is already started and the
test setUp reverts. Push freePenaltyStartTime past the test horizon via
the MANAGER role (vaultAdmin) so locks succeed under the upgraded impl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MANAGER can flag a distributorId as blacklisted; blacklisted ids receive
zero new emissions and cannot be set in setWeeklyDistributorPercent.
Already-allocated balances are not affected.

Two enforcement gates:
- setWeeklyDistributorPercent: revert "distributor blacklisted" — loud
  failure for OPERATOR rather than silent zero
- getDistributorWeeklyEmissions: short-circuit to 0 — covers both the
  percent and emissionVoting paths, and blocks downstream allocateNewEmissions

Storage: append-only mapping at the end of ListaVault state, preserves
UUPS layout.

Tests: 10 fork-based + 15 pure unit (mocks for VeLista/EmissionVoting) —
covers access control, validation, both gates across all three emission
paths, and downstream allocateNewEmissions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the single-id setter for batchSetDistributorBlacklist(uint16[] ids,
bool blacklisted) — uniform flag across the array. Semantics adjusted:

- Empty array reverts ("ids is empty")
- Unregistered id in the batch reverts ("distributor not registered")
- Ids already in the target state are skipped silently (no event)

The silent skip makes the call idempotent so OPERATOR can replay batches
without curating the input list. State changes still emit one
DistributorBlacklistUpdated event per id.

Tests: 30 total (12 fork + 18 unit) — adds appliesToAllIds,
revertsOnEmptyArray, partialNoOpEmitsOnlyForChanged, plus the renamed
batchSet variants of every prior case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…utors

Drop the if (distributorBlacklist[id]) return 0; short-circuit in
getDistributorWeeklyEmissions. Reading past percent/voting weights and
short-circuiting on current blacklist state would retroactively erase
emissions a distributor already earned in earlier weeks — they could no
longer be claimed via allocateNewEmissions.

Blacklist enforcement now lives at the input only:
- setWeeklyDistributorPercent reverts on blacklisted ids → no NEW
  percent gets recorded for them, so future weeks naturally yield 0
- existing weeklyDistributorPercent[week][id] entries from prior weeks
  are honored when the distributor calls allocateNewEmissions
- voting-path emissions for blacklisted ids are NOT blocked here —
  voters' choice prevails (separate governance concern)

Tests: drop the three retroactive-zero cases, add three preservation
cases (percent path, voting path, downstream allocate) plus future-gate
verification. 30/30 pass (12 fork + 18 unit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove unused Withdraw event declaration (I04)
- Add indexed week parameter to Deposit event (I05)
- Emit WeeklyDistributorPercentSet on weekly percent updates (I05)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@razww razww left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@razww
razww merged commit b3bd942 into master May 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants