diff --git a/contracts/Voter.sol b/contracts/Voter.sol index b86fa81..ce95cb4 100644 --- a/contracts/Voter.sol +++ b/contracts/Voter.sol @@ -76,6 +76,8 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { mapping(address => uint256) internal supplyIndex; /// @inheritdoc IVoter mapping(address => uint256) public claimable; + /// @inheritdoc IVoter + mapping(address => uint256) public triggerThreshold; constructor(address _forwarder, address _strategyManager, address _factoryRegistry) ERC2771Context(_forwarder) { forwarder = _forwarder; @@ -151,6 +153,15 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { maxVotingNum = _maxVotingNum; } + /// @inheritdoc IVoter + function setThreshold(address _pool, uint256 _threshold) external { + if (_msgSender() != governor) revert NotGovernor(); + address _gauge = gauges[_pool]; + if (_gauge == address(0)) revert GaugeDoesNotExist(_pool); + + triggerThreshold[_gauge] = _threshold; + } + /// @inheritdoc IVoter function reset() external onlyNewEpoch(msg.sender) nonReentrant { address _user = msg.sender; @@ -265,7 +276,7 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { } /// @inheritdoc IVoter - function createGauge(address _poolFactory, address _pool, uint8 _gaugeType) external nonReentrant returns (address) { + function createGauge(address _poolFactory, address _pool, uint8 _gaugeType, uint256 threshold) external nonReentrant returns (address) { if (gauges[_pool] != address(0)) revert GaugeExists(); (address incentiveFactory, address gaugeFactory) = IFactoryRegistry(factoryRegistry).factoriesToPoolFactory( _poolFactory @@ -277,7 +288,7 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { } address _incentiveReward = IIncentivesFactory(incentiveFactory).createRewards(forwarder, _pool); - address _gauge = IGaugeFactory(gaugeFactory).createGauge(forwarder, _pool, _incentiveReward, _gaugeType); + address _gauge = IGaugeFactory(gaugeFactory).createGauge(forwarder, _pool, _incentiveReward, _gaugeType, threshold); IIncentive(_incentiveReward).setGauge(_gauge); gauges[_pool] = _gauge; @@ -286,6 +297,7 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { isAlive[_gauge] = true; _updateFor(_gauge); pools.push(_pool); + triggerThreshold[_gauge] = threshold; emit GaugeCreated(_poolFactory, gaugeFactory, _pool, _gauge, sender); return _gauge; @@ -323,6 +335,10 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { address sender = _msgSender(); if (sender != vault) revert NotVault(); uint256 _amount = msg.value; + if (totalWeight == 0) { + payable(vault).transfer(_amount); + return; + } uint256 _ratio = (_amount * 1e18) / Math.max(totalWeight, 1); // 1e18 adjustment is removed during claim if (_ratio > 0) { index += _ratio; @@ -360,7 +376,7 @@ contract Voter is IVoter, ERC2771Context, ReentrancyGuard { uint256 _delta = _index - _supplyIndex; // see if there is any difference that need to be accrued if (_delta > 0) { uint256 _share = (_supplied * _delta) / 1e18; // add accrued difference for each supplied token - if (isAlive[_gauge]) { + if (isAlive[_gauge] && IRewardGauge(_gauge).depositUserNum() >= triggerThreshold[_gauge]) { claimable[_gauge] += _share; } else { payable(vault).transfer(_share); // send rewards back to Vault so they're not stuck in Voter diff --git a/contracts/factories/GaugeFactory.sol b/contracts/factories/GaugeFactory.sol index 990db46..e52de1e 100644 --- a/contracts/factories/GaugeFactory.sol +++ b/contracts/factories/GaugeFactory.sol @@ -14,14 +14,17 @@ contract GaugeFactory is IGaugeFactory { address _forwarder, address _poolOrDeviceNFTOrGauge, address _incentives, - uint8 _gaugeType + uint8 _gaugeType, + uint256 threshold ) external returns (address gauge) { address _gauge; if (_gaugeType == Erc20Gauge) { + require(threshold == 0, "threshold in erc20 gauge should be 0"); _gauge = createERC20Gauge(_forwarder, _poolOrDeviceNFTOrGauge, _incentives); } else if (_gaugeType == DeviceNFTGauge) { _gauge = createDeviceGauge(_forwarder, _poolOrDeviceNFTOrGauge, _incentives); } else if (_gaugeType == WithdrawGauge) { + require(threshold == 0, "threshold in withdraw gauge should be 0"); _gauge = createWithdrawalGauge(_poolOrDeviceNFTOrGauge); } else { revert IncorrectnessGaugeType(); diff --git a/contracts/gauges/DeviceGauge.sol b/contracts/gauges/DeviceGauge.sol index 3dc8af8..2f16793 100644 --- a/contracts/gauges/DeviceGauge.sol +++ b/contracts/gauges/DeviceGauge.sol @@ -60,6 +60,9 @@ contract DeviceGauge is RewardGauge, ERC721Holder { delete tokenWeight[_tokenId]; updateWeightBalance(sender); IIncentive(incentive).withdraw(_amount, sender); + if (balanceOf[sender] == 0){ + depositUserNum--; + } emit WithdrawDevice(sender, _amount, _tokenId); } diff --git a/contracts/gauges/ERC20Gauge.sol b/contracts/gauges/ERC20Gauge.sol index af87e17..8c99709 100644 --- a/contracts/gauges/ERC20Gauge.sol +++ b/contracts/gauges/ERC20Gauge.sol @@ -47,6 +47,9 @@ contract ERC20Gauge is RewardGauge { IERC20(stakingToken).safeTransfer(sender, _amount); updateWeightBalance(sender); IIncentive(incentive).withdraw(_amount, sender); + if (balanceOf[sender] == 0){ + depositUserNum--; + } emit Withdraw(sender, _amount); } diff --git a/contracts/gauges/RewardGauge.sol b/contracts/gauges/RewardGauge.sol index d7c986d..6bf3704 100644 --- a/contracts/gauges/RewardGauge.sol +++ b/contracts/gauges/RewardGauge.sol @@ -52,6 +52,8 @@ abstract contract RewardGauge is IRewardGauge, ERC2771Context, ReentrancyGuard { uint256 public totalWeightedBalance; address public incentive; + uint256 public depositUserNum; + constructor( address _forwarder, address _stakingToken, @@ -129,11 +131,18 @@ abstract contract RewardGauge is IRewardGauge, ERC2771Context, ReentrancyGuard { /// @inheritdoc IRewardGauge function deposit(uint256 _amountOrNFTID) external { - _depositFor(_amountOrNFTID, _msgSender()); + address sender = _msgSender(); + if (balanceOf[sender] == 0){ + depositUserNum++; + } + _depositFor(_amountOrNFTID, sender); } /// @inheritdoc IRewardGauge function deposit(uint256 _amountOrNFTID, address _recipient) external { + if (balanceOf[_recipient] == 0){ + depositUserNum++; + } _depositFor(_amountOrNFTID, _recipient); } diff --git a/contracts/interfaces/IRewardGauge.sol b/contracts/interfaces/IRewardGauge.sol index 4a75cb7..a2be6ab 100644 --- a/contracts/interfaces/IRewardGauge.sol +++ b/contracts/interfaces/IRewardGauge.sol @@ -94,4 +94,6 @@ interface IRewardGauge is IGauge { /// @param _user which vote to gauge with share /// @param _share amount of share to deposit in gauge function updateShare(address _user, uint256 _share) external; + + function depositUserNum() external returns (uint256); } diff --git a/contracts/interfaces/IVoter.sol b/contracts/interfaces/IVoter.sol index 7740378..eb82a64 100644 --- a/contracts/interfaces/IVoter.sol +++ b/contracts/interfaces/IVoter.sol @@ -150,6 +150,11 @@ interface IVoter { /// @param _governor . function setGovernor(address _governor) external; + /// @notice Set threshold for gauge with the pool + /// @param _pool . + /// @param _threshold . + function setThreshold(address _pool, uint256 _threshold) external; + /// @notice Set new emergency council. /// @dev Throws if not called by emergency council. /// @param _emergencyCouncil . @@ -162,6 +167,10 @@ interface IVoter { /// @param _maxVotingNum . function setMaxVotingNum(uint256 _maxVotingNum) external; + /// @notice get the threshold for the gauge + /// @param _gauge . + function triggerThreshold(address _gauge) external returns (uint256); + /// @notice Whitelist (or unwhitelist) token for use in bribes. /// @dev Throws if not called by governor. /// @param _token . @@ -174,7 +183,8 @@ interface IVoter { /// @param _poolFactory . /// @param _pool . /// @param _gaugeType 0: ERC20Gauge, 1: DeviceNFTGauge, 2: WithdrawGauge - function createGauge(address _poolFactory, address _pool, uint8 _gaugeType) external returns (address); + /// @param threshold only >0 for deviceNFTGauge + function createGauge(address _poolFactory, address _pool, uint8 _gaugeType, uint256 threshold) external returns (address); /// @notice Kills a gauge. The gauge will not receive any new emissions and cannot be deposited into. /// Can still withdraw from gauge. diff --git a/contracts/interfaces/factories/IGaugeFactory.sol b/contracts/interfaces/factories/IGaugeFactory.sol index e229cb4..4567f30 100644 --- a/contracts/interfaces/factories/IGaugeFactory.sol +++ b/contracts/interfaces/factories/IGaugeFactory.sol @@ -8,6 +8,7 @@ interface IGaugeFactory { address _forwarder, address _poolOrDeviceNFTOrGauge, address _incentives, - uint8 _gaugeType + uint8 _gaugeType, + uint256 _threshold ) external returns (address gauge); } diff --git a/contracts/test/TestDeviceNFT.sol b/contracts/test/TestDeviceNFT.sol index af171f5..2c52c1b 100644 --- a/contracts/test/TestDeviceNFT.sol +++ b/contracts/test/TestDeviceNFT.sol @@ -29,5 +29,6 @@ contract TestDeviceNFT is IWeightedNFT, ERC721 { function mint(address to, uint tokenId) external { _mint(to, tokenId); + weightOf[tokenId] = 1 ether; } } diff --git a/test/TestGauge.t.sol b/test/TestGauge.t.sol index be96dd9..622a6b6 100644 --- a/test/TestGauge.t.sol +++ b/test/TestGauge.t.sol @@ -9,12 +9,17 @@ import {DAOForwarder} from "../contracts/DAOForwarder.sol"; import {TestToken} from "../contracts/test/TestToken.sol"; import {ProtocolTimeLibrary} from "../contracts/libraries/ProtocolTimeLibrary.sol"; import {Incentives} from "../contracts/rewards/Incentive.sol"; +import "../contracts/test/TestStrategyManager.sol"; +import "../contracts/factories/GaugeFactory.sol"; +import "../contracts/factories/FactoryRegistry.sol"; +import "../contracts/factories/IncentivesFactory.sol"; contract TestERC20Gauge is Test { ERC20Gauge public gauge; DAOForwarder public forwarder; TestToken public pool; Voter public voter; + TestStrategyManager public strategyManager; fallback() external payable {} @@ -22,11 +27,16 @@ contract TestERC20Gauge is Test { pool = new TestToken("lp_pool", "pool"); forwarder = new DAOForwarder(); // manager & _factoryRegistry not used in gauge - voter = new Voter(address(forwarder), address(this), address(this)); - Incentives inti = new Incentives(address(forwarder), address(voter), new address[](0)); - gauge = new ERC20Gauge(address(forwarder), address(pool), address(voter), address(inti)); - vm.prank(address(voter)); - inti.setGauge(address(gauge)); + strategyManager = new TestStrategyManager(); + strategyManager.setShare(address (this), 100); + GaugeFactory gaugeFactory = new GaugeFactory(); + IncentivesFactory incentiveFactory = new IncentivesFactory(); + address poolFactory = address(1); + FactoryRegistry factoryRegistry = new FactoryRegistry(poolFactory, address(incentiveFactory), address(gaugeFactory)); + voter = new Voter(address(forwarder), address(strategyManager), address(factoryRegistry)); + address _gauge = voter.createGauge(poolFactory, address(pool), 0, 0); + gauge = ERC20Gauge(_gauge); + voter.killGauge(_gauge); } function test_deposit() external { @@ -70,30 +80,36 @@ contract TestERC20Gauge is Test { // 2. success rewards-1 // 2.1 deposit rewards to voter + skip(8 days); + address[] memory poolvote = new address[](1); + uint256[] memory weights = new uint256[](1); + poolvote[0] = address(pool); + weights[0] = 5000; + voter.vote(poolvote, weights); voter.notifyRewardAmount{value: 90 ether}(); // 2.2 simulate notify rewards by voter vm.prank(address(voter)); gauge.notifyRewardAmount{value: 1 ether}(); - assertEq(1, gauge.lastUpdateTime()); + assertEq(8 days + 1, gauge.lastUpdateTime()); assertEq(ProtocolTimeLibrary.epochNext(block.timestamp), gauge.periodFinish()); uint256 firstRate = gauge.rewardRate(); uint256 firstPeriod = gauge.periodFinish(); uint256 firstRewardPerToken = gauge.rewardPerToken(); console.log("firstRewardPerToken: ", firstRewardPerToken); skip(3000); // blockTime will set after 3000s - assertEq(3001, block.timestamp); + assertEq(8 days + 3000 + 1, block.timestamp); // 3. success reward-2 in same epoch vm.prank(address(voter)); gauge.notifyRewardAmount{value: 1 ether}(); - assertEq(3001, gauge.lastUpdateTime()); + assertEq(8 days + 3000 + 1, gauge.lastUpdateTime()); assertGt(gauge.rewardRate(), firstRate); assertEq(firstPeriod, gauge.periodFinish()); assertEq(2 ether, address(gauge).balance); uint256 secondRewardPerToken = gauge.rewardPerToken(); // 4. view earned in first epoch - skip(7 days - block.timestamp); + skip(14 days - block.timestamp); assertEq(block.timestamp, firstPeriod); uint256 firstEarned = gauge.earned(address(this)); console.log("firstPeriod: ", firstPeriod, "; blocktime: ", block.timestamp); diff --git a/test/TestVault.t.sol b/test/TestVault.t.sol index 8874979..89eb685 100644 --- a/test/TestVault.t.sol +++ b/test/TestVault.t.sol @@ -17,6 +17,7 @@ contract TestVault is Test { Vault public vault; Voter public voter; DAOForwarder public forwarder; + address public poolFactory; GaugeFactory public gaugeFactory; IncentivesFactory public incentiveFactory; FactoryRegistry public factoryRegistry; @@ -27,7 +28,9 @@ contract TestVault is Test { gaugeFactory = new GaugeFactory(); incentiveFactory = new IncentivesFactory(); strategyManager = new TestStrategyManager(); - factoryRegistry = new FactoryRegistry(address(1), address(incentiveFactory), address(gaugeFactory)); + strategyManager.setShare(address (this), 100); + poolFactory = address(1); + factoryRegistry = new FactoryRegistry(poolFactory, address(incentiveFactory), address(gaugeFactory)); voter = new Voter(address(forwarder), address(strategyManager), address(factoryRegistry)); vault = new Vault(); vault.initialize(address(voter), address(strategyManager)); @@ -65,8 +68,16 @@ contract TestVault is Test { uint256 _period = vault.emitReward(); // 5. updatePeriod success + skip(2 hours); payable(address(vault)).transfer(vault.weekly() - 1 ether); voter.initialize(new address[](0), address(vault)); + address pool = address (111); + voter.createGauge(poolFactory, address(pool), 0, 0); + address[] memory poolvote = new address[](1); + uint256[] memory weights = new uint256[](1); + poolvote[0] = address(pool); + weights[0] = 5000; + voter.vote(poolvote, weights); _period = vault.emitReward(); assertEq(7 days, _period); assertEq(address(vault).balance, 0); diff --git a/test/TestVoter.t.sol b/test/TestVoter.t.sol index 8a9e3b7..fc4f949 100644 --- a/test/TestVoter.t.sol +++ b/test/TestVoter.t.sol @@ -16,6 +16,7 @@ import {FactoryRegistry} from "../contracts/factories/FactoryRegistry.sol"; import {GaugeFactory} from "../contracts/factories/GaugeFactory.sol"; import {IGaugeFactory} from "../contracts/interfaces/factories/IGaugeFactory.sol"; import {IncentivesFactory} from "../contracts/factories/IncentivesFactory.sol"; +import "../contracts/libraries/ProtocolTimeLibrary.sol"; contract TestVoter is Test { Voter public voter; @@ -44,25 +45,25 @@ contract TestVoter is Test { function test_gauge_actions() external { //1. createGauge success - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); assertEq(1, voter.length()); assertNotEq(address(0), voter.gauges(address(pool))); //1.1 repeat add same pool so failed vm.expectRevert(IVoter.GaugeExists.selector); - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); assertEq(1, voter.length()); //1.2 caller not governor & pool is not whitelistedToken address pool2 = address(22); vm.prank(address(2)); vm.expectRevert(IVoter.NotWhitelistedToken.selector); - voter.createGauge(poolFactory, pool2, 0); + voter.createGauge(poolFactory, pool2, 0, 0); assertEq(1, voter.length()); //1.3 set whitelistedToken voter.whitelistToken(pool2, true); - voter.createGauge(poolFactory, pool2, 0); + voter.createGauge(poolFactory, pool2, 0, 0); assertEq(2, voter.length()); //2. kill gauge @@ -78,7 +79,7 @@ contract TestVoter is Test { function test_vote_actions() external { skip(10 days); - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); strategyManager.setShare(address(this), 500); //1. vote failed due to UnequalLengths @@ -123,7 +124,7 @@ contract TestVoter is Test { function test_notifyReward_updateFor_distribute_claimRewards() external { // 0. setup to create gauge and vote for the gauge - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); address gauge = voter.gauges(address(pool)); strategyManager.setShare(address(this), 1000); address[] memory poolvote = new address[](1); @@ -154,30 +155,30 @@ contract TestVoter is Test { function test_create_gauge() external { // 1. first create ERC20Gauge - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); address gauge = voter.gauges(address(pool)); assertTrue(gauge != address(0)); // 2. again create ERC20Gauge should failed for same pool vm.expectRevert(IVoter.GaugeExists.selector); - voter.createGauge(poolFactory, address(pool), 0); + voter.createGauge(poolFactory, address(pool), 0, 0); // 3. create NFT gauge address deviceNFT = address(new TestDeviceNFT("name", "symbol")); - voter.createGauge(poolFactory, deviceNFT, 1); + voter.createGauge(poolFactory, deviceNFT, 1, 10); gauge = voter.gauges(address(deviceNFT)); assertTrue(gauge != address(0)); // 4. create withdraw gauge address onlyWithdrawGauge = address(11); - voter.createGauge(poolFactory, onlyWithdrawGauge, 2); + voter.createGauge(poolFactory, onlyWithdrawGauge, 2, 0); gauge = voter.gauges(address(onlyWithdrawGauge)); assertTrue(gauge != address(0)); // 5. incorrect gaugeType vm.expectRevert(IGaugeFactory.IncorrectnessGaugeType.selector); address nextPool = address(12); - voter.createGauge(poolFactory, nextPool, 3); + voter.createGauge(poolFactory, nextPool, 3, 0); } receive() external payable {} diff --git a/test/flow.test.ts b/test/flow.test.ts index 1cfa389..586cb80 100644 --- a/test/flow.test.ts +++ b/test/flow.test.ts @@ -28,7 +28,7 @@ describe('Flow', function () { await voter.initialize([], vault.target); const token = await ethers.deployContract('TestToken', ['Test Token', 'TEST']); - await voter.createGauge(poolFactory.target, token.target, 0); + await voter.createGauge(poolFactory.target, token.target, 0, 0); await voter.vote([token.target], [5000]); });