diff --git a/contracts/GaugeReward.sol b/contracts/GaugeReward.sol index 2d1155c3..f85edd43 100644 --- a/contracts/GaugeReward.sol +++ b/contracts/GaugeReward.sol @@ -13,8 +13,10 @@ import "./interfaces/IPrizePoolLiquidatorListener.sol"; /** * @title PoolTogether V4 GaugeReward * @author PoolTogether Inc Team - * @notice The GaugeReward contract handles the rewards for users + * @notice The GaugeReward contract handles rewards for users who staked in one or several gauges on the GaugeController contract. + * @dev This contract is only keeping track of the rewards. + Reward tokens are actually stored in the TokenVault contract. */ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { using SafeERC20 for IERC20; @@ -23,6 +25,7 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { /** * @notice Tracks user token reward balances + * @dev user => token => balance */ mapping(address => mapping(IERC20 => uint256)) public userTokenRewardBalances; @@ -70,7 +73,7 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { /// @notice Address of the liquidator that this contract is listening to address public liquidator; - /// @notice Percentage of rewards that goes to stakers. Fixed point 9 number this is less than 1. + /// @notice Percentage of rewards that goes to stakers. Fixed point 9 number that is less than 1. uint32 public stakerCut; /* ============ Events ============ */ @@ -79,6 +82,8 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { * @notice Emitted when the contract is deployed * @param gaugeController Address of the GaugeController * @param vault Address of the Vault + * @param liquidator Address of the Liquidator + * @param stakerCut Percentage of rewards that goes to stakers */ event Deployed( IGaugeController indexed gaugeController, @@ -88,16 +93,18 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { ); /** - * @notice Emitted when rewards token are added to a gauge - * @param gauge Address of the gauge for which the rewards are added - * @param token Address of the token being added - * @param amount Amount of tokens added to the gauge + * @notice Emitted when tickets are swapped for tokens + * @param gauge Address of the gauge for which tokens were added + * @param token Address of the token sent to the vault + * @param amount Amount of tokens sent to the vault + * @param stakerRewards Amount of rewards allocated to stakers * @param exchangeRate New exchange rate for this `token` in this `gauge` */ event RewardsAdded( address indexed gauge, IERC20 indexed token, uint256 amount, + uint256 stakerRewards, uint256 exchangeRate ); @@ -105,7 +112,7 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { * @notice Emitted when a user claimed their rewards for a given gauge and token * @param gauge Address of the gauge for which the user claimed rewards * @param token Address of the token for which the user claimed rewards - * @param user Address of the user who claimed rewards + * @param user Address of the user for which the rewards were claimed * @param amount Total amount of rewards claimed * @param exchangeRate Exchange rate at which the rewards were claimed */ @@ -117,6 +124,20 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { uint256 exchangeRate ); + /** + * @notice Emitted when a user redeemed their rewards for a given token + * @param caller Address who called the redeem function + * @param user Address of the user for which the rewards were redeemed + * @param token Address of the token for which the user redeemed rewards + * @param amount Total amount of rewards redeemed + */ + event RewardsRedeemed( + address indexed caller, + address indexed user, + IERC20 indexed token, + uint256 amount + ); + /** * @notice Emitted when a new reward token is pushed onto the `gaugeRewardTokens` mapping * @param gauge Address of the gauge for which the reward token is added @@ -131,6 +152,8 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { * @notice GaugeReward constructor * @param _gaugeController Address of the GaugeController * @param _vault Address of the Vault + * @param _liquidator Address of the Liquidator + * @param _stakerCut Percentage of rewards that goes to stakers */ constructor( IGaugeController _gaugeController, @@ -139,21 +162,16 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { uint32 _stakerCut ) { require(address(_gaugeController) != address(0), "GReward/GC-not-zero-address"); - require(address(_vault) != address(0), "GReward/Vault-not-zero-address"); + require(_vault != address(0), "GReward/Vault-not-zero-address"); + require(_liquidator != address(0), "GReward/Liq-not-zero-address"); require(_stakerCut < 1e9, "GReward/staker-cut-lt-1e9"); - require(_liquidator != address(0), "GReward/liq-not-zero-address"); gaugeController = _gaugeController; vault = _vault; stakerCut = _stakerCut; liquidator = _liquidator; - emit Deployed( - _gaugeController, - _vault, - _liquidator, - _stakerCut - ); + emit Deployed(_gaugeController, _vault, _liquidator, _stakerCut); } /* ============ External Functions ============ */ @@ -168,29 +186,36 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { } /** - * @notice Add rewards denominated in `token` for the given `gauge`. - * @dev Called by the liquidation contract anytime tokens are liquidated. - * @dev Will push token to the `gaugeRewardTokens` mapping if different from the current one. - * @param ticket The address of the tickets that were sold - * @param token The address of the token that the tickets were sold for - * @param tokenAmount The amount of tokens that the tickets were sold for + * @notice Records exchange rate after swapping an amount of `ticket` for `token`. + * @dev Called by the liquidator contract anytime tokens are liquidated. + * @dev Will push `token` to the `gaugeRewardTokens` mapping if different from the current one. + * @param _ticket Address of the tickets that were sold + * @param _token Address of the token that the tickets were sold for + * @param _tokenAmount Amount of tokens that the tickets were sold for */ - function afterSwap(IPrizePool, ITicket ticket, uint256, IERC20 token, uint256 tokenAmount) external override { + function afterSwap( + IPrizePool, + ITicket _ticket, + uint256, + IERC20 _token, + uint256 _tokenAmount + ) external override { require(msg.sender == liquidator, "GReward/only-liquidator"); - address gauge = address(ticket); - if (token != _currentRewardToken(gauge).token) { - _pushRewardToken(gauge, token); + address _gauge = address(_ticket); + + if (_token != _currentRewardToken(_gauge).token) { + _pushRewardToken(_gauge, _token); } - uint256 stakerRewards = (tokenAmount * stakerCut) / 1e9; + uint256 _gaugeRewards = (_tokenAmount * stakerCut) / 1e9; // Exchange rate = amount / current staked amount on gauge - uint256 _exchangeRate = (stakerRewards * 1e18) / gaugeController.getGaugeBalance(gauge); + uint256 _exchangeRate = (_gaugeRewards * 1e18) / gaugeController.getGaugeBalance(_gauge); - tokenGaugeExchangeRates[token][gauge] += _exchangeRate; + tokenGaugeExchangeRates[_token][_gauge] += _exchangeRate; - emit RewardsAdded(gauge, token, stakerRewards, _exchangeRate); + emit RewardsAdded(_gauge, _token, _tokenAmount, _gaugeRewards, _exchangeRate); } /// @inheritdoc IGaugeReward @@ -199,13 +224,7 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { address _user, uint256 _oldStakeBalance ) external override onlyGaugeController { - RewardToken memory _rewardToken = _claimPastRewards(_gauge, _user, _oldStakeBalance); - - if (address(_rewardToken.token) != address(0)) { - _claim(_gauge, _rewardToken.token, _user, _oldStakeBalance, false); - } - - userLastClaimedTimestamp[_user] = block.timestamp; + _claim(_gauge, _user, _oldStakeBalance); } /// @inheritdoc IGaugeReward @@ -214,43 +233,39 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { address _user, uint256 _oldStakeBalance ) external override onlyGaugeController { - RewardToken memory _rewardToken = _claimPastRewards(_gauge, _user, _oldStakeBalance); - if (_rewardToken.token != IERC20(address(0))) { - _claim(_gauge, _rewardToken.token, _user, _oldStakeBalance, false); - } - userLastClaimedTimestamp[_user] = block.timestamp; + _claim(_gauge, _user, _oldStakeBalance); } /** - * @notice Claim user rewards for a given gauge and token. + * @notice Claim user rewards for a given gauge. * @param _gauge Address of the gauge to claim rewards for - * @param _token Address of the token to claim rewards for * @param _user Address of the user to claim rewards for */ function claim( address _gauge, - IERC20 _token, address _user ) external { uint256 _stakeBalance = gaugeController.getUserGaugeBalance(_gauge, _user); - - _claimPastRewards(_gauge, _user, _stakeBalance); - - _claim(_gauge, _token, _user, _stakeBalance, false); - - userLastClaimedTimestamp[_user] = block.timestamp; + _claim(_gauge, _user, _stakeBalance); } - // function isPrizePoolLiquidator(address _prizePoolLiquidator) public view returns (bool) { - // return gaugeScaleTwabs[_prizePoolLiquidator].details.balance > 0; - // } + /** + * @notice Redeem user rewards for a given token. + * @dev Rewards can be redeemed on behalf of a user. + * @param _user Address of the user to redeem rewards for + * @param _token Address of the token to redeem rewards for + * @return Amount of rewards redeemed + */ + function redeem(address _user, IERC20 _token) external returns (uint256) { + uint256 _rewards = userTokenRewardBalances[_user][_token]; - /* ============ Modifiers ============ */ + userTokenRewardBalances[_user][_token] = 0; + _token.safeTransferFrom(vault, _user, _rewards); - // modifier requirePrizePoolLiquidator(address _prizePoolLiquidator) { - // require(isPrizePoolLiquidator(_prizePoolLiquidator), "GReward/caller-not-liquidator"); - // _; - // } + emit RewardsRedeemed(msg.sender, _user, _token, _rewards); + + return _rewards; + } /* ============ Internal Functions ============ */ @@ -276,9 +291,9 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { * @param _token Address of the token to claim rewards for * @param _user Address of the user to claim rewards for * @param _stakeBalance User stake balance - * @param _eligibleForPastRewards Whether this function is called in `_eligibleForPastRewards` or not + * @param _eligibleForPastRewards Whether user is eligible for past rewards or not */ - function _claim( + function _claimRewards( address _gauge, IERC20 _token, address _user, @@ -298,22 +313,19 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { // Record current exchange rate userTokenGaugeExchangeRates[_user][_token][_gauge] = _currentExchangeRate; - userTokenRewardBalances[_user][_token] += _rewards; + // Skip event and rewards accrual if rewards are equal to zero + if (_rewards > 0) { + userTokenRewardBalances[_user][_token] += _rewards; - emit RewardsClaimed(_gauge, _token, _user, _rewards, _currentExchangeRate); + emit RewardsClaimed(_gauge, _token, _user, _rewards, _currentExchangeRate); + } return _rewards; } - function redeem(address _user, IERC20 _token) external returns (uint256) { - uint256 rewards = userTokenRewardBalances[_user][_token]; - userTokenRewardBalances[_user][_token] = 0; - _token.safeTransferFrom(address(vault), _user, rewards); - return rewards; - } - /** * @notice Claim user past rewards for a given gauge. + * @dev Go through all the past reward tokens for the given gauge and claim rewards. * @param _gauge Address of the gauge to claim rewards for * @param _user Address of the user to claim rewards for * @param _stakeBalance User stake balance @@ -340,8 +352,11 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { _latestRewardToken = _rewardToken; } - if (_userLastClaimedTimestamp > 0 && _rewardToken.timestamp > _userLastClaimedTimestamp) { - _claim(_gauge, _rewardToken.token, _user, _stakeBalance, true); + if ( + _userLastClaimedTimestamp > 0 && + _rewardToken.timestamp > _userLastClaimedTimestamp + ) { + _claimRewards(_gauge, _rewardToken.token, _user, _stakeBalance, true); } else { break; } @@ -351,6 +366,26 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { return _latestRewardToken; } + /** + * @notice Claim user rewards for a given gauge. + * @param _gauge Address of the gauge to claim rewards for + * @param _user Address of the user to claim rewards for + * @param _stakeBalance User stake balance + */ + function _claim( + address _gauge, + address _user, + uint256 _stakeBalance + ) internal { + RewardToken memory _rewardToken = _claimPastRewards(_gauge, _user, _stakeBalance); + + if (address(_rewardToken.token) != address(0)) { + _claimRewards(_gauge, _rewardToken.token, _user, _stakeBalance, false); + } + + userLastClaimedTimestamp[_user] = block.timestamp; + } + /** * @notice Push a new reward token into the `gaugeRewardTokens` array * @param _gauge Address of the gauge to push reward token for @@ -366,8 +401,11 @@ contract GaugeReward is IGaugeReward, IPrizePoolLiquidatorListener, Multicall { emit RewardTokenPushed(_gauge, _token, _currentTimestamp); } + /* ============ Modifiers ============ */ + + /// @notice Restricts call to GaugeController contract modifier onlyGaugeController() { - require(msg.sender == address(gaugeController), "GReward/only-gc"); + require(msg.sender == address(gaugeController), "GReward/only-GaugeController"); _; } } diff --git a/contracts/TokenVault.sol b/contracts/TokenVault.sol index a7575c51..6ef30fdb 100644 --- a/contracts/TokenVault.sol +++ b/contracts/TokenVault.sol @@ -7,16 +7,16 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** - * @title PoolTogether Vault + * @title PoolTogether TokenVault * @author PoolTogether Inc Team */ -contract Vault is Manageable { +contract TokenVault is Manageable { using SafeERC20 for IERC20; mapping(address => bool) public approved; /** - * @notice Constructs Vault + * @notice Constructs TokenVault * @param _owner Owner address */ constructor(address _owner) Ownable(_owner) {} diff --git a/contracts/interfaces/IGaugeReward.sol b/contracts/interfaces/IGaugeReward.sol index 70569cd2..51669cd6 100644 --- a/contracts/interfaces/IGaugeReward.sol +++ b/contracts/interfaces/IGaugeReward.sol @@ -9,7 +9,7 @@ pragma solidity 0.8.6; */ interface IGaugeReward { /** - * @notice Fallback function to call in GaugeController after a user has increased their gauge stake. + * @notice Callback function to call in GaugeController after a user has increased their gauge stake. * @param gauge Address of the gauge to increase stake for * @param user Address of the user to increase stake for * @param oldStakeBalance Old stake balance of the user @@ -21,7 +21,7 @@ interface IGaugeReward { ) external; /** - * @notice Fallback function to call in GaugeController after a user has decreased his gauge stake. + * @notice Callback function to call in GaugeController after a user has decreased his gauge stake. * @param gauge Address of the gauge to decrease stake for * @param user Address of the user to decrease stake for * @param oldStakeBalance Old stake balance of the user diff --git a/test/GaugeController.test.ts b/test/GaugeController.test.ts index 215ddcc1..7d699e43 100644 --- a/test/GaugeController.test.ts +++ b/test/GaugeController.test.ts @@ -19,7 +19,6 @@ describe('GaugeController', () => { let GaugeControllerFactory: ContractFactory; let GaugeRewardArtifact: Artifact; let TokenFactory: ContractFactory; - let VaultFactory: ContractFactory; const gaugeAddress = '0x0000000000000000000000000000000000000001'; @@ -28,7 +27,6 @@ describe('GaugeController', () => { GaugeControllerFactory = await ethers.getContractFactory('GaugeController'); GaugeRewardArtifact = await artifacts.readArtifact('GaugeReward'); TokenFactory = await ethers.getContractFactory('ERC20Mintable'); - VaultFactory = await ethers.getContractFactory('Vault'); }); beforeEach(async () => { diff --git a/test/GaugeReward.test.ts b/test/GaugeReward.test.ts index e7e145f4..e83069d8 100644 --- a/test/GaugeReward.test.ts +++ b/test/GaugeReward.test.ts @@ -4,33 +4,56 @@ import { deployMockContract, MockContract } from 'ethereum-waffle'; import { BigNumber, Contract } from 'ethers'; import { ethers, artifacts } from 'hardhat'; -import { fillPrizeTiersWithZeros } from './helpers/fillPrizeTiersWithZeros'; - const { constants, getContractFactory, getSigners, provider, utils } = ethers; -const { AddressZero, MaxUint256 } = constants; +const { AddressZero, MaxUint256, Zero } = constants; const { parseEther: toWei, parseUnits } = utils; +// 10% cut +const stakerCut = parseUnits('0.1', 9); +const stakerCutInWei = toWei('0.1'); + +const gaugeRewardAmount = (swapAmount: BigNumber) => swapAmount.mul(stakerCutInWei).div(toWei('1')); + +const exchangeRate = (swapAmount: BigNumber, gaugeBalance: BigNumber) => + swapAmount.mul(stakerCutInWei).div(gaugeBalance); + +const userRewardAmount = ( + swapAmount: BigNumber, + gaugeBalance: BigNumber, + userStakeBalance: BigNumber, +) => { + const userShareOfStake = userStakeBalance.mul(toWei('100')).div(gaugeBalance); + + return gaugeRewardAmount(swapAmount).mul(userShareOfStake).div(toWei('100')); +}; + const deployGaugeReward = async ( gaugeControllerAddress: string, vaultAddress: string, - liquidatorAddress: string + liquidatorAddress: string, + stakerCutNumber = stakerCut, ): Promise => { const gaugeRewardFactory = await getContractFactory('GaugeReward'); - return await gaugeRewardFactory.deploy(gaugeControllerAddress, vaultAddress, liquidatorAddress, ethers.utils.parseUnits('0.1', 9)); + return await gaugeRewardFactory.deploy( + gaugeControllerAddress, + vaultAddress, + liquidatorAddress, + stakerCutNumber, // 10% staker cut, + ); }; describe('GaugeReward', () => { - let gauge: string; + let gaugeAddress: string; let gaugeReward: Contract; let gaugeController: MockContract; + let tokenVault: Contract; let poolToken: Contract; let usdcToken: Contract; - + let owner: SignerWithAddress; let wallet2: SignerWithAddress; let liquidator: SignerWithAddress; - let vault: SignerWithAddress; let constructorTest = false; @@ -38,20 +61,29 @@ describe('GaugeReward', () => { token: Contract, rewardsAmount: BigNumber, gaugeBalance: BigNumber, + caller = liquidator, ) => { - await token.mint(vault.address, rewardsAmount); - await token.approve(gaugeReward.address, MaxUint256); - await token.connect(vault).approve(gaugeReward.address, MaxUint256); + await token.mint(tokenVault.address, rewardsAmount); + + await tokenVault.setApproved(gaugeReward.address, true); + + const currentAllowance = await token.allowance(tokenVault.address, gaugeReward.address); + + if (currentAllowance.eq(Zero)) { + await tokenVault.increaseERC20Allowance(token.address, gaugeReward.address, MaxUint256); + } await gaugeController.mock.getGaugeBalance.returns(gaugeBalance); - return await gaugeReward.connect(liquidator).afterSwap(AddressZero, gauge, '0', token.address, rewardsAmount); + return await gaugeReward + .connect(caller) + .afterSwap(AddressZero, gaugeAddress, '0', token.address, rewardsAmount); }; beforeEach(async () => { - [owner, wallet2, liquidator, vault] = await getSigners(); + [owner, wallet2, liquidator] = await getSigners(); - gauge = '0xDe3825B1309E823D52C677E4981a1c67fF0d03E5'; + gaugeAddress = '0xDe3825B1309E823D52C677E4981a1c67fF0d03E5'; const ERC20MintableContract = await getContractFactory('ERC20Mintable', owner); poolToken = await ERC20MintableContract.deploy('PoolTogether', 'POOL'); @@ -60,8 +92,15 @@ describe('GaugeReward', () => { let gaugeControllerArtifact = await artifacts.readArtifact('GaugeController'); gaugeController = await deployMockContract(owner, gaugeControllerArtifact.abi); + let tokenVaultContract = await getContractFactory('TokenVault', owner); + tokenVault = await tokenVaultContract.deploy(owner.address); + if (!constructorTest) { - gaugeReward = await deployGaugeReward(gaugeController.address, vault.address, liquidator.address); + gaugeReward = await deployGaugeReward( + gaugeController.address, + tokenVault.address, + liquidator.address, + ); } }); @@ -75,22 +114,26 @@ describe('GaugeReward', () => { }); it('should deploy GaugeReward', async () => { - const gaugeReward = await deployGaugeReward(gaugeController.address, vault.address, liquidator.address); + const gaugeReward = await deployGaugeReward( + gaugeController.address, + tokenVault.address, + liquidator.address, + ); await expect(gaugeReward.deployTransaction) .to.emit(gaugeReward, 'Deployed') .withArgs( gaugeController.address, - vault.address, + tokenVault.address, liquidator.address, - ethers.utils.parseUnits('0.1', 9) + stakerCut, ); }); it('should fail if GaugeController is address zero', async () => { - await expect(deployGaugeReward(AddressZero, vault.address, liquidator.address)).to.be.revertedWith( - 'GReward/GC-not-zero-address', - ); + await expect( + deployGaugeReward(AddressZero, tokenVault.address, liquidator.address), + ).to.be.revertedWith('GReward/GC-not-zero-address'); }); it('should fail if Vault is address zero', async () => { @@ -98,6 +141,23 @@ describe('GaugeReward', () => { deployGaugeReward(gaugeController.address, AddressZero, liquidator.address), ).to.be.revertedWith('GReward/Vault-not-zero-address'); }); + + it('should fail if Liquidator is address zero', async () => { + await expect( + deployGaugeReward(gaugeController.address, tokenVault.address, AddressZero), + ).to.be.revertedWith('GReward/Liq-not-zero-address'); + }); + + it('should fail if staker cut is greater than 8 decimals', async () => { + await expect( + deployGaugeReward( + gaugeController.address, + tokenVault.address, + liquidator.address, + parseUnits('1', 9), + ), + ).to.be.revertedWith('GReward/staker-cut-lt-1e9'); + }); }); describe('gaugeController()', () => { @@ -107,16 +167,30 @@ describe('GaugeReward', () => { }); describe('afterSwap()', () => { - it('should add rewards', async () => { - const swapAmount = toWei('1000'); - const rewardAmount = swapAmount.div('10') // 10% cut - const gaugeBalance = toWei('100000'); - const exchangeRate = rewardAmount.mul(toWei('1')).div(gaugeBalance); + let swapAmount: BigNumber; + let gaugeBalance: BigNumber; + + beforeEach(() => { + swapAmount = toWei('1000'); + gaugeBalance = toWei('100000'); + }); - // cut is 10% + it('should add rewards', async () => { expect(await afterSwap(poolToken, swapAmount, gaugeBalance)) .to.emit(gaugeReward, 'RewardsAdded') - .withArgs(gauge, poolToken.address, rewardAmount, exchangeRate); + .withArgs( + gaugeAddress, + poolToken.address, + swapAmount, + gaugeRewardAmount(swapAmount), + exchangeRate(swapAmount, gaugeBalance), + ); + }); + + it('should fail if not liquidator', async () => { + await expect(afterSwap(poolToken, swapAmount, gaugeBalance, owner)).to.be.revertedWith( + 'GReward/only-liquidator', + ); }); }); @@ -130,13 +204,13 @@ describe('GaugeReward', () => { }); it('should return an empty struct if no reward token has been set yet', async () => { - const rewardToken = await gaugeReward.currentRewardToken(gauge); + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); expect(rewardToken.token).to.equal(AddressZero); expect(rewardToken.timestamp).to.equal(0); }); - it('should add rewards and return the newly added reward token', async () => { + it('should add rewards and return the newly pushed reward token', async () => { await gaugeController.mock.getGaugeBalance.returns(gaugeBalance); const afterSwapTx = await afterSwap(poolToken, rewardsAmount, gaugeBalance); @@ -145,55 +219,346 @@ describe('GaugeReward', () => { expect(afterSwapTx) .to.emit(gaugeReward, 'RewardTokenPushed') - .withArgs(gauge, poolToken.address, currentTimestamp); + .withArgs(gaugeAddress, poolToken.address, currentTimestamp); - const rewardToken = await gaugeReward.currentRewardToken(gauge); + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); expect(rewardToken.token).to.equal(poolToken.address); expect(rewardToken.timestamp).to.equal(currentTimestamp); }); - it('should add rewards twice and return the last added reward token', async () => { + it('should add rewards twice and return the last pushed reward token', async () => { await afterSwap(poolToken, rewardsAmount, gaugeBalance); await afterSwap(usdcToken, rewardsAmount, gaugeBalance); - const rewardToken = await gaugeReward.currentRewardToken(gauge); + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); const currentTimestamp = (await provider.getBlock('latest')).timestamp; expect(rewardToken.token).to.equal(usdcToken.address); expect(rewardToken.timestamp).to.equal(currentTimestamp); }); + + it('should add rewards twice and return the first reward token', async () => { + await afterSwap(poolToken, rewardsAmount, gaugeBalance); + + const firstSwapTimestamp = (await provider.getBlock('latest')).timestamp; + + await afterSwap(poolToken, rewardsAmount, gaugeBalance); + + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); + + expect(rewardToken.token).to.equal(poolToken.address); + expect(rewardToken.timestamp).to.equal(firstSwapTimestamp); + }); + }); + + describe('afterIncreaseGauge()', () => { + let swapAmount: BigNumber; + let userStakeBalance: BigNumber; + let gaugeBalance: BigNumber; + + beforeEach(() => { + swapAmount = toWei('1000'); + userStakeBalance = toWei('100'); + gaugeBalance = toWei('100000'); + }); + + it('should call afterIncreaseGauge', async () => { + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + const currentTimestamp = (await provider.getBlock('latest')).timestamp; + + expect(await gaugeReward.userLastClaimedTimestamp(owner.address)).to.equal( + currentTimestamp, + ); + + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); + const exchangeRate = await gaugeReward.tokenGaugeExchangeRates( + rewardToken.token, + gaugeAddress, + ); + + expect( + await gaugeReward.userTokenGaugeExchangeRates( + owner.address, + rewardToken.token, + gaugeAddress, + ), + ).to.equal(exchangeRate); + }); + + it('should claim rewards', async () => { + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + await afterSwap(poolToken, swapAmount, gaugeBalance); + + await gaugeController.mock.getUserGaugeBalance + .withArgs(gaugeAddress, owner.address) + .returns(userStakeBalance); + + expect( + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ), + ) + .to.emit(gaugeReward, 'RewardsClaimed') + .withArgs( + gaugeAddress, + poolToken.address, + owner.address, + userRewardAmount(swapAmount, gaugeBalance, userStakeBalance), + exchangeRate(swapAmount, gaugeBalance), + ); + }); + + it('should fail if not called by gaugeController', async () => { + await expect( + gaugeReward.afterIncreaseGauge(gaugeAddress, owner.address, userStakeBalance), + ).to.be.revertedWith('GReward/only-GaugeController'); + }); + }); + + describe('afterDecreaseGauge()', () => { + let swapAmount: BigNumber; + let userStakeBalance: BigNumber; + let gaugeBalance: BigNumber; + + beforeEach(() => { + swapAmount = toWei('1000'); + userStakeBalance = toWei('100'); + gaugeBalance = toWei('100000'); + }); + + it('should call afterDecreaseGauge', async () => { + await gaugeController.call( + gaugeReward, + 'afterDecreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + const currentTimestamp = (await provider.getBlock('latest')).timestamp; + + expect(await gaugeReward.userLastClaimedTimestamp(owner.address)).to.equal( + currentTimestamp, + ); + + const rewardToken = await gaugeReward.currentRewardToken(gaugeAddress); + const exchangeRate = await gaugeReward.tokenGaugeExchangeRates( + rewardToken.token, + gaugeAddress, + ); + + expect( + await gaugeReward.userTokenGaugeExchangeRates( + owner.address, + rewardToken.token, + gaugeAddress, + ), + ).to.equal(exchangeRate); + }); + + it('should claim rewards', async () => { + await gaugeController.call( + gaugeReward, + 'afterDecreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + await afterSwap(poolToken, swapAmount, gaugeBalance); + + await gaugeController.mock.getUserGaugeBalance + .withArgs(gaugeAddress, owner.address) + .returns(userStakeBalance); + + expect( + await gaugeController.call( + gaugeReward, + 'afterDecreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ), + ) + .to.emit(gaugeReward, 'RewardsClaimed') + .withArgs( + gaugeAddress, + poolToken.address, + owner.address, + userRewardAmount(swapAmount, gaugeBalance, userStakeBalance), + exchangeRate(swapAmount, gaugeBalance), + ); + }); + + it('should fail if not called by gaugeController', async () => { + await expect( + gaugeReward.afterDecreaseGauge(gaugeAddress, owner.address, userStakeBalance), + ).to.be.revertedWith('GReward/only-GaugeController'); + }); }); describe('claim()', () => { let swapAmount: BigNumber; - let rewardAmount: BigNumber; let userStakeBalance: BigNumber; let gaugeBalance: BigNumber; - let exchangeRate: BigNumber; beforeEach(() => { swapAmount = toWei('1000'); - rewardAmount = swapAmount.div('10') // 10% cut userStakeBalance = toWei('100'); gaugeBalance = toWei('100000'); - exchangeRate = rewardAmount.mul(toWei('1')).div(gaugeBalance); }); - it('should claim rewards ', async () => { + it('should claim rewards', async () => { + // Alice increases her stake + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + // Bob swaps tokens through the PrizePoolLiquidator + await afterSwap(poolToken, swapAmount, gaugeBalance); + + await gaugeController.mock.getUserGaugeBalance + .withArgs(gaugeAddress, owner.address) + .returns(userStakeBalance); - await gaugeController.call(gaugeReward, 'afterIncreaseGauge', gauge, owner.address, userStakeBalance) + // Alice claims her share of rewards earned from the swap + expect(await gaugeReward.claim(gaugeAddress, owner.address)) + .to.emit(gaugeReward, 'RewardsClaimed') + .withArgs( + gaugeAddress, + poolToken.address, + owner.address, + userRewardAmount(swapAmount, gaugeBalance, userStakeBalance), + exchangeRate(swapAmount, gaugeBalance), + ); + }); + + it('should claim past rewards', async () => { + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); await afterSwap(poolToken, swapAmount, gaugeBalance); - + await afterSwap(usdcToken, swapAmount, gaugeBalance); + await gaugeController.mock.getUserGaugeBalance - .withArgs(gauge, owner.address) + .withArgs(gaugeAddress, owner.address) .returns(userStakeBalance); - const claimTx = await gaugeReward.claim(gauge, poolToken.address, owner.address); + const claimTx = await gaugeReward.claim(gaugeAddress, owner.address); + expect(claimTx) .to.emit(gaugeReward, 'RewardsClaimed') - .withArgs(gauge, poolToken.address, owner.address, rewardAmount.div('1000'), exchangeRate); + .withArgs( + gaugeAddress, + poolToken.address, + owner.address, + userRewardAmount(swapAmount, gaugeBalance, userStakeBalance), + exchangeRate(swapAmount, gaugeBalance), + ); + + expect(claimTx) + .to.emit(gaugeReward, 'RewardsClaimed') + .withArgs( + gaugeAddress, + usdcToken.address, + owner.address, + userRewardAmount(swapAmount, gaugeBalance, userStakeBalance), + exchangeRate(swapAmount, gaugeBalance), + ); + }); + + it('should not be eligible to claim past rewards', async () => { + await afterSwap(poolToken, swapAmount, gaugeBalance); + + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + await gaugeController.mock.getUserGaugeBalance + .withArgs(gaugeAddress, owner.address) + .returns(userStakeBalance); + + expect(await gaugeReward.claim(gaugeAddress, owner.address)) + .to.not.emit(gaugeReward, 'RewardsClaimed') + .withArgs( + gaugeAddress, + poolToken.address, + owner.address, + Zero, + exchangeRate(swapAmount, gaugeBalance), + ); + }); + }); + + describe('redeem()', () => { + let swapAmount: BigNumber; + let userStakeBalance: BigNumber; + let gaugeBalance: BigNumber; + + beforeEach(() => { + swapAmount = toWei('1000'); + userStakeBalance = toWei('100'); + gaugeBalance = toWei('100000'); + }); + + it('should redeem accumulated rewards', async () => { + await gaugeController.call( + gaugeReward, + 'afterIncreaseGauge', + gaugeAddress, + owner.address, + userStakeBalance, + ); + + await afterSwap(poolToken, swapAmount, gaugeBalance); + + await gaugeController.mock.getUserGaugeBalance + .withArgs(gaugeAddress, owner.address) + .returns(userStakeBalance); + + await gaugeReward.claim(gaugeAddress, owner.address); + + const rewardToken = (await gaugeReward.currentRewardToken(gaugeAddress)).token; + const gaugeRewardAmount = await gaugeReward.userTokenRewardBalances( + owner.address, + rewardToken, + ); + + expect(await gaugeReward.redeem(owner.address, rewardToken)) + .to.emit(gaugeReward, 'RewardsRedeemed') + .withArgs(owner.address, owner.address, rewardToken, gaugeRewardAmount); }); }); }); diff --git a/test/TokenVault.test.ts b/test/TokenVault.test.ts index 7dbf2974..ec00286c 100644 --- a/test/TokenVault.test.ts +++ b/test/TokenVault.test.ts @@ -4,14 +4,10 @@ import { deployMockContract } from 'ethereum-waffle'; import { Contract, Signer } from 'ethers'; import { ethers, artifacts } from 'hardhat'; -const newDebug = require('debug'); - -const debug = newDebug('pt:Vault.test.ts'); - const { getSigners } = ethers; describe('Vault', () => { - let vault: Contract; + let tokenVault: Contract; let token: Contract; let wallet1: SignerWithAddress; @@ -25,52 +21,52 @@ describe('Vault', () => { const IERC20Artifact = await artifacts.readArtifact('IERC20'); token = await deployMockContract(wallet1 as Signer, IERC20Artifact.abi); - const VaultFactory = await ethers.getContractFactory('Vault', wallet1); - vault = await VaultFactory.deploy(wallet1.address); + const TokenVaultFactory = await ethers.getContractFactory('TokenVault', wallet1); + tokenVault = await TokenVaultFactory.deploy(wallet1.address); }); describe('constructor', () => { it('should set the owner', async () => { - expect(await vault.owner()).to.equal(wallet1.address) + expect(await tokenVault.owner()).to.equal(wallet1.address) }) }) describe('setApproval()', () => { it('should allow the owner to approve accounts', async () => { - await vault.setApproved(wallet2.address, true) - expect(await vault.approved(wallet2.address)).to.equal(true) + await tokenVault.setApproved(wallet2.address, true) + expect(await tokenVault.approved(wallet2.address)).to.equal(true) }) }) describe('increaseERC20Allowance()', () => { it('should allow owners to increase approval amount', async () => { - await vault.setApproved(wallet2.address, true) - await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('0') + await tokenVault.setApproved(wallet2.address, true) + await token.mock.allowance.withArgs(tokenVault.address, wallet2.address).returns('0') await token.mock.approve.withArgs(wallet2.address, '1111').returns(true) - await vault.increaseERC20Allowance(token.address, wallet2.address, '1111') + await tokenVault.increaseERC20Allowance(token.address, wallet2.address, '1111') }) it('should allow managers to increase approval amount', async () => { - await vault.setManager(wallet3.address) - await vault.setApproved(wallet2.address, true) - await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('0') + await tokenVault.setManager(wallet3.address) + await tokenVault.setApproved(wallet2.address, true) + await token.mock.allowance.withArgs(tokenVault.address, wallet2.address).returns('0') await token.mock.approve.withArgs(wallet2.address, '1111').returns(true) - await vault.connect(wallet3).increaseERC20Allowance(token.address, wallet2.address, '1111') + await tokenVault.connect(wallet3).increaseERC20Allowance(token.address, wallet2.address, '1111') }) }) describe('decreaseERC20Allowance()', () => { it('should allow manager to decrease approval amount', async () => { - await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('1111') + await token.mock.allowance.withArgs(tokenVault.address, wallet2.address).returns('1111') await token.mock.approve.withArgs(wallet2.address, '111').returns(true) - await vault.decreaseERC20Allowance(token.address, wallet2.address, '1000') + await tokenVault.decreaseERC20Allowance(token.address, wallet2.address, '1000') }) it('should allow manager to decrease approval amount', async () => { - await vault.setManager(wallet3.address) - await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('1111') + await tokenVault.setManager(wallet3.address) + await token.mock.allowance.withArgs(tokenVault.address, wallet2.address).returns('1111') await token.mock.approve.withArgs(wallet2.address, '0').returns(true) - await vault.connect(wallet3).decreaseERC20Allowance(token.address, wallet2.address, '1111') + await tokenVault.connect(wallet3).decreaseERC20Allowance(token.address, wallet2.address, '1111') }) }) });