Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions contracts/dao/ListaVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU

using SafeERC20 for IERC20;

event Deposit(address indexed account, uint256 amount);
event Withdraw(address indexed account, uint256 amount);
event Deposit(address indexed account, uint16 indexed week, uint256 amount);
event NewDistributorRegistered(address distributor, uint256 id);
event EmergencyWithdraw(address token, uint256 amount);
event Paused(address account);
event Unpaused(address account);
event EmissionVotingSet(address emissionVoting);
event DistributorBlacklistUpdated(uint16 indexed distributorId, bool blacklisted);
event WeeklyDistributorPercentSet(uint16 indexed week, uint16[] ids, uint256[] percents);

// lista token address
IERC20 public token;
Expand Down Expand Up @@ -61,6 +62,8 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU
bool public paused;
// emission voting address
IEmissionVoting public emissionVoting;
// blacklisted distributor ids — receive zero new emissions
mapping(uint16 => bool) public distributorBlacklist;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -120,7 +123,7 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU
weeklyEmissions[week] += amount;
token.safeTransferFrom(msg.sender, address(this), amount);

emit Deposit(msg.sender, amount);
emit Deposit(msg.sender, week, amount);
}

/**
Expand All @@ -142,6 +145,7 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU
}
for (uint16 i = 0; i < ids.length; ++i) {
require(idToDistributor[ids[i]] != address(0), "distributor not registered");
require(!distributorBlacklist[ids[i]], "distributor blacklisted");
require(weeklyDistributorPercent[week][ids[i]] == 0, "distributor percent already set");
weeklyDistributorPercent[week][ids[i]] = percent[i];
totalPercent += percent[i];
Expand All @@ -150,6 +154,8 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU
// mark this week set flag
weeklyDistributorPercent[week][0] = 1;
require(totalPercent <= 1e18, "Total percent must be less than or equal to 1e18");

emit WeeklyDistributorPercentSet(week, ids, percent);
}

/**
Expand All @@ -167,6 +173,25 @@ contract ListaVault is Initializable, AccessControlUpgradeable, ReentrancyGuardU
return distributorId;
}

/**
* @dev batch-set blacklist state for distributor ids. Blacklisted ids receive
* zero new emissions and cannot be set in setWeeklyDistributorPercent.
* Already-allocated balances are not affected. Ids already in the target
* state are skipped silently (no event); the whole batch is idempotent.
* @param ids distributor ids
* @param blacklisted true to blacklist all, false to remove all from blacklist
*/
function batchSetDistributorBlacklist(uint16[] memory ids, bool blacklisted) external onlyRole(MANAGER) {
require(ids.length > 0, "ids is empty");
for (uint16 i = 0; i < ids.length; ++i) {
require(idToDistributor[ids[i]] != address(0), "distributor not registered");
if (distributorBlacklist[ids[i]] != blacklisted) {
distributorBlacklist[ids[i]] = blacklisted;
emit DistributorBlacklistUpdated(ids[i], blacklisted);
}
}
}

