diff --git a/contracts/Booster.sol b/contracts/Booster.sol index 2610a93..f8d63b5 100644 --- a/contracts/Booster.sol +++ b/contracts/Booster.sol @@ -45,7 +45,7 @@ contract Booster is ReentrancyGuardUpgradeable { address public staker; address public minter; address public veAsset; - address public feeDistro; + // address public feeDistro; address public rewardFactory; address public stashFactory; address public tokenFactory; @@ -55,8 +55,8 @@ contract Booster is ReentrancyGuardUpgradeable { address public stakerRewards; //vetoken rewards address public stakerLockRewards; // veToken lock rewards xVE3D address public lockRewards; //ve3Token rewards(veAsset) - address public lockFees; //ve3Token veVeAsset fees - address public feeToken; + // address public lockFees; //ve3Token veVeAsset fees + // address public feeToken; bool public isShutdown; @@ -69,6 +69,18 @@ contract Booster is ReentrancyGuardUpgradeable { bool shutdown; } + struct FeeDistro { + address distro; + address rewards; + bytes32 executionHash; + bool active; + } + + address[] public allFeeTokens; + + //reward identifier -> distro, execution and virtual pool data + mapping(address => FeeDistro) public feeTokens; + //index(pid) -> pool PoolInfo[] public poolInfo; mapping(address => bool) public gaugeMap; @@ -110,8 +122,8 @@ contract Booster is ReentrancyGuardUpgradeable { function __Booster_init( address _staker, address _minter, - address _veAsset, - address _feeDistro + address _veAsset + // address _feeDistro ) external initializer { isShutdown = false; staker = _staker; @@ -121,7 +133,7 @@ contract Booster is ReentrancyGuardUpgradeable { poolManager = msg.sender; minter = _minter; veAsset = _veAsset; - feeDistro = _feeDistro; + // feeDistro = _feeDistro; lockIncentive = 1000; stakerIncentive = 450; earmarkIncentive = 50; @@ -190,34 +202,39 @@ contract Booster is ReentrancyGuardUpgradeable { //reward contracts are immutable or else the owner //has a means to redeploy and mint cvx via rewardClaimed() - if (lockRewards == address(0)) { - require(_rewards != address(0), "Not allowed!"); + if (lockRewards == address(0) && _rewards != address(0)) { lockRewards = _rewards; } - if (stakerRewards == address(0)) { - require(_stakerRewards != address(0), "Not allowed!"); + if (stakerRewards == address(0) && _stakerRewards != address(0)) { stakerRewards = _stakerRewards; } - if (stakerLockRewards == address(0)) { - require(_stakerLockRewards != address(0), "Not allowed!"); + if (stakerLockRewards == address(0) && _stakerLockRewards != address(0)) { stakerLockRewards = _stakerLockRewards; } emit RewardContractsUpdated(_rewards, _stakerRewards, _stakerLockRewards); } - // Set reward token and claim contract, get from Curve's registry - function setFeeInfo(uint256 _lockFeesIncentive, uint256 _stakerLockFeesIncentive) external { + // Set reward token and distro claim contract; create new virtual reward pool and sets execution hash + // Also used to add or update distro contracts and execution hashes for an already active reward token + function setFeeInfo( + uint256 _lockFeesIncentive, + uint256 _stakerLockFeesIncentive, + address _feeToken, + address _distro, + bytes32 _executionHash + ) external { require(msg.sender == feeManager, "!auth"); - require(_lockFeesIncentive.add(_stakerLockFeesIncentive) == FEE_DENOMINATOR); + require(_lockFeesIncentive.add(_stakerLockFeesIncentive) == FEE_DENOMINATOR, "!fee"); lockFeesIncentive = _lockFeesIncentive; stakerLockFeesIncentive = _stakerLockFeesIncentive; - address _feeToken = IFeeDistro(feeDistro).token(); - if (feeToken != _feeToken) { + if (feeTokens[_feeToken].active != true) { + require(!gaugeMap[_feeToken], "!token"); + //require that we initialize at the zero index //create a new reward contract for the new token - lockFees = IRewardFactory(rewardFactory).CreateTokenRewards(_feeToken, lockRewards); + address lockFees = IRewardFactory(rewardFactory).CreateTokenRewards(_feeToken, lockRewards); if (_feeToken != veAsset) { IRewards(stakerLockRewards).addReward( @@ -230,7 +247,14 @@ contract Booster is ReentrancyGuardUpgradeable { ); } - feeToken = _feeToken; + feeTokens[_feeToken] = FeeDistro(_distro, lockFees, _executionHash, true); + + allFeeTokens.push(_feeToken); + + } else { + feeTokens[_feeToken].distro = _distro; + feeTokens[_feeToken].executionHash = _executionHash; + } } @@ -407,6 +431,13 @@ contract Booster is ReentrancyGuardUpgradeable { address token = pool.token; ITokenMinter(token).burn(_from, _amount); + // @dev handle staking factor for Angle , + // use try and catch as not all Angle gauges have scaling factor + if (IVoteEscrow(staker).escrowModle() == IVoteEscrow.EscrowModle.ANGLE) { + try IGauge(gauge).scaling_factor() { + _amount = _amount.mul(IGauge(gauge).scaling_factor()).div(10**18); + } catch {} + } //pull from gauge if not shutdown // if shutdown tokens will be in this contract if (!pool.shutdown) { @@ -524,6 +555,7 @@ contract Booster is ReentrancyGuardUpgradeable { _claimStashReward(stash); } } + //veAsset balance uint256 veAssetBal = IERC20Upgradeable(veAsset).balanceOf(address(this)); @@ -594,20 +626,27 @@ contract Booster is ReentrancyGuardUpgradeable { } //claim fees from fee distro contract, put in lockers' reward contract - function earmarkFees() external returns (bool) { + function earmarkFees(address feeToken, bytes calldata _executionData) external returns (bool) { + // hash our execution data for comparison + bytes32 hashedExecutionData = keccak256(_executionData); + // enforce that the execution data is approved + require(hashedExecutionData == feeTokens[feeToken].executionHash, "!auth"); //claim fee rewards - IStaker(staker).claimFees(feeDistro, feeToken); - //send fee rewards to reward contract + IStaker(staker).claimFees(feeTokens[feeToken].distro, feeToken, _executionData); + // access contract token balance after claiming rewards uint256 _balance = IERC20Upgradeable(feeToken).balanceOf(address(this)); + // calculate incentive split for reward contracts uint256 _lockFeesIncentive = _balance.mul(lockFeesIncentive).div(FEE_DENOMINATOR); uint256 _stakerLockFeesIncentive = _balance.mul(stakerLockFeesIncentive).div( FEE_DENOMINATOR ); + // transfer to virtual reward pool and queue the new rewards if (_lockFeesIncentive > 0) { - IERC20Upgradeable(feeToken).safeTransfer(lockFees, _lockFeesIncentive); - IRewards(lockFees).queueNewRewards(_lockFeesIncentive); + IERC20Upgradeable(feeToken).safeTransfer(feeTokens[feeToken].rewards, _lockFeesIncentive); + IRewards(feeTokens[feeToken].rewards).queueNewRewards(_lockFeesIncentive); } + // transfer to the VE3D Locker and queue the new rewards if (_stakerLockFeesIncentive > 0) { IERC20Upgradeable(feeToken).safeTransfer(stakerLockRewards, _stakerLockFeesIncentive); IRewards(stakerLockRewards).queueNewRewards(feeToken, _stakerLockFeesIncentive); diff --git a/contracts/Interfaces/IStaker.sol b/contracts/Interfaces/IStaker.sol index 3a72230..8e6a84b 100644 --- a/contracts/Interfaces/IStaker.sol +++ b/contracts/Interfaces/IStaker.sol @@ -26,7 +26,7 @@ interface IStaker { function claimRewards(address) external; - function claimFees(address, address) external; + function claimFees(address, address, bytes calldata) external; function setStashAccess(address, bool) external; diff --git a/contracts/VestedEscrow.sol b/contracts/VestedEscrow.sol index 854d5f5..1825eb9 100644 --- a/contracts/VestedEscrow.sol +++ b/contracts/VestedEscrow.sol @@ -113,9 +113,11 @@ contract VestedEscrow is Ownable, ReentrancyGuard { if (delta != 0) { rewardToken.safeTransfer(owner(), delta); + initialLockedSupply = initialLockedSupply.sub(delta); } initialLocked[_recipient] = 0; + totalClaimed[_recipient] = 0; } function _totalVestedOf(address _recipient, uint256 _time) internal view returns (uint256) { diff --git a/contracts/VoterProxy.sol b/contracts/VoterProxy.sol index 4a3f337..f17a402 100644 --- a/contracts/VoterProxy.sol +++ b/contracts/VoterProxy.sol @@ -251,9 +251,17 @@ contract VoterProxy { return true; } - function claimFees(address _distroContract, address _token) external returns (uint256) { + // execute low level contract call on the distro contract and forward claimed rewards to Booster + function claimFees( + address _distroContract, + address _token, + bytes calldata executionData) + external returns (uint256) { + // enforce contract call comes from the correct source require(msg.sender == operator, "!auth"); - IFeeDistro(_distroContract).claim(); + // execute arbitrary enforced claim + (bool success, bytes memory result) = _distroContract.call(executionData); + require(success, "!fail"); uint256 _balance = IERC20(_token).balanceOf(address(this)); IERC20(_token).safeTransfer(operator, _balance); return _balance; @@ -275,4 +283,4 @@ contract VoterProxy { return (success, result); } -} +} \ No newline at end of file diff --git a/migrations/6_deploy_contracts_idle.js b/migrations/6_deploy_contracts_idle.js index 4e4a4f7..22391ef 100644 --- a/migrations/6_deploy_contracts_idle.js +++ b/migrations/6_deploy_contracts_idle.js @@ -3,7 +3,8 @@ const { addContract, getContract } = require("./helper/addContracts"); const escrowABI = require("./helper/escrowABI.json"); const { deployProxy } = require("@openzeppelin/truffle-upgrades"); -const VoterProxyV2 = artifacts.require("VoterProxyV2"); +// const VoterProxyV2 = artifacts.require("VoterProxyV2"); +const VoterProxy = artifacts.require("VoterProxy"); const VeTokenMinter = artifacts.require("VeTokenMinter"); const RewardFactory = artifacts.require("RewardFactory"); const VE3Token = artifacts.require("VE3Token"); @@ -26,7 +27,13 @@ function toBN(number) { module.exports = async function (deployer, network, accounts) { global.created = true; const contractList = getContract(); + let executionInterface = {"name":"claim","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"nonpayable","type":"function"}; + let executionData = web3.eth.abi.encodeFunctionCall(executionInterface, []); + let executionHash = web3.utils.keccak256(executionData); + // the commented line below also works for function calls requiring no parameters + // let executionData = web3.eth.abi.encodeFunctionSignature("claim()"); let smartWalletWhitelistAddress = "0x2D8b5b65c6464651403955aC6D71f9c0204169D3"; + let idleAddress = "0x875773784Af8135eA0ef43b5a374AaD105c5D39e"; let idle = await IERC20.at("0x875773784Af8135eA0ef43b5a374AaD105c5D39e"); let checkerAdmin = "0xFb3bD022D5DAcF95eE28a6B07825D4Ff9C5b3814"; let idleAdmin = "0xd6dabbc2b275114a2366555d6c481ef08fdc2556"; @@ -62,11 +69,7 @@ module.exports = async function (deployer, network, accounts) { await web3.eth.sendTransaction({ from: admin, to: stashRewardTokenUser, value: web3.utils.toWei("1") }); // voter proxy - const voter = await deployProxy( - VoterProxyV2, - ["idleVoterProxy", idle.address, stkIDLE, gaugeController, idleMintr, 3], - { deployer, initializer: "__VoterProxyV2_init" } - ); + const voter = await deployer.deploy(VoterProxy, "idleVoterProxy", idle.address, stkIDLE, gaugeController, idleMintr, 3); // whitelist the voter proxy const whitelist = await SmartWalletWhitelist.at(smartWalletWhitelistAddress); @@ -88,7 +91,7 @@ module.exports = async function (deployer, network, accounts) { // booster const booster = await deployProxy( Booster, - [voter.address, contractList.system.vetokenMinter, idle.address, feeDistro], + [voter.address, contractList.system.vetokenMinter, idle.address], { deployer, initializer: "__Booster_init" } ); @@ -157,7 +160,12 @@ module.exports = async function (deployer, network, accounts) { await booster.setFactories(rFactory.address, sFactory.address, tFactory.address), "booster setFactories" ); - logTransaction(await booster.setFeeInfo(toBN(10000), toBN(0)), "booster setFeeInfo"); + logTransaction(await booster.setFeeInfo( + toBN(10000), + toBN(0), + idleAddress, + feeDistro, + executionHash), "booster setFeeInfo"); //vetoken minter setup const vetokenMinter = await VeTokenMinter.at(contractList.system.vetokenMinter); logTransaction( diff --git a/test/booster-lp-short.js b/test/booster-lp-short.js index 424ac99..0c8d63d 100644 --- a/test/booster-lp-short.js +++ b/test/booster-lp-short.js @@ -39,7 +39,6 @@ contract("Booster LP Stake", async (accounts) => { let veassetToken; let ve3dLocker; let escrow; - let feeDistro; let lpToken; let voterProxy; let booster; @@ -57,6 +56,11 @@ contract("Booster LP Stake", async (accounts) => { let network; let uniExchangeRouterAddress = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; const sushiExchangeRouterAddress = "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F"; + let idleAddress = "0x875773784Af8135eA0ef43b5a374AaD105c5D39e"; + let feeDistro = "0xbabb82456c013fd7e3f25857e0729de8207f80e2"; + let executionInterface = {"name":"claim","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"nonpayable","type":"function"} + let executionData = web3.eth.abi.encodeFunctionCall(executionInterface, []); + let executionHash = web3.utils.keccak256(executionData); before("setup", async () => { network = await loadContracts(); @@ -77,7 +81,6 @@ contract("Booster LP Stake", async (accounts) => { ve3Token = await VE3Token.at(contractAddresseList[5]); veassetDepositer = await VeAssetDepositor.at(contractAddresseList[6]); ve3TokenRewardPool = await BaseRewardPool.at(contractAddresseList[7]); - feeDistro = await booster.feeDistro(); uniExchange = new web3.eth.Contract(uniswapV2Router, uniExchangeRouterAddress); sushiExchange = new web3.eth.Contract(uniswapV2Router, sushiExchangeRouterAddress); ve3dLocker = await VE3DLocker.at(baseContractList.system.ve3dLocker); @@ -829,7 +832,12 @@ contract("Booster LP Stake", async (accounts) => { // }); it("check setFeeInfo (try to set more than FEE_DENOMINATOR)", async () => { - await truffleAssert.reverts(booster.setFeeInfo(toBN(10001), toBN(0)), "status 0"); + await truffleAssert.reverts(booster.setFeeInfo( + toBN(10001), + toBN(0), + idleAddress, + feeDistro, + executionHash), "!fee"); // Seems not failing at all! }); diff --git a/test/booster.js b/test/booster.js index 3e06c52..07c4d29 100644 --- a/test/booster.js +++ b/test/booster.js @@ -50,6 +50,9 @@ contract("Booster", async (accounts) => { let feeToken; let stakerLockPool; let treasury; + let executionInterface; + let executionData; + let executionHash; const reverter = new Reverter(web3); const wei = web3.utils.toWei; const USER1 = accounts[0]; @@ -79,9 +82,14 @@ contract("Booster", async (accounts) => { ve3TokenRewardPool = await BaseRewardPool.at(contractAddresseList[7]); feeDistro = contractAddresseList[8]; feeDistroAdmin = contractAddresseList[9]; - feeToken = await IERC20.at(await booster.feeToken()); + feeToken = await IERC20.at(await booster.allFeeTokens(0)); treasury = accounts[2]; + // execution interface and data for earmarkFees + executionInterface = {"name":"claim","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"nonpayable","type":"function"}; + executionData = web3.eth.abi.encodeFunctionCall(executionInterface, []); + executionHash = web3.utils.keccak256(executionData); + await reverter.snapshot(); }); @@ -713,6 +721,13 @@ contract("Booster", async (accounts) => { it("earmarkFees full flow", async () => { await veassetDepositer.deposit(depositAmount, true, ve3TokenRewardPool.address); + await booster.setFeeInfo( + toBN(10000), + toBN(0), + feeToken.address, + feeDistro, + executionHash); + //increase time await time.increaseTo( toBN(await time.latest()) @@ -720,8 +735,13 @@ contract("Booster", async (accounts) => { .toString() ); await time.advanceBlock(); - - const lockFeesAddress = await booster.lockFees(); + const claimableToken = await booster.allFeeTokens(0); + // log("claimable token address : " + claimableToken.toString()); + // log("feeToken address : " + feeToken.address.toString()); + const feeTokenData = await booster.feeTokens(claimableToken); + const lockFeesAddress = feeTokenData[0]; + log("lockFees / distro address : " + lockFeesAddress.toString()); + log("feeTokenData is active : " + feeTokenData[3]); const lockFees = await VirtualBalanceRewardPool.at(lockFeesAddress); const rewardBalBefore = (await feeToken.balanceOf(lockFeesAddress)).toString(); @@ -735,7 +755,7 @@ contract("Booster", async (accounts) => { await feeDistroContract.methods.checkpoint_token().send({ from: feeDistroAdmin, gas: 8000000 }); // claim fee rewards - await booster.earmarkFees(); + await booster.earmarkFees(feeToken.address, executionData); const rewardBalAfter = (await feeToken.balanceOf(lockFeesAddress)).toString(); assert.isTrue(toBN(rewardBalAfter).gt(0)); @@ -784,7 +804,12 @@ contract("Booster", async (accounts) => { ); await time.advanceBlock(); - await booster.setFeeInfo(toBN(3000), toBN(7000)); + await booster.setFeeInfo( + toBN(3000), + toBN(7000), + feeToken.address, + feeDistro, + executionHash); assert.equal((await booster.lockFeesIncentive()).toString(), toBN(3000)); assert.equal((await booster.stakerLockFeesIncentive()).toString(), toBN(7000)); @@ -807,7 +832,7 @@ contract("Booster", async (accounts) => { console.log("distributes 30% of fee to the lock pool + 70% to xve3d reward pool"); // claim fee rewards - await booster.earmarkFees(); + await booster.earmarkFees(feeToken, executionData, 0); const lockFeesBalAfter = (await feeToken.balanceOf(lockFeesAddress)).toString(); log("lockFees reward pool balance before earmarkFees", formatUnits(lockFeesBalBefore, unit)); diff --git a/test/vestedEscrow.js b/test/vestedEscrow.js index 481dafa..f5742a3 100644 --- a/test/vestedEscrow.js +++ b/test/vestedEscrow.js @@ -360,6 +360,55 @@ contract("VestedEscrow", async (accounts) => { assert.equal(await ve3d.balanceOf(userA), claimed.toString()); assert.equal(adminBalanceDifference.toString(), toBN(amountUserA).minus(userABalanceAfter).toString()); }); + + it("it can fund a user again after cancel", async () => { + await vestedEscrow.cancel(userA, { from: admin }); + + assert.equal(await vestedEscrow.totalClaimed(userA), "0"); + + await time.increase("1000"); + + // fund again for the same user + await ve3d.approve(vestedEscrow.address, constants.MAX_UINT256, { + from: admin, + }); + await vestedEscrow.addTokens(totalAmount, { from: admin }); + await vestedEscrow.fund([userA], [amountUserA], { + from: admin, + }); + + await time.increase("2000"); + + await vestedEscrow.claim(userA, { + from: userA, + }); + + const currentTime = Number(await time.latest()); + const elapsed = currentTime - startTime; + + const claimed = toBN(amountUserA).times(elapsed).dividedToIntegerBy(TOTAL_TIME); + + assert.equal(await ve3d.balanceOf(userA), claimed.toString()); + }); + + it("it decreases the initialLockedSupply after cancel", async () => { + const initialLockedSupplyBefore = await vestedEscrow.initialLockedSupply(); + + await time.increase("2000"); + + await vestedEscrow.cancel(userA, { from: admin }); + + const currentTime = Number(await time.latest()); + const elapsed = currentTime - startTime; + + const claimed = toBN(amountUserA).times(elapsed).dividedToIntegerBy(TOTAL_TIME); + + const userADelta = toBN(amountUserA).minus(toBN(claimed)); + + const initialLockedSupplyAfter = await vestedEscrow.initialLockedSupply(); + + assert.equal(initialLockedSupplyAfter.toString(), toBN(initialLockedSupplyBefore).minus(userADelta).toString()); + }); }); describe("#overview", () => {