/**
* @dev set emission voting contract address
* @param _emissionVoting emission voting contract address
Expand Down
6 changes: 6 additions & 0 deletions test/EmissionVoting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ contract EmissionVotingTest is Test {
listaToken.approve(address(vault), type(uint256).max);
vault.depositRewards(weeklyEmission, veLista.getCurrentWeek() + 1);
console.logString("Rewards deposited.");
// Deployed VeLista enforces freePenaltyPeriodNotStart on lock(); push the
// start time past test horizon so locks succeed under the upgraded impl.
(bool ok, ) = address(veLista).call(
abi.encodeWithSignature("setFreePenaltyPeriod(uint256,uint256)", block.timestamp + 365 days, block.timestamp + 730 days)
);
require(ok, "setFreePenaltyPeriod failed");
vm.stopPrank();

deal(address(listaToken), multiSig, 10000e18);
Expand Down
247 changes: 247 additions & 0 deletions test/dao/ListaVault.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

import "../../contracts/VeLista.sol";
import "../../contracts/ListaToken.sol";
import "../../contracts/dao/ListaVault.sol";
import "../../contracts/dao/EmissionVoting.sol";
import "../../contracts/dao/interfaces/IDistributor.sol";

contract MockDistributor is IDistributor {
uint16 public registeredId;
function vaultClaimReward(address) external pure returns (uint256) { return 0; }
function vaultClaimStakingReward(address) external pure returns (uint256) { return 0; }
function notifyRegisteredId(uint16 _emissionId) external returns (bool) {
registeredId = _emissionId;
return true;
}
function claimableReward(address) external pure returns (uint256) { return 0; }
function notifyStakingReward(uint256) external {}
function lpToken() external pure returns (address) { return address(0); }
}

contract ListaVaultTest is Test {
ListaVault public vault = ListaVault(0x307d13267f360f78005f476Fa913F8848F30292A);
ListaToken public listaToken = ListaToken(0xFceB31A79F71AC9CBDCF853519c1b12D379EdC46);
VeLista public veLista = VeLista(0xd0C380D31DB43CD291E2bbE2Da2fD6dc877b87b3);
ProxyAdmin public vaultProxyAdmin = ProxyAdmin(0xd6cd036133cbf6a275B7700fF7B41887A9d5FCAe);

address timelock = 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253;
address vaultAdmin = 0x8d388136d578dCD791D081c6042284CED6d9B0c6; // MANAGER
address operator = makeAddr("operator");
address attacker = makeAddr("attacker");

MockDistributor public d1;
MockDistributor public d2;
uint16 public id1;
uint16 public id2;

function setUp() public {
vm.createSelectFork("https://bsc-dataseed.binance.org");

// upgrade vault to local impl with blacklist
ListaVault impl = new ListaVault();
vm.prank(timelock);
vaultProxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(address(vault)),
address(impl),
bytes("")
);

// grant OPERATOR + register two fresh distributors
vm.startPrank(vaultAdmin);
vault.grantRole(vault.OPERATOR(), operator);
d1 = new MockDistributor();
d2 = new MockDistributor();
id1 = vault.registerDistributor(address(d1));
id2 = vault.registerDistributor(address(d2));

// fund weekly emissions for next week so getDistributorWeeklyEmissions has a non-zero pool
uint16 nextWeek = veLista.getCurrentWeek() + 1;
deal(address(listaToken), vaultAdmin, 1000e18);
listaToken.approve(address(vault), type(uint256).max);
vault.depositRewards(1000e18, nextWeek);
vm.stopPrank();
}

// ---------- access control ----------

function test_batchSetDistributorBlacklist_revertsForNonManager() public {
vm.prank(attacker);
vm.expectRevert(); // AccessControl reverts with role-specific message
vault.batchSetDistributorBlacklist(_one(id1), true);
}

function test_batchSetDistributorBlacklist_managerCanSet() public {
vm.expectEmit(true, false, false, true, address(vault));
emit ListaVault.DistributorBlacklistUpdated(id1, true);

vm.prank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);

assertTrue(vault.distributorBlacklist(id1));
}

function test_batchSetDistributorBlacklist_canUnblacklist() public {
vm.startPrank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);
vault.batchSetDistributorBlacklist(_one(id1), false);
vm.stopPrank();

assertFalse(vault.distributorBlacklist(id1));
}

function test_batchSetDistributorBlacklist_appliesToAllIds() public {
uint16[] memory ids = new uint16[](2);
ids[0] = id1;
ids[1] = id2;

vm.prank(vaultAdmin);
vault.batchSetDistributorBlacklist(ids, true);

assertTrue(vault.distributorBlacklist(id1));
assertTrue(vault.distributorBlacklist(id2));
}

// ---------- validation ----------

function test_batchSetDistributorBlacklist_revertsOnEmptyArray() public {
uint16[] memory ids = new uint16[](0);
vm.prank(vaultAdmin);
vm.expectRevert("ids is empty");
vault.batchSetDistributorBlacklist(ids, true);
}

function test_batchSetDistributorBlacklist_revertsOnUnregisteredId() public {
uint16 unregisteredId = vault.distributorId() + 1;
vm.prank(vaultAdmin);
vm.expectRevert("distributor not registered");
vault.batchSetDistributorBlacklist(_one(unregisteredId), true);
}

function test_batchSetDistributorBlacklist_skipsNoOpIdsSilently() public {
// ids already in target state are skipped — no revert, no event
vm.startPrank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), false); // already false → silent
assertFalse(vault.distributorBlacklist(id1));

vault.batchSetDistributorBlacklist(_one(id1), true);
vault.batchSetDistributorBlacklist(_one(id1), true); // already true → silent
assertTrue(vault.distributorBlacklist(id1));
vm.stopPrank();
}

// ---------- gate: setWeeklyDistributorPercent ----------

function test_setWeeklyDistributorPercent_revertsForBlacklistedId() public {
vm.prank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);

uint16 nextWeek = veLista.getCurrentWeek() + 1;
uint16[] memory ids = new uint16[](1);
uint256[] memory pct = new uint256[](1);
ids[0] = id1;
pct[0] = 5e17;

vm.prank(operator);
vm.expectRevert("distributor blacklisted");
vault.setWeeklyDistributorPercent(nextWeek, ids, pct);
}

function test_setWeeklyDistributorPercent_succeedsAfterUnblacklist() public {
vm.startPrank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);
vault.batchSetDistributorBlacklist(_one(id1), false);
vm.stopPrank();

uint16 nextWeek = veLista.getCurrentWeek() + 1;
uint16[] memory ids = new uint16[](1);
uint256[] memory pct = new uint256[](1);
ids[0] = id1;
pct[0] = 5e17;

vm.prank(operator);
vault.setWeeklyDistributorPercent(nextWeek, ids, pct);

assertEq(vault.weeklyDistributorPercent(nextWeek, id1), 5e17);
}

// ---------- preserve historical claim ----------
//
// Blacklist enforcement is at the input (setWeeklyDistributorPercent) only.
// getDistributorWeeklyEmissions does NOT short-circuit, so emissions already
// earned in past weeks (before the blacklist was set) remain claimable.

function test_allocateNewEmissions_preservesHistoricalAfterBlacklist() public {
// OPERATOR sets percent for nextWeek BEFORE blacklist
uint16 nextWeek = veLista.getCurrentWeek() + 1;
uint16[] memory ids = new uint16[](1);
uint256[] memory pct = new uint256[](1);
ids[0] = id1; pct[0] = 1e18; // 100%

vm.prank(operator);
vault.setWeeklyDistributorPercent(nextWeek, ids, pct);

// advance past nextWeek so the percent matures into a claimable amount
vm.warp(block.timestamp + 8 days);

// NOW blacklist — should not erase the past week's entitlement
vm.prank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);

uint256 expected = vault.weeklyEmissions(nextWeek);
assertGt(expected, 0, "weeklyEmissions sanity");

vm.prank(address(d1));
uint256 got = vault.allocateNewEmissions(id1);
assertEq(got, expected, "blacklisted distributor must still claim past-week emissions");
assertEq(vault.allocated(address(d1)), expected);
}

function test_setWeeklyDistributorPercent_blocksFutureWhileBlacklisted() public {
// confirm the input gate stops new allocations
vm.prank(vaultAdmin);
vault.batchSetDistributorBlacklist(_one(id1), true);

uint16 nextWeek = veLista.getCurrentWeek() + 1;
uint16[] memory ids = new uint16[](1);
uint256[] memory pct = new uint256[](1);
ids[0] = id1; pct[0] = 1e18;

vm.prank(operator);
vm.expectRevert("distributor blacklisted");
vault.setWeeklyDistributorPercent(nextWeek, ids, pct);
}

function test_allocateNewEmissions_nonzeroForNonBlacklisted() public {
uint16 nextWeek = veLista.getCurrentWeek() + 1;
uint16[] memory ids = new uint16[](1);
uint256[] memory pct = new uint256[](1);
ids[0] = id1; pct[0] = 1e18;

vm.prank(operator);
vault.setWeeklyDistributorPercent(nextWeek, ids, pct);

vm.warp(block.timestamp + 8 days);

// 100% allocation → distributor receives the full weeklyEmissions pool for that week
uint256 expected = vault.weeklyEmissions(nextWeek);
assertGt(expected, 0, "weeklyEmissions sanity");

vm.prank(address(d1));
uint256 got = vault.allocateNewEmissions(id1);
assertEq(got, expected);
assertEq(vault.allocated(address(d1)), expected);
}

// ---------- helpers ----------

function _one(uint16 id) internal pure returns (uint16[] memory a) {
a = new uint16[](1);
a[0] = id;
}
}
Loading
Loading