From d23a301ef1b9bfbd1f0aa95ba4a6cb1fa06b80e6 Mon Sep 17 00:00:00 2001 From: Kevin Huo Date: Wed, 30 Nov 2022 17:43:39 -0500 Subject: [PATCH 1/3] initial fraxbp modifications --- .../fraxBPInitModule/FraxBPInitModule.sol | 41 +++- .../fraxBPInitModule/FraxBPInitModuleAMO.sol | 221 ++++++++++++++++++ .../interfaces/ICurveGauge.sol | 20 ++ .../interfaces/ICurveGaugeController.sol | 15 ++ .../fraxBPInitModule/fraxBPInitModule.t.sol | 28 ++- 5 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol create mode 100644 src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol create mode 100644 src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol diff --git a/src/modules/fraxBPInitModule/FraxBPInitModule.sol b/src/modules/fraxBPInitModule/FraxBPInitModule.sol index 457f6a3..8d3b8eb 100644 --- a/src/modules/fraxBPInitModule/FraxBPInitModule.sol +++ b/src/modules/fraxBPInitModule/FraxBPInitModule.sol @@ -10,6 +10,7 @@ import "@protocol/interfaces/IPHO.sol"; import "@protocol/interfaces/IModuleManager.sol"; import "@external/curve/ICurvePool.sol"; import "@oracle/IPriceOracle.sol"; +import "./FraxBPInitModuleAMO.sol"; /// @title FraxBP Init module /// @author Ekonomia: https://github.com/ekonomia-tech @@ -44,6 +45,9 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { uint256 public saleEndDate; // when sale ends uint256 public redemptionStartDate; // when redemptions are available IPHO public pho; + address public fraxBPInitModuleAMO; + address public stakingToken = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; // LUSD + address rewardToken = 0xD533a949740bb3306d119CC777fa900bA034cd52; // CRV mapping(address => uint256) public metapoolBalance; @@ -79,6 +83,21 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { usdc.approve(address(fraxBPPool), type(uint256).max); frax.approve(address(fraxBPPool), type(uint256).max); + + FraxBPInitModuleAMO fraxBPModuleAMO = new FraxBPInitModuleAMO( + "FRAXBPPHO Module AMO", + "FBPPHO-AMO", + stakingToken, + rewardToken, + msg.sender, + address(this), + address(fraxBPPHOMetapool), + address(fraxBPPHOMetapool) + ); + + fraxBPInitModuleAMO = address(fraxBPModuleAMO); + + IERC20(_fraxBPPHOMetapool).approve(fraxBPInitModuleAMO, type(uint256).max); } /// @notice Helper for user depositing both FRAX and USDC @@ -112,27 +131,29 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { // call _depositFor() for user based on FraxBP LP received uint256 fraxBPLpAmount = fraxBPLpBalanceAfter - fraxBPLpBalanceBefore; - _depositFor(msg.sender, fraxBPLpAmount); + uint256 fraxBPPHOLpBalanceIssued = _depositFor(msg.sender, fraxBPLpAmount); + IModuleAMO(fraxBPInitModuleAMO).stakeFor(msg.sender, fraxBPPHOLpBalanceIssued); } /// @notice Helper function for deposits in FraxBP LP token for user /// @param depositor Depositor /// @param amount Amount in FraxBP LP - function _depositFor(address depositor, uint256 amount) private { + function _depositFor(address depositor, uint256 amount) private returns (uint256) { uint256 usdPerFraxBP = getUSDPerFraxBP(); uint256 phoAmount = (usdPerFraxBP * amount) / 10 ** 18; moduleManager.mintPHO(address(this), phoAmount); - uint256 fraxBPPHOLpBalance = _addFraxBPPHOLiquidity(amount, phoAmount); + uint256 fraxBPPHOLpBalanceIssued = _addFraxBPPHOLiquidity(amount, phoAmount); - metapoolBalance[depositor] += fraxBPPHOLpBalance; + metapoolBalance[depositor] += fraxBPPHOLpBalanceIssued; emit Deposited(depositor, amount, phoAmount); + return fraxBPPHOLpBalanceIssued; } /// @notice Places deposits in FraxBP LP token for user /// @param depositor Depositor /// @param amount Amount in FraxBP LP - function depositFor(address depositor, uint256 amount) public { + function depositFor(address depositor, uint256 amount) public returns (uint256) { if (block.timestamp > saleEndDate) { revert CannotDepositAfterSaleEnded(); } @@ -140,13 +161,15 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { revert CannotDepositZero(); } fraxBPLp.safeTransferFrom(depositor, address(this), amount); - _depositFor(depositor, amount); + uint256 fraxBPPHOLPNetBalance = _depositFor(depositor, amount); + return fraxBPPHOLPNetBalance; } /// @notice Accept deposits in FraxBP LP token /// @param amount Amount in FraxBP LP function deposit(uint256 amount) external nonReentrant { - depositFor(msg.sender, amount); + uint256 fraxBPPHOLpBalanceIssued = depositFor(msg.sender, amount); + IModuleAMO(fraxBPInitModuleAMO).stakeFor(msg.sender, fraxBPPHOLpBalanceIssued); } /// @notice Adds FraxBP LP and PHO to FraxBP/PHO pool @@ -185,7 +208,9 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { } delete metapoolBalance[msg.sender]; - fraxBPPHOMetapool.transfer(msg.sender, redeemAmount / 2); + // Note: Always a full withdrawal + IModuleAMO(fraxBPInitModuleAMO).withdrawAllFor(msg.sender); + //fraxBPPHOMetapool.transfer(msg.sender, redeemAmount / 2); emit Redeemed(msg.sender, redeemAmount); } diff --git a/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol new file mode 100644 index 0000000..63a55f8 --- /dev/null +++ b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../interfaces/IModuleAMO.sol"; +import "./interfaces/ICurveGauge.sol"; +import "forge-std/console2.sol"; + +/// @title FraxBPInitModuleAMO +/// @notice FraxBPInit Module AMO +/// @author Ekonomia: https://github.com/Ekonomia +contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // Errors + error ZeroAddressDetected(); + error CannotStakeZero(); + error CannotWithdrawMoreThanDeposited(); + + /// State vars + address public rewardToken; + address public stakingToken; + address public operator; + address public module; + uint256 private _totalDeposits; + uint256 private _totalShares; + uint256 private _totalRewards; // rewards in CRV + + mapping(address => uint256) public depositedAmount; // MPL deposited + mapping(address => uint256) public stakedAmount; // MPL staked + mapping(address => uint256) public claimedRewards; // rewards claimed + mapping(address => uint256) private _shares; + + // Needed for interactions w/ external contracts + address public depositToken; + IERC20 public crv = IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); + ICurveGauge public curveGauge; // TODO: gauge address + + // Events + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount, uint256 shares); + event Withdrawn(address indexed user, uint256 amount, uint256 shares); + event RewardPaid(address indexed user, uint256 reward); + event FraxBPInitRewardsReceived(uint256 totalRewards); + + modifier onlyOperator() { + require(msg.sender == address(operator), "Only Operator"); + _; + } + + modifier onlyModule() { + require(msg.sender == address(module), "Only Module"); + _; + } + + /// Constructor + constructor( + string memory _name, + string memory _symbol, + address _stakingToken, + address _rewardToken, + address _operator, + address _module, + address _depositToken, + address _curveGauge + ) ERC20(_name, _symbol) { + if ( + _stakingToken == address(0) || _rewardToken == address(0) || _operator == address(0) + || _module == address(0) || _depositToken == address(0) || _curveGauge == address(0) + ) { + revert ZeroAddressDetected(); + } + stakingToken = _stakingToken; + rewardToken = _rewardToken; + operator = _operator; + module = _module; + depositToken = _depositToken; + curveGauge = ICurveGauge(_curveGauge); + } + + /// @notice Get total shares + function totalShares() public view returns (uint256) { + return _totalShares; + } + + /// @notice Get shares of account + /// @param account Account + function sharesOf(address account) public view returns (uint256) { + return _shares[account]; + } + + /// @notice Get earned amount by account (total available - claimed) + /// @param account Account + function earned(address account) public view returns (uint256) { + uint256 ts = totalSupply(); + if (ts == 0) { + return 0; + } + uint256 earnedRewards = (balanceOf(account) * _totalRewards) / ts - claimedRewards[account]; + return earnedRewards; + } + + /// @notice Convert a deposit/withdraw amount into shares + function _toShares(uint256 amount) private view returns (uint256) { + if (_totalShares == 0) { + return amount; + } + return (amount * _totalShares) / _totalDeposits; + } + + /// @notice Tracks shares for deposits + function _trackDepositShares(address account, uint256 amount) private returns (uint256) { + uint256 shares = _toShares(amount); + _shares[account] += shares; + _totalShares += shares; + _mint(account, shares); + return shares; + } + + /// @notice Tracks shares for withdrawals + function _trackWithdrawShares(address account) private returns (uint256) { + uint256 shares = _shares[account]; + _shares[account] = 0; + _totalShares -= shares; + _burn(account, shares); + return shares; + } + + /// @notice Stake for + /// @param account For + /// @param amount Amount + function stakeFor(address account, uint256 amount) public onlyModule returns (bool) { + if (amount == 0) { + revert CannotStakeZero(); + } + + // Get depositToken from module + IERC20(depositToken).safeTransferFrom(module, address(this), amount); + + // Deposit to gauge + //curveGauge.deposit(amount); + + depositedAmount[account] += amount; + stakedAmount[account] += amount; + _totalDeposits += amount; + + uint256 shares = _trackDepositShares(account, amount); + emit Staked(account, amount, shares); + return true; + } + + /// @notice Withdraw + /// @param account Account + /// @param amount amount + function withdrawFor(address account, uint256 amount) public onlyModule returns (bool) { + uint256 depositAmount = depositedAmount[account]; + uint256 stakedPoolTokenAmount = stakedAmount[account]; + if (amount > depositAmount) { + revert CannotWithdrawMoreThanDeposited(); + } + depositedAmount[account] -= depositAmount; + stakedAmount[account] -= stakedPoolTokenAmount; + + // Withdraw from gauge + //curveGauge.withdraw(amount); + + uint256 shares = _trackWithdrawShares(account); + _totalDeposits -= depositAmount; + + console2.log( + "this is in withdrawFor, this is balance of depositToken: ", + IERC20(depositToken).balanceOf(address(this)) + ); + + // Transfer depositToken to caller + IERC20(depositToken).transfer(account, depositAmount / 2); + emit Withdrawn(account, amount, shares); + return true; + } + + /// @notice Withdraw all for + /// @param account Account + function withdrawAllFor(address account) external returns (bool) { + return withdrawFor(account, depositedAmount[account]); + } + + /// @notice Gets reward + function getRewardFraxBPInit() external onlyOperator returns (uint256) { + uint256 crvBalanceBefore = crv.balanceOf(address(this)); + // Get rewards + curveGauge.claim_rewards(); + uint256 crvBalanceAfter = crv.balanceOf(address(this)); + _totalRewards = crvBalanceAfter - crvBalanceBefore; + emit FraxBPInitRewardsReceived(_totalRewards); + return _totalRewards; + } + + /// @notice Get reward + /// @param account Account + function getReward(address account) public returns (bool) { + uint256 reward = earned(account); + if (reward > 0) { + claimedRewards[account] += reward; + IERC20(rewardToken).safeTransfer(account, reward); + emit RewardPaid(account, reward); + } + return true; + } + + /// @notice Get reward for msg.sender + function getReward() external returns (bool) { + getReward(msg.sender); + return true; + } +} diff --git a/src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol b/src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol new file mode 100644 index 0000000..594bc12 --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +// Inspired from: https://github.com/convex-eth/platform/blob/ecea24d8ff6eb850134573e80cfc795d26805b76/contracts/contracts/Interfaces.sol +interface ICurveGauge { + function balanceOf(address) external view returns (uint256); + + function claim_rewards() external; + + function deposit(uint256 _amount) external; + + function withdraw(uint256 _amount) external; + + function lp_token() external view returns (address); + + function reward_tokens(uint256 index) external view returns (address); + + function claimable_reward(address, address) external view returns (uint256); +} diff --git a/src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol b/src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol new file mode 100644 index 0000000..df2eb63 --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +// Inspired from: https://github.com/convex-eth/platform/blob/ecea24d8ff6eb850134573e80cfc795d26805b76/contracts/contracts/interfaces/IGaugeController.sol + +interface ICurveGaugeController { + function get_gauge_weight(address _gauge) external view returns (uint256); + + function vote_user_slopes(address, address) external view returns (uint256, uint256, uint256); //slope,power,end + + function vote_for_gauge_weights(address, uint256) external; + + function add_gauge(address, int128, uint256) external; +} diff --git a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol index d3578be..9b3eaff 100644 --- a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol +++ b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol @@ -102,6 +102,10 @@ contract FraxBPInitModuleTest is BaseSetup { frax.approve(address(fraxBPInitModule), ONE_MILLION_D18); fraxBPLP.approve(address(fraxBPInitModule), ONE_MILLION_D18); vm.stopPrank(); + + // Do same for maple AMO + vm.prank(user1); + fraxBPPHOMetapool.approve(address(fraxBPInitModule.fraxBPInitModuleAMO()), TEN_THOUSAND_D18); } // Cannot set addresses to 0 @@ -277,11 +281,13 @@ contract FraxBPInitModuleTest is BaseSetup { assertEq(aft.modulePHOBalance, before.modulePHOBalance); assertEq(aft.moduleUSDCBalance, before.moduleUSDCBalance); assertEq(aft.moduleFRAXBalance, before.moduleFRAXBalance); - assertApproxEqAbs( - aft.moduleFraxBPPHOLPBalance, - before.moduleFraxBPPHOLPBalance + _expectedMintAmount, - deltaThreshold * 10 ** 18 - ); + + // TODO: modify -> balance same b/c goes to AMO + // assertApproxEqAbs( + // aft.moduleFraxBPPHOLPBalance, + // before.moduleFraxBPPHOLPBalance + _expectedMintAmount, + // deltaThreshold * 10**18 + // ); // Check issued amount goes up assertApproxEqAbs( @@ -409,11 +415,13 @@ contract FraxBPInitModuleTest is BaseSetup { assertEq(aft.userFRAXBalance, before.userFRAXBalance); // Frax BP Init module balance - FraxBPPHO LP down, FraxBPLP and PHO same, USDC & FRAX same - assertApproxEqAbs( - aft.moduleFraxBPPHOLPBalance, - before.moduleFraxBPPHOLPBalance - before.userMetapoolBalance / 2, - deltaThreshold * 10 ** 18 - ); + + // TODO: modify + // assertApproxEqAbs( + // aft.moduleFraxBPPHOLPBalance, + // before.moduleFraxBPPHOLPBalance - before.userMetapoolBalance / 2, + // deltaThreshold * 10**18 + // ); assertEq(aft.moduleFraxBPLPBalance, before.moduleFraxBPLPBalance); assertEq(aft.modulePHOBalance, before.modulePHOBalance); assertEq(aft.moduleUSDCBalance, before.moduleUSDCBalance); From 9301e0d2c7ea6c4ed5685f6e97e80f027c8cb3c7 Mon Sep 17 00:00:00 2001 From: Kevin Huo Date: Thu, 1 Dec 2022 21:10:30 -0500 Subject: [PATCH 2/3] add wip testing with vyper --- src/external/curve/LiquidityGaugeReward.vy | 442 ++++++++++++++++++ .../fraxBPInitModule/FraxBPInitModuleAMO.sol | 12 +- .../fraxBPInitModule/LiquidityGauge.sol | 363 ++++++++++++++ .../{ICurveGauge.sol => IGauge.sol} | 8 +- ...ugeController.sol => IGaugeController.sol} | 10 +- .../fraxBPInitModule/interfaces/IMinter.sol | 16 + .../interfaces/IOwnership.sol | 13 + .../fraxBPInitModule/interfaces/IToken.sol | 15 + .../interfaces/IVotingEscrow.sol | 18 + .../fraxBPInitModule/fraxBPInitModule.t.sol | 74 ++- test/utils/VyperDeployer.sol | 78 ++++ 11 files changed, 1037 insertions(+), 12 deletions(-) create mode 100644 src/external/curve/LiquidityGaugeReward.vy create mode 100644 src/modules/fraxBPInitModule/LiquidityGauge.sol rename src/modules/fraxBPInitModule/interfaces/{ICurveGauge.sol => IGauge.sol} (66%) rename src/modules/fraxBPInitModule/interfaces/{ICurveGaugeController.sol => IGaugeController.sol} (63%) create mode 100644 src/modules/fraxBPInitModule/interfaces/IMinter.sol create mode 100644 src/modules/fraxBPInitModule/interfaces/IOwnership.sol create mode 100644 src/modules/fraxBPInitModule/interfaces/IToken.sol create mode 100644 src/modules/fraxBPInitModule/interfaces/IVotingEscrow.sol create mode 100644 test/utils/VyperDeployer.sol diff --git a/src/external/curve/LiquidityGaugeReward.vy b/src/external/curve/LiquidityGaugeReward.vy new file mode 100644 index 0000000..7bacd10 --- /dev/null +++ b/src/external/curve/LiquidityGaugeReward.vy @@ -0,0 +1,442 @@ +# @version 0.3.8 +""" +@title Staking Liquidity Gauge +@author Curve Finance +@license MIT +@notice Simultaneously stakes using Synthetix (== YFI) rewards contract +""" + +from vyper.interfaces import ERC20 + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + +interface CurveRewards: + def stake(amount: uint256): nonpayable + def withdraw(amount: uint256): nonpayable + def getReward(): nonpayable + def earned(addr: address) -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + + +TOKENLESS_PRODUCTION: constant(uint256) = 40 +BOOST_WARMUP: constant(uint256) = 2 * 7 * 86400 +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +voting_escrow: public(address) +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +future_epoch_time: public(uint256) + +# caller -> recipient -> can deposit? +approved_to_deposit: public(HashMap[address, HashMap[address, bool]]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) +period_timestamp: public(uint256[100000000000000000000000000000]) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +inflation_rate: public(uint256) + +# For tracking external rewards +reward_contract: public(address) +rewarded_token: public(address) + +reward_integral: public(uint256) +reward_integral_for: public(HashMap[address, uint256]) +rewards_for: public(HashMap[address, uint256]) +claimed_rewards_for: public(HashMap[address, uint256]) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +is_killed: public(bool) +is_claiming_rewards: public(bool) + +@external +def __init__(lp_addr: address, _minter: address, _reward_contract: address, _rewarded_token: address, _admin: address): + """ + @notice Contract constructor + @param lp_addr Liquidity Pool contract address + @param _minter Minter contract address + @param _reward_contract Synthetix reward contract address + @param _rewarded_token Received synthetix token contract address + @param _admin Admin who can kill the gauge + """ + assert lp_addr != ZERO_ADDRESS + assert _minter != ZERO_ADDRESS + assert _reward_contract != ZERO_ADDRESS + + self.lp_token = lp_addr + self.minter = _minter + crv_addr: address = Minter(_minter).token() + self.crv_token = crv_addr + controller_addr: address = Minter(_minter).controller() + self.controller = controller_addr + self.voting_escrow = Controller(controller_addr).voting_escrow() + self.period_timestamp[0] = block.timestamp + self.inflation_rate = CRV20(crv_addr).rate() + self.future_epoch_time = CRV20(crv_addr).future_epoch_time_write() + self.reward_contract = _reward_contract + assert ERC20(lp_addr).approve(_reward_contract, MAX_UINT256) + self.rewarded_token = _rewarded_token + self.admin = _admin + self.is_claiming_rewards = True + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + _voting_escrow: address = self.voting_escrow + voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr) + voting_total: uint256 = ERC20(_voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if (voting_total > 0) and (block.timestamp > self.period_timestamp[0] + BOOST_WARMUP): + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_rewards(addr: address, claim_rewards: bool): + # Update reward integrals (no gauge weights involved: easy) + _rewarded_token: address = self.rewarded_token + + d_reward: uint256 = 0 + if claim_rewards: + d_reward = ERC20(_rewarded_token).balanceOf(self) + CurveRewards(self.reward_contract).getReward() + d_reward = ERC20(_rewarded_token).balanceOf(self) - d_reward + + user_balance: uint256 = self.balanceOf[addr] + total_balance: uint256 = self.totalSupply + dI: uint256 = 0 + if total_balance > 0: + dI = 10 ** 18 * d_reward / total_balance + I: uint256 = self.reward_integral + dI + self.reward_integral = I + self.rewards_for[addr] += user_balance * (I - self.reward_integral_for[addr]) / 10 ** 18 + self.reward_integral_for[addr] = I + + +@internal +def _checkpoint(addr: address, claim_rewards: bool): + """ + @notice Checkpoint for a user + @param addr User address + """ + _token: address = self.crv_token + _controller: address = self.controller + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch >= _period_time: + self.future_epoch_time = CRV20(_token).future_epoch_time_write() + new_rate = CRV20(_token).rate() + self.inflation_rate = new_rate + Controller(_controller).checkpoint_gauge(self) + + _working_balance: uint256 = self.working_balances[addr] + _working_supply: uint256 = self.working_supply + + if self.is_killed: + rate = 0 # Stop distributing inflation as soon as killed + + # Update integral of 1/supply + if block.timestamp > _period_time: + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + self._checkpoint_rewards(addr, claim_rewards) + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized + self._checkpoint(addr, self.is_claiming_rewards) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr, True) + return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self) + + +@external +@view +def claimable_reward(addr: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @param addr Account to get reward amount for + @return uint256 Claimable reward token amount + """ + d_reward: uint256 = CurveRewards(self.reward_contract).earned(self) + + user_balance: uint256 = self.balanceOf[addr] + total_balance: uint256 = self.totalSupply + dI: uint256 = 0 + if total_balance > 0: + dI = 10 ** 18 * d_reward / total_balance + I: uint256 = self.reward_integral + dI + + return self.rewards_for[addr] + user_balance * (I - self.reward_integral_for[addr]) / 10 ** 18 + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + _voting_escrow: address = self.voting_escrow + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts( + addr, VotingEscrow(_voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr, self.is_claiming_rewards) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +@external +def set_approve_deposit(addr: address, can_deposit: bool): + """ + @notice Set whether `addr` can deposit tokens for `msg.sender` + @param addr Address to set approval on + @param can_deposit bool - can this account deposit for `msg.sender`? + """ + self.approved_to_deposit[addr][msg.sender] = can_deposit + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, addr: address = msg.sender): + """ + @notice Deposit `_value` LP tokens + @param _value Number of tokens to deposit + @param addr Address to deposit for + """ + if addr != msg.sender: + assert self.approved_to_deposit[msg.sender][addr], "Not approved" + + self._checkpoint(addr, True) + + if _value != 0: + _balance: uint256 = self.balanceOf[addr] + _value + _supply: uint256 = self.totalSupply + _value + self.balanceOf[addr] = _balance + self.totalSupply = _supply + + self._update_liquidity_limit(addr, _balance, _supply) + + assert ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + CurveRewards(self.reward_contract).stake(_value) + + log Deposit(addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256, claim_rewards: bool = True): + """ + @notice Withdraw `_value` LP tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender, claim_rewards) + + _balance: uint256 = self.balanceOf[msg.sender] - _value + _supply: uint256 = self.totalSupply - _value + self.balanceOf[msg.sender] = _balance + self.totalSupply = _supply + + self._update_liquidity_limit(msg.sender, _balance, _supply) + + if _value > 0: + CurveRewards(self.reward_contract).withdraw(_value) + assert ERC20(self.lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + + +@external +@nonreentrant('lock') +def claim_rewards(addr: address = msg.sender): + self._checkpoint_rewards(addr, True) + _rewards_for: uint256 = self.rewards_for[addr] + assert ERC20(self.rewarded_token).transfer( + addr, _rewards_for - self.claimed_rewards_for[addr]) + self.claimed_rewards_for[addr] = _rewards_for + + +@external +@view +def integrate_checkpoint() -> uint256: + return self.period_timestamp[self.period] + + +@external +def kill_me(): + assert msg.sender == self.admin + self.is_killed = not self.is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def apply_transfer_ownership(): + """ + @notice Apply pending ownership transfer + """ + assert msg.sender == self.admin # dev: admin only + _admin: address = self.future_admin + assert _admin != ZERO_ADDRESS # dev: admin not set + self.admin = _admin + log ApplyOwnership(_admin) + +@external +def toggle_external_rewards_claim(val: bool): + """ + @notice Switch claiming rewards on/off. + This is to prevent a malicious rewards contract from preventing CRV claiming + """ + assert msg.sender == self.admin + self.is_claiming_rewards = val \ No newline at end of file diff --git a/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol index 63a55f8..1030dc5 100644 --- a/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol +++ b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol @@ -8,7 +8,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IModuleAMO.sol"; -import "./interfaces/ICurveGauge.sol"; +import "./interfaces/IGauge.sol"; import "forge-std/console2.sol"; /// @title FraxBPInitModuleAMO @@ -40,7 +40,7 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { // Needed for interactions w/ external contracts address public depositToken; IERC20 public crv = IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); - ICurveGauge public curveGauge; // TODO: gauge address + IGauge public curveGauge; // TODO: gauge address // Events event RewardAdded(uint256 reward); @@ -81,7 +81,7 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { operator = _operator; module = _module; depositToken = _depositToken; - curveGauge = ICurveGauge(_curveGauge); + curveGauge = IGauge(_curveGauge); } /// @notice Get total shares @@ -144,7 +144,7 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { IERC20(depositToken).safeTransferFrom(module, address(this), amount); // Deposit to gauge - //curveGauge.deposit(amount); + curveGauge.deposit(amount); depositedAmount[account] += amount; stakedAmount[account] += amount; @@ -168,7 +168,7 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { stakedAmount[account] -= stakedPoolTokenAmount; // Withdraw from gauge - //curveGauge.withdraw(amount); + curveGauge.withdraw(amount); uint256 shares = _trackWithdrawShares(account); _totalDeposits -= depositAmount; @@ -194,7 +194,7 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { function getRewardFraxBPInit() external onlyOperator returns (uint256) { uint256 crvBalanceBefore = crv.balanceOf(address(this)); // Get rewards - curveGauge.claim_rewards(); + //curveGauge.claim_rewards(); uint256 crvBalanceAfter = crv.balanceOf(address(this)); _totalRewards = crvBalanceAfter - crvBalanceBefore; emit FraxBPInitRewardsReceived(_totalRewards); diff --git a/src/modules/fraxBPInitModule/LiquidityGauge.sol b/src/modules/fraxBPInitModule/LiquidityGauge.sol new file mode 100644 index 0000000..8e12fa6 --- /dev/null +++ b/src/modules/fraxBPInitModule/LiquidityGauge.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +import "./interfaces/IGauge.sol"; +import "./interfaces/IGaugeController.sol"; +import "./interfaces/IToken.sol"; +import "./interfaces/IMinter.sol"; +import "./interfaces/IVotingEscrow.sol"; +import "./interfaces/IOwnership.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +/// @title LiquidityGauge +/// @author Ekonomia: https://github.com/ekonomia-tech +/// @notice Accepts FRAX & USDC +contract LiquidityGauge is ReentrancyGuard, IGauge { + event Deposit(address indexed provider, uint256 value); + event Withdraw(address indexed provider, uint256 value); + event UpdateLiquidityLimit( + address user, + uint256 original_balance, + uint256 original_supply, + uint256 working_balance, + uint256 working_supply, + uint256 voting_balance, + uint256 voting_total + ); + + uint256 constant TOKENLESS_PRODUCTION = 40; + uint256 constant BOOST_WARMUP = 86400 * 14; + uint256 constant WEEK = 604800; + + //Contracts + IMinter public minter; + IToken public token; + IERC20 public template; + IGaugeController public controller; + IVotingEscrow public voting_escrow; + + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + uint256 public future_epoch_time; + + // caller -> recipient -> can deposit? + mapping(address => mapping(address => bool)) public approved_to_deposit; + + mapping(address => uint256) public working_balances; + uint256 public working_supply; + + // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint + // All values are kept in units of being multiplied by 1e18 + uint256 public period; //modified from "int256 public period" since it never be minus. + + uint256[100000000000000000000000000000] public period_timestamp; + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint + uint256[100000000000000000000000000000] public integrate_inv_supply; // bump epoch when rate() changes. Iis(t)=int(r'(t)/S(t))dt (from CurveDAO whitepaper) + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint + mapping(address => uint256) public integrate_inv_supply_of; + mapping(address => uint256) public integrate_checkpoint_of; + + // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint + // Units rate * t = already number of coins per address to issue + mapping(address => uint256) public integrate_fraction; //Mintable Token amount (include minted amount) + + uint256 public inflation_rate; + bool public is_killed; + + IOwnership public immutable ownership; + + modifier onlyOwner() { + require(ownership.owner() == msg.sender, "Caller is not allowed to operate"); + _; + } + + /** + * + * @notice Contract constructor + * @param _lp_addr Liquidity Pool contract address + * @param _minter Minter contract address + * @param _admin Admin who can kill the gauge + */ + constructor(address _lp_addr, address _minter, address _ownership) { + require(_lp_addr != address(0)); + require(_minter != address(0)); + + template = IERC20(_lp_addr); + minter = IMinter(_minter); + address _token_addr = minter.token(); + token = IToken(_token_addr); + controller = IGaugeController(minter.controller()); + voting_escrow = IVotingEscrow(controller.voting_escrow()); + period_timestamp[0] = block.timestamp; + inflation_rate = token.rate(); + future_epoch_time = token.future_epoch_time_write(); + ownership = IOwnership(_ownership); + } + + function lp_token() external view returns (address) { + return address(template); + } + + /** + * + * @notice Calculate limits which depend on the amount of INSURE Token per-user. + * Effectively it calculates working balances to apply amplification + * of INSURE production by INSURE + * @param _addr User address + * @param _l User's amount of liquidity (LP tokens) + * @param _L Total amount of liquidity (LP tokens) + */ + function _update_liquidity_limit(address _addr, uint256 _l, uint256 _L) internal { + // To be called after totalSupply is updated + uint256 _voting_balance = voting_escrow.balanceOf(_addr, block.timestamp); + uint256 _voting_total = voting_escrow.totalSupply(block.timestamp); + + uint256 _lim = (_l * TOKENLESS_PRODUCTION) / 100; + if ((_voting_total > 0) && (block.timestamp > period_timestamp[0] + BOOST_WARMUP)) { + _lim += (_L * _voting_balance * (100 - TOKENLESS_PRODUCTION)) / _voting_total / 100; + } + + _lim = min(_l, _lim); + uint256 _old_bal = working_balances[_addr]; + working_balances[_addr] = _lim; + uint256 _working_supply = working_supply + _lim - _old_bal; + working_supply = _working_supply; + + emit UpdateLiquidityLimit( + _addr, _l, _L, _lim, _working_supply, _voting_balance, _voting_total + ); + } + + //to avoid "stack too deep" + struct CheckPointParameters { + uint256 period; + uint256 period_time; + uint256 integrate_inv_supply; + uint256 rate; + uint256 new_rate; + uint256 prev_future_epoch; + uint256 working_balance; + uint256 working_supply; + } + + /** + * + * @notice Checkpoint for a user + * @param _addr User address + * + * This function does, + * 1. Calculate Iis for All: Calc and add Iis for every week. Iis only increses over time. + * 2. Calculate Iu for _addr: Calc by (defferece between Iis(last time) and Iis(this time))* LP deposit amount of _addr(include INSURE locking boost) + * + * working_supply & working_balance = total_supply & total_balance with INSURE locking boost。 + * Check whitepaper about Iis and Iu. + */ + function _checkpoint(address _addr) internal { + CheckPointParameters memory _st; + + _st.period = period; + _st.period_time = period_timestamp[_st.period]; + _st.integrate_inv_supply = integrate_inv_supply[_st.period]; + _st.rate = inflation_rate; + _st.new_rate = _st.rate; + _st.prev_future_epoch = future_epoch_time; + if (_st.prev_future_epoch >= _st.period_time) { + //update future_epoch_time & inflation_rate + future_epoch_time = token.future_epoch_time_write(); + _st.new_rate = token.rate(); + inflation_rate = _st.new_rate; + } + controller.checkpoint_gauge(address(this)); + + uint256 _working_balance = working_balances[_addr]; + uint256 _working_supply = working_supply; + + if (is_killed) { + _st.rate = 0; // Stop distributing inflation as soon as killed + } + + // Update integral of 1/supply + if (block.timestamp > _st.period_time) { + uint256 _prev_week_time = _st.period_time; + uint256 _week_time; + unchecked { + _week_time = min(((_st.period_time + WEEK) / WEEK) * WEEK, block.timestamp); + } + + for (uint256 i; i < 500;) { + uint256 _dt = _week_time - _prev_week_time; + uint256 _w = + controller.gauge_relative_weight(address(this), (_prev_week_time / WEEK) * WEEK); + + if (_working_supply > 0) { + if ( + _st.prev_future_epoch >= _prev_week_time + && _st.prev_future_epoch < _week_time + ) { + // If we went across one or multiple epochs, apply the rate + // of the first epoch until it ends, and then the rate of + // the last epoch. + // If more than one epoch is crossed - the gauge gets less, + // but that'd meen it wasn't called for more than 1 year + _st.integrate_inv_supply += ( + _st.rate * _w * (_st.prev_future_epoch - _prev_week_time) + ) / _working_supply; + _st.rate = _st.new_rate; + _st.integrate_inv_supply += + (_st.rate * _w * (_week_time - _st.prev_future_epoch)) / _working_supply; + } else { + _st.integrate_inv_supply += (_st.rate * _w * _dt) / _working_supply; + } + // On precisions of the calculation + // rate ~= 10e18 + // last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + // _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + // The largest loss is at dt = 1 + // Loss is 1e-9 - acceptable + } + if (_week_time == block.timestamp) { + break; + } + _prev_week_time = _week_time; + _week_time = min(_week_time + WEEK, block.timestamp); + unchecked { + ++i; + } + } + } + + _st.period += 1; + period = _st.period; + period_timestamp[_st.period] = block.timestamp; + integrate_inv_supply[_st.period] = _st.integrate_inv_supply; + + // Update user-specific integrals + // Calc the ΔIu of _addr and add it to Iu. + integrate_fraction[_addr] += ( + _working_balance * (_st.integrate_inv_supply - integrate_inv_supply_of[_addr]) + ) / 10 ** 18; + integrate_inv_supply_of[_addr] = _st.integrate_inv_supply; + integrate_checkpoint_of[_addr] = block.timestamp; + } + + /** + * + * @notice Record a checkpoint for `_addr` + * @param _addr User address + * @return bool success + */ + function user_checkpoint(address _addr) external returns (bool) { + require((msg.sender == _addr) || (msg.sender == address(minter)), "dev: unauthorized"); + _checkpoint(_addr); + _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); + return true; + } + + /** + * + * @notice Get the number of claimable tokens per user + * @dev This function should be manually changed to "view" in the ABI + * @return uint256 number of claimable tokens per user + */ + function claimable_tokens(address _addr) external returns (uint256) { + _checkpoint(_addr); + return (integrate_fraction[_addr] - minter.minted(_addr, address(this))); + } + + /** + * + * @notice Kick `_addr` for abusing their boost + * @dev Only if either they had another voting event, or their voting escrow lock expired + * @param _addr Address to kick + */ + function kick(address _addr) external { + uint256 _t_last = integrate_checkpoint_of[_addr]; + uint256 _t_ve = + voting_escrow.user_point_history__ts(_addr, voting_escrow.get_user_point_epoch(_addr)); + uint256 _balance = balanceOf[_addr]; + + require( + voting_escrow.balanceOf(_addr, block.timestamp) == 0 || _t_ve > _t_last, + "dev: kick not allowed" + ); + require( + working_balances[_addr] > (_balance * TOKENLESS_PRODUCTION) / 100, + "dev: kick not needed" + ); + + _checkpoint(_addr); + _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); + } + + /** + * + * @notice Set whether `_addr` can deposit tokens for `msg.sender` + * @param _addr Address to set approval on + * @param can_deposit bool - can this account deposit for `msg.sender`? + */ + function set_approve_deposit(address _addr, bool can_deposit) external { + approved_to_deposit[_addr][msg.sender] = can_deposit; + } + + /** + * + * @notice Deposit `_value` LP tokens + * @param _value Number of tokens to deposit + */ + function deposit(uint256 _value) external nonReentrant { + // if (_addr != msg.sender) { + // require(approved_to_deposit[msg.sender][_addr], "Not approved"); + // } + + _checkpoint(msg.sender); + + if (_value != 0) { + uint256 _balance = balanceOf[msg.sender] + _value; + uint256 _supply = totalSupply + _value; + balanceOf[msg.sender] = _balance; + totalSupply = _supply; + + _update_liquidity_limit(msg.sender, _balance, _supply); + + require(template.transferFrom(msg.sender, address(this), _value)); + } + emit Deposit(msg.sender, _value); + } + + /** + * + * @notice Withdraw `_value` LP tokens + * @param _value Number of tokens to withdraw + */ + function withdraw(uint256 _value) external nonReentrant { + _checkpoint(msg.sender); + + uint256 _balance = balanceOf[msg.sender] - _value; + uint256 _supply = totalSupply - _value; + balanceOf[msg.sender] = _balance; + totalSupply = _supply; + + _update_liquidity_limit(msg.sender, _balance, _supply); + + require(template.transfer(msg.sender, _value)); + + emit Withdraw(msg.sender, _value); + } + + function integrate_checkpoint() external view returns (uint256) { + return period_timestamp[period]; + } + + function kill_me() external onlyOwner { + is_killed = !is_killed; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol b/src/modules/fraxBPInitModule/interfaces/IGauge.sol similarity index 66% rename from src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol rename to src/modules/fraxBPInitModule/interfaces/IGauge.sol index 594bc12..7646aaa 100644 --- a/src/modules/fraxBPInitModule/interfaces/ICurveGauge.sol +++ b/src/modules/fraxBPInitModule/interfaces/IGauge.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.13; // Inspired from: https://github.com/convex-eth/platform/blob/ecea24d8ff6eb850134573e80cfc795d26805b76/contracts/contracts/Interfaces.sol -interface ICurveGauge { +interface IGauge { function balanceOf(address) external view returns (uint256); - function claim_rewards() external; + //function claim_rewards() external; function deposit(uint256 _amount) external; @@ -14,7 +14,7 @@ interface ICurveGauge { function lp_token() external view returns (address); - function reward_tokens(uint256 index) external view returns (address); + //function reward_tokens(uint256 index) external view returns (address); - function claimable_reward(address, address) external view returns (uint256); + //function claimable_reward(address, address) external view returns (uint256); } diff --git a/src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol b/src/modules/fraxBPInitModule/interfaces/IGaugeController.sol similarity index 63% rename from src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol rename to src/modules/fraxBPInitModule/interfaces/IGaugeController.sol index df2eb63..cbc194c 100644 --- a/src/modules/fraxBPInitModule/interfaces/ICurveGaugeController.sol +++ b/src/modules/fraxBPInitModule/interfaces/IGaugeController.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; // Inspired from: https://github.com/convex-eth/platform/blob/ecea24d8ff6eb850134573e80cfc795d26805b76/contracts/contracts/interfaces/IGaugeController.sol -interface ICurveGaugeController { +interface IGaugeController { function get_gauge_weight(address _gauge) external view returns (uint256); function vote_user_slopes(address, address) external view returns (uint256, uint256, uint256); //slope,power,end @@ -12,4 +12,12 @@ interface ICurveGaugeController { function vote_for_gauge_weights(address, uint256) external; function add_gauge(address, int128, uint256) external; + + function gauge_types(address _addr) external view returns (uint256); + + function voting_escrow() external view returns (address); + + function checkpoint_gauge(address addr) external; + + function gauge_relative_weight(address addr, uint256 time) external view returns (uint256); } diff --git a/src/modules/fraxBPInitModule/interfaces/IMinter.sol b/src/modules/fraxBPInitModule/interfaces/IMinter.sol new file mode 100644 index 0000000..29af691 --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/IMinter.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +// TODO: +// https://github.com/InsureDAO/dao-contracts/blob/develop/contracts/interfaces/dao/IMinter.sol + +import "./IGaugeController.sol"; + +interface IMinter { + function token() external view returns (address); + + function controller() external view returns (address); + + function minted(address user, address gauge) external view returns (uint256); +} diff --git a/src/modules/fraxBPInitModule/interfaces/IOwnership.sol b/src/modules/fraxBPInitModule/interfaces/IOwnership.sol new file mode 100644 index 0000000..04176fe --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/IOwnership.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +interface IOwnership { + function owner() external view returns (address); + + function futureOwner() external view returns (address); + + function commitTransferOwnership(address newOwner) external; + + function acceptTransferOwnership() external; +} diff --git a/src/modules/fraxBPInitModule/interfaces/IToken.sol b/src/modules/fraxBPInitModule/interfaces/IToken.sol new file mode 100644 index 0000000..315b723 --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/IToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +interface IToken { + function mint(address _to, uint256 _value) external returns (bool); + + function emergency_mint(uint256 _amountOut, address _to) external; + + function approve(address _spender, uint256 _value) external; + + function rate() external view returns (uint256); + + function future_epoch_time_write() external returns (uint256); +} diff --git a/src/modules/fraxBPInitModule/interfaces/IVotingEscrow.sol b/src/modules/fraxBPInitModule/interfaces/IVotingEscrow.sol new file mode 100644 index 0000000..4b0225e --- /dev/null +++ b/src/modules/fraxBPInitModule/interfaces/IVotingEscrow.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +interface IVotingEscrow { + function get_last_user_slope(address _addr) external view returns (uint256); + + function locked__end(address _addr) external view returns (uint256); + + function balanceOf(address _addr, uint256 _t) external view returns (uint256); + + //function balanceOf(address addr)external view returns (uint256); + function totalSupply(uint256 _t) external view returns (uint256); + + function get_user_point_epoch(address _user) external view returns (uint256); + + function user_point_history__ts(address _addr, uint256 _idx) external view returns (uint256); +} diff --git a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol index 9b3eaff..1f4810c 100644 --- a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol +++ b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol @@ -9,6 +9,11 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@modules/priceController/PriceController.sol"; import "@modules/fraxBPInitModule/FraxBPInitModule.sol"; import "forge-std/console2.sol"; +import "@modules/fraxBPInitModule/interfaces/IGauge.sol"; +import "@modules/fraxBPInitModule/interfaces/IGaugeController.sol"; +import "@modules/fraxBPInitModule/interfaces/IMinter.sol"; +import "@modules/fraxBPInitModule/LiquidityGauge.sol"; +import "../../utils/VyperDeployer.sol"; contract FraxBPInitModuleTest is BaseSetup { /// Errors @@ -26,6 +31,9 @@ contract FraxBPInitModuleTest is BaseSetup { ICurvePool public fraxBPPHOMetapool; FraxBPInitModule public fraxBPInitModule; + IGaugeController public curveGaugeController; + IMinter public curveMinter; + IGauge public curveLiquidityGauge; /// Constants uint256 public saleEndDate; @@ -47,9 +55,24 @@ contract FraxBPInitModuleTest is BaseSetup { uint256 totalPHOSupply; } + VyperDeployer deployer; + function setUp() public { fraxBPLP = IERC20(FRAXBP_LP_TOKEN); curveFactory = ICurveFactory(metaPoolFactoryAddress); + curveGaugeController = IGaugeController(0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB); + + curveMinter = IMinter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0); + + console2.log("in setup start.."); + deployer = new VyperDeployer(); + console2.log("in setup, got vyper deployer.."); + + // voting escrow contract: + // 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2 + + // example of LiquidityGaugeReward + // 0xA90996896660DEcC6E997655E065b23788857849 // Give user FRAX and USDC _getFRAX(user1, TEN_THOUSAND_D18); @@ -103,9 +126,58 @@ contract FraxBPInitModuleTest is BaseSetup { fraxBPLP.approve(address(fraxBPInitModule), ONE_MILLION_D18); vm.stopPrank(); - // Do same for maple AMO + // Do same for AMO vm.prank(user1); fraxBPPHOMetapool.approve(address(fraxBPInitModule.fraxBPInitModuleAMO()), TEN_THOUSAND_D18); + + console2.log("this is curveGaugeController address: ", address(curveGaugeController)); + + console2.log("SETTING UP GAUGE......."); + + // curveLiquidityGauge = IGauge( + // new LiquidityGauge( + // address(fraxBPPHOMetapool), + // address(curveMinter), + // address(owner) + // ) + // ); + + // Reward contract: SNX rewards + // 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92 + // Rewarded token: SNX + // 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F + + // Still reverts in venv with vyper + curveLiquidityGauge = IGauge( + deployer.deployContract( + "LiquidityGaugeReward", + abi.encode( + address(fraxBPPHOMetapool), + address(curveMinter), + 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92, + 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F, + address(owner) + ) + ) + ); + + // curveLiquidityGauge = IGauge( + // 0xA90996896660DEcC6E997655E065b23788857849 + // ); + + console2.log("DEPLOYED GAUGE --------------> address is: ", address(curveLiquidityGauge)); + + console2.log("ADDING GAUGE..."); + + // Need msg.sender to be gauge admin + vm.prank(0x40907540d8a6C65c637785e8f8B742ae6b0b9968); + curveGaugeController.add_gauge(address(curveLiquidityGauge), 0, 100); + + console2.log("ADDED GAUGE..."); + + uint256 weight = curveGaugeController.get_gauge_weight(address(curveLiquidityGauge)); + + console2.log("THIS IS GAUGE WEIGHT: ", weight); } // Cannot set addresses to 0 diff --git a/test/utils/VyperDeployer.sol b/test/utils/VyperDeployer.sol new file mode 100644 index 0000000..a82daa6 --- /dev/null +++ b/test/utils/VyperDeployer.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import "forge-std/console2.sol"; + +pragma solidity ^0.8.13; + +///@notice This cheat codes interface is named _CheatCodes so you can use the CheatCodes interface in other testing files without errors +interface _CheatCodes { + function ffi(string[] calldata) external returns (bytes memory); +} + +contract VyperDeployer { + address constant HEVM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + + /// @notice Initializes cheat codes in order to use ffi to compile Vyper contracts + _CheatCodes cheatCodes = _CheatCodes(HEVM_ADDRESS); + + ///@notice Compiles a Vyper contract and returns the address that the contract was deployeod to + ///@notice If deployment fails, an error will be thrown + ///@param fileName - The file name of the Vyper contract. For example, the file name for "SimpleStore.vy" is "SimpleStore" + ///@return deployedAddress - The address that the contract was deployed to + + function deployContract(string memory fileName) public returns (address) { + console2.log("starting deployContract().."); + ///@notice create a list of strings with the commands necessary to compile Vyper contracts + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat("vyper_contracts/", fileName, ".vy"); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory bytecode = cheatCodes.ffi(cmds); + + console2.log("in deployContract() -> running create()"); + + ///@notice deploy the bytecode with the create instruction + address deployedAddress; + assembly { + deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode)) + } + + console2.log("in deployContract() -> done running create()"); + + ///@notice check that the deployment was successful + require(deployedAddress != address(0), "VyperDeployer could not deploy contract"); + + ///@notice return the address that the contract was deployed to + return deployedAddress; + } + + ///@notice Compiles a Vyper contract with constructor arguments and returns the address that the contract was deployeod to + ///@notice If deployment fails, an error will be thrown + ///@param fileName - The file name of the Vyper contract. For example, the file name for "SimpleStore.vy" is "SimpleStore" + ///@return deployedAddress - The address that the contract was deployed to + function deployContract(string memory fileName, bytes calldata args) public returns (address) { + ///@notice create a list of strings with the commands necessary to compile Vyper contracts + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat("src/", fileName, ".vy"); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory _bytecode = cheatCodes.ffi(cmds); + + //add args to the deployment bytecode + bytes memory bytecode = abi.encodePacked(_bytecode, args); + + ///@notice deploy the bytecode with the create instruction + address deployedAddress; + assembly { + deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode)) + } + + ///@notice check that the deployment was successful + require(deployedAddress != address(0), "VyperDeployer could not deploy contract"); + + ///@notice return the address that the contract was deployed to + return deployedAddress; + } +} From 2b3cf02313358b374c6d056f1208b06d5d828594 Mon Sep 17 00:00:00 2001 From: Kevin Huo Date: Fri, 2 Dec 2022 17:08:05 -0500 Subject: [PATCH 3/3] modify forked sol version and tests --- .../fraxBPInitModule/FraxBPInitModule.sol | 7 +- .../fraxBPInitModule/FraxBPInitModuleAMO.sol | 11 +- .../fraxBPInitModule/LiquidityGauge.sol | 54 ++- .../fraxBPInitModule/interfaces/IGauge.sol | 4 + .../fraxBPInitModule/fraxBPInitModule.t.sol | 428 ++++++++++++++++-- 5 files changed, 464 insertions(+), 40 deletions(-) diff --git a/src/modules/fraxBPInitModule/FraxBPInitModule.sol b/src/modules/fraxBPInitModule/FraxBPInitModule.sol index 8d3b8eb..3da555a 100644 --- a/src/modules/fraxBPInitModule/FraxBPInitModule.sol +++ b/src/modules/fraxBPInitModule/FraxBPInitModule.sol @@ -63,11 +63,12 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { address _pho, address _priceOracle, uint256 _saleEndDate, - uint256 _redemptionStartDate + uint256 _redemptionStartDate, + address _gauge ) { if ( _moduleManager == address(0) || _fraxBPPHOMetapool == address(0) || _pho == address(0) - || _priceOracle == address(0) + || _priceOracle == address(0) || _gauge == address(0) ) { revert ZeroAddressDetected(); } @@ -92,7 +93,7 @@ contract FraxBPInitModule is Ownable, ReentrancyGuard { msg.sender, address(this), address(fraxBPPHOMetapool), - address(fraxBPPHOMetapool) + _gauge ); fraxBPInitModuleAMO = address(fraxBPModuleAMO); diff --git a/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol index 1030dc5..a004882 100644 --- a/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol +++ b/src/modules/fraxBPInitModule/FraxBPInitModuleAMO.sol @@ -82,6 +82,8 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { module = _module; depositToken = _depositToken; curveGauge = IGauge(_curveGauge); + + IERC20(depositToken).approve(address(curveGauge), type(uint256).max); } /// @notice Get total shares @@ -143,8 +145,15 @@ contract FraxBPInitModuleAMO is IModuleAMO, ERC20 { // Get depositToken from module IERC20(depositToken).safeTransferFrom(module, address(this), amount); + console2.log("In FraxBPInitModuleAMO.. about to approve().."); + + //IERC20(depositToken).approve(address(curveGauge), type(uint256).max); + + console2.log("In FraxBPInitModuleAMO.. approved, now about to call curveGauge.deposit().."); + // Deposit to gauge - curveGauge.deposit(amount); + //curveGauge.deposit(amount); + curveGauge.deposit(amount, address(this)); depositedAmount[account] += amount; stakedAmount[account] += amount; diff --git a/src/modules/fraxBPInitModule/LiquidityGauge.sol b/src/modules/fraxBPInitModule/LiquidityGauge.sol index 8e12fa6..0e3bf3a 100644 --- a/src/modules/fraxBPInitModule/LiquidityGauge.sol +++ b/src/modules/fraxBPInitModule/LiquidityGauge.sol @@ -11,6 +11,7 @@ import "./interfaces/IOwnership.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "forge-std/console2.sol"; /// @title LiquidityGauge /// @author Ekonomia: https://github.com/ekonomia-tech @@ -81,12 +82,14 @@ contract LiquidityGauge is ReentrancyGuard, IGauge { * @notice Contract constructor * @param _lp_addr Liquidity Pool contract address * @param _minter Minter contract address - * @param _admin Admin who can kill the gauge + * @param _ownership Admin who can kill the gauge */ constructor(address _lp_addr, address _minter, address _ownership) { require(_lp_addr != address(0)); require(_minter != address(0)); + console2.log("In LiquidityGauge, constructor.."); + template = IERC20(_lp_addr); minter = IMinter(_minter); address _token_addr = minter.token(); @@ -310,12 +313,37 @@ contract LiquidityGauge is ReentrancyGuard, IGauge { * @param _value Number of tokens to deposit */ function deposit(uint256 _value) external nonReentrant { - // if (_addr != msg.sender) { - // require(approved_to_deposit[msg.sender][_addr], "Not approved"); - // } + _checkpoint(msg.sender); + + if (_value != 0) { + uint256 _balance = balanceOf[msg.sender] + _value; + uint256 _supply = totalSupply + _value; + balanceOf[msg.sender] = _balance; + totalSupply = _supply; + + _update_liquidity_limit(msg.sender, _balance, _supply); + + require(template.transferFrom(msg.sender, address(this), _value)); + } + emit Deposit(msg.sender, _value); + } + + /** + * + * @notice Deposit `_value` LP tokens + * @param _value Number of tokens to deposit + * @param _addr Address + */ + function deposit(uint256 _value, address _addr) external nonReentrant { + console2.log("In LiquidityGauge -> deposit()..."); + if (_addr != msg.sender) { + require(approved_to_deposit[msg.sender][_addr], "Not approved"); + } _checkpoint(msg.sender); + // console2.log("In LiquidityGauge -> deposit() done with _checkpoint"); + if (_value != 0) { uint256 _balance = balanceOf[msg.sender] + _value; uint256 _supply = totalSupply + _value; @@ -324,7 +352,25 @@ contract LiquidityGauge is ReentrancyGuard, IGauge { _update_liquidity_limit(msg.sender, _balance, _supply); + // console2.log( + // "In LiquidityGauge -> deposit() done with _update_liquidity_limit" + // ); + + // console2.log( + // "In LiquidityGauge -> deposit() this is template balanceOf(msg.sender)", + // template.balanceOf(msg.sender) + // ); + + // console2.log( + // "In LiquidityGauge -> deposit() this is template value", + // _value + // ); + require(template.transferFrom(msg.sender, address(this), _value)); + + // console2.log( + // "In LiquidityGauge -> deposit() done with template transferFrom()" + // ); } emit Deposit(msg.sender, _value); } diff --git a/src/modules/fraxBPInitModule/interfaces/IGauge.sol b/src/modules/fraxBPInitModule/interfaces/IGauge.sol index 7646aaa..1f54d5b 100644 --- a/src/modules/fraxBPInitModule/interfaces/IGauge.sol +++ b/src/modules/fraxBPInitModule/interfaces/IGauge.sol @@ -8,8 +8,12 @@ interface IGauge { //function claim_rewards() external; + function set_approve_deposit(address depositor, bool can_deposit) external; + function deposit(uint256 _amount) external; + function deposit(uint256 _amount, address sender) external; + function withdraw(uint256 _amount) external; function lp_token() external view returns (address); diff --git a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol index 1f4810c..291b0de 100644 --- a/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol +++ b/test/modules/fraxBPInitModule/fraxBPInitModule.t.sol @@ -34,6 +34,7 @@ contract FraxBPInitModuleTest is BaseSetup { IGaugeController public curveGaugeController; IMinter public curveMinter; IGauge public curveLiquidityGauge; + IERC20 public crv; /// Constants uint256 public saleEndDate; @@ -55,6 +56,12 @@ contract FraxBPInitModuleTest is BaseSetup { uint256 totalPHOSupply; } + struct SharesVars { + uint256 shares; + uint256 earned; + uint256 totalShares; + } + VyperDeployer deployer; function setUp() public { @@ -63,8 +70,8 @@ contract FraxBPInitModuleTest is BaseSetup { curveGaugeController = IGaugeController(0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB); curveMinter = IMinter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0); + crv = IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); - console2.log("in setup start.."); deployer = new VyperDeployer(); console2.log("in setup, got vyper deployer.."); @@ -93,6 +100,31 @@ contract FraxBPInitModuleTest is BaseSetup { saleEndDate = block.timestamp + 10000; redemptionStartDate = block.timestamp + 20000; + console2.log("Setting up gauge.."); + + curveLiquidityGauge = IGauge( + new LiquidityGauge( + address(fraxBPPHOMetapool), + address(curveMinter), + address(owner) + ) + ); + + // curveLiquidityGauge = IGauge( + // deployer.deployContract( + // "LiquidityGaugeReward", + // abi.encode( + // address(fraxBPPHOMetapool), + // address(curveMinter), + // 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92, + // 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F, + // address(owner) + // ) + // ) + // ); + + console2.log("this is gauge address: ", address(curveLiquidityGauge)); + vm.prank(owner); fraxBPInitModule = new FraxBPInitModule( address(moduleManager), @@ -100,9 +132,18 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(priceFeed), saleEndDate, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) ); + console2.log("made fraxBPInitModule.. now"); + + // vm.prank(user1); + // curveLiquidityGauge.set_approve_deposit( + // address(fraxBPInitModule), + // true + // ); + vm.prank(PHOGovernance); moduleManager.addModule(address(fraxBPInitModule)); @@ -132,34 +173,24 @@ contract FraxBPInitModuleTest is BaseSetup { console2.log("this is curveGaugeController address: ", address(curveGaugeController)); - console2.log("SETTING UP GAUGE......."); - - // curveLiquidityGauge = IGauge( - // new LiquidityGauge( - // address(fraxBPPHOMetapool), - // address(curveMinter), - // address(owner) - // ) - // ); - // Reward contract: SNX rewards // 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92 // Rewarded token: SNX // 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F - // Still reverts in venv with vyper - curveLiquidityGauge = IGauge( - deployer.deployContract( - "LiquidityGaugeReward", - abi.encode( - address(fraxBPPHOMetapool), - address(curveMinter), - 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92, - 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F, - address(owner) - ) - ) - ); + // // Still reverts in venv with vyper + // curveLiquidityGauge = IGauge( + // deployer.deployContract( + // "LiquidityGaugeReward", + // abi.encode( + // address(fraxBPPHOMetapool), + // address(curveMinter), + // 0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92, + // 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F, + // address(owner) + // ) + // ) + // ); // curveLiquidityGauge = IGauge( // 0xA90996896660DEcC6E997655E065b23788857849 @@ -191,7 +222,8 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(priceFeed), saleEndDate, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) ); // Frax BP / PHO Pool @@ -202,7 +234,8 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(priceFeed), saleEndDate, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) ); // PHO @@ -213,7 +246,8 @@ contract FraxBPInitModuleTest is BaseSetup { address(0), address(priceFeed), saleEndDate, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) ); // Oracle @@ -224,7 +258,20 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(0), saleEndDate, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) + ); + + // Gauge + vm.expectRevert(abi.encodeWithSelector(ZeroAddressDetected.selector)); + fraxBPInitModule = new FraxBPInitModule( + address(moduleManager), + address(fraxBPPHOMetapool), + address(pho), + address(priceFeed), + saleEndDate, + redemptionStartDate, + address(0) ); vm.stopPrank(); @@ -240,7 +287,8 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(priceFeed), block.timestamp - 1, - redemptionStartDate + redemptionStartDate, + address(curveLiquidityGauge) ); vm.expectRevert(abi.encodeWithSelector(InvalidTimeWindows.selector)); @@ -250,7 +298,8 @@ contract FraxBPInitModuleTest is BaseSetup { address(pho), address(priceFeed), saleEndDate, - saleEndDate + saleEndDate, + address(curveLiquidityGauge) ); vm.stopPrank(); @@ -354,7 +403,7 @@ contract FraxBPInitModuleTest is BaseSetup { assertEq(aft.moduleUSDCBalance, before.moduleUSDCBalance); assertEq(aft.moduleFRAXBalance, before.moduleFRAXBalance); - // TODO: modify -> balance same b/c goes to AMO + // // TODO: modify -> balance same b/c goes to AMO // assertApproxEqAbs( // aft.moduleFraxBPPHOLPBalance, // before.moduleFraxBPPHOLPBalance + _expectedMintAmount, @@ -505,4 +554,319 @@ contract FraxBPInitModuleTest is BaseSetup { // Check PHO supply same assertEq(aft.totalPHOSupply, before.totalPHOSupply); } + + // Test Reward + function testRewardFraxBPInitModule() public { + uint256 usdcDepositAmount = ONE_HUNDRED_D6; + uint256 fraxDepositAmount = ONE_HUNDRED_D18; + _testGetRewardAnyModule( + usdcDepositAmount, fraxDepositAmount, fraxBPInitModule, saleEndDate - 500 + ); + } + + // Helper function to test FraxBPInit rewards from any module + function _testGetRewardAnyModule( + uint256 _usdcDepositAmount, + uint256 _fraxDepositAmount, + FraxBPInitModule _module, + uint256 _depositTimestamp + ) public { + address moduleRewardPool = _module.fraxBPInitModuleAMO(); + + vm.warp(_depositTimestamp); + + vm.prank(user1); + _module.depositHelper(_usdcDepositAmount, _fraxDepositAmount); + + // Advance days to accrue rewards + vm.warp(block.timestamp + 7 days); + + // Get reward + vm.prank(owner); + uint256 rewardsFraxBPInit = FraxBPInitModuleAMO(moduleRewardPool).getRewardFraxBPInit(); + + // User gets the reward + vm.warp(block.timestamp + 1 days); + + vm.prank(user1); + FraxBPInitModuleAMO(moduleRewardPool).getReward(user1); + + uint256 finalUserRewardsBalance = + IERC20(FraxBPInitModuleAMO(moduleRewardPool).rewardToken()).balanceOf(user1); + + // Check that user got rewards and protocol has none + assertTrue(finalUserRewardsBalance > 0); + } + + // Testing shares + + // // Test basic shares for deposit + // function testSharesDepositFraxBPInitModule() public { + // uint256 usdcDepositAmount = ONE_HUNDRED_D6; + // uint256 fraxDepositAmount = ONE_HUNDRED_D18; + // uint256 expectedMint = 4 * fraxDepositAmount; + // _testSharesDepositAnyModule( + // usdcDepositAmount, + // fraxDepositAmount, + // expectedMint, + // address(crv), + // fraxBPInitModule, + // saleEndDate - 500, + // 1 + // ); + // } + + // Helper function to test shares for FraxBPInit deposit from any module + function _testSharesDepositAnyModule( + uint256 _usdcDepositAmount, + uint256 _fraxDepositAmount, + uint256 _expectedMintAmount, + address _depositToken, + FraxBPInitModule _module, + uint256 _depositTimestamp, + uint256 deltaThreshold + ) public { + uint256 usdcDepositAmount = _usdcDepositAmount; + uint256 fraxDepositAmount = _fraxDepositAmount; + FraxBPInitModuleAMO amo = FraxBPInitModuleAMO(_module.fraxBPInitModuleAMO()); + + // Shares tracking before - users 1 & 2 + SharesVars memory before1; + before1.shares = amo.sharesOf(user1); + before1.earned = amo.earned(user1); + before1.totalShares = amo.totalShares(); + SharesVars memory before2; + before2.shares = amo.sharesOf(user2); + before2.earned = amo.earned(user2); + before2.totalShares = amo.totalShares(); + + vm.warp(_depositTimestamp); + + // Deposit - user 1 + vm.prank(user1); + _module.depositHelper(_usdcDepositAmount, _fraxDepositAmount); + + // Shares tracking afterwards for user 1 + SharesVars memory aft1; + aft1.shares = amo.sharesOf(user1); + aft1.earned = amo.earned(user1); + aft1.totalShares = amo.totalShares(); + + // After deposit 1 checks + + // Check that before state was all 0 + assertEq(before1.shares, 0); + assertEq(before1.earned, 0); + assertEq(before1.totalShares, 0); + + // Check that after state was modified except earned + assertApproxEqAbs(aft1.shares, _expectedMintAmount, deltaThreshold * 10 ** 18); + assertEq(aft1.earned, 0); + assertApproxEqAbs(aft1.totalShares, _expectedMintAmount, deltaThreshold * 10 ** 18); + + // Deposit - user 2 + vm.prank(user2); + _module.depositHelper(_usdcDepositAmount / 4, _fraxDepositAmount / 4); + + // Shares tracking afterwards for user 2 + SharesVars memory aft2; + aft2.shares = amo.sharesOf(user2); + aft2.earned = amo.earned(user2); + aft2.totalShares = amo.totalShares(); + + // After deposit 2 checks - total deposits was N, they put in N/4 + // Should have N/4 / (N/4 + N) = N/5 of total shares + + // Check that before state was all 0 + assertEq(before2.shares, 0); + assertEq(before2.earned, 0); + assertEq(before2.totalShares, 0); + + // Check that after state was modified except earned + assertApproxEqAbs(aft2.shares, _expectedMintAmount / 5, deltaThreshold * 10 ** 18); + assertEq(aft2.earned, 0); + assertApproxEqAbs( + aft2.totalShares, + _expectedMintAmount + _expectedMintAmount / 5, + deltaThreshold * 10 ** 18 + ); + } + + // Test Redeem + function testSharesRedeemFraxBPInitModule() public { + uint256 usdcDepositAmount = ONE_HUNDRED_D6; + uint256 fraxDepositAmount = ONE_HUNDRED_D18; + uint256 expectedMint = 4 * fraxDepositAmount; + uint256 redeemAmount = 2 * ONE_HUNDRED_D18; + uint256 redeemTimestamp = redemptionStartDate + 1; + _testSharesDepositAnyModule( + usdcDepositAmount, + fraxDepositAmount, + expectedMint, + address(crv), + fraxBPInitModule, + saleEndDate - 500, + 1 + ); + uint256 startingTotalDeposits = fraxDepositAmount + fraxDepositAmount / 4; + uint256 startingTotalShares = expectedMint + expectedMint / 5; + _testSharesRedeemAnyModule( + redeemAmount, + address(crv), + fraxBPInitModule, + redeemTimestamp, + startingTotalDeposits, + startingTotalShares, + 1 + ); + } + + // Helper function to test shares for FraxBPInit redeem from any module + function _testSharesRedeemAnyModule( + uint256 _redeemAmount, + address _depositToken, + FraxBPInitModule _module, + uint256 _redeemTimestamp, + uint256 _startingTotalDeposits, + uint256 _startingTotalShares, + uint256 deltaThreshold + ) public { + // Convert expected issue amount based on stablecoin decimals + address moduleRewardPool = _module.fraxBPInitModuleAMO(); + + FraxBPInitModuleAMO amo = FraxBPInitModuleAMO(_module.fraxBPInitModuleAMO()); + + // Shares tracking before - users 1 & 2 + SharesVars memory before1; + before1.shares = amo.sharesOf(user1); + before1.earned = amo.earned(user1); + before1.totalShares = amo.totalShares(); + SharesVars memory before2; + before2.shares = amo.sharesOf(user2); + before2.earned = amo.earned(user2); + before2.totalShares = amo.totalShares(); + + vm.warp(_redeemTimestamp); + + // Redeem for user 1 + vm.warp(_redeemTimestamp); + vm.prank(user1); + _module.redeem(); + + // Shares tracking afterwards - user 1 + SharesVars memory aft1; + aft1.shares = amo.sharesOf(user1); + aft1.earned = amo.earned(user1); + aft1.totalShares = amo.totalShares(); + + // // User 2 redeems + vm.prank(user2); + _module.redeem(); + + // Shares tracking afterwards - user 2 + SharesVars memory aft2; + aft2.shares = amo.sharesOf(user2); + aft2.earned = amo.earned(user2); + aft2.totalShares = amo.totalShares(); + + // Check before state + assertApproxEqAbs(before1.shares, 2 * _redeemAmount, deltaThreshold * 10 ** 18); + assertEq(before1.earned, 0); + assertApproxEqAbs(before1.totalShares, _startingTotalShares, deltaThreshold * 10 ** 18); + assertApproxEqAbs(before2.shares, (2 * _redeemAmount) / 5, deltaThreshold * 10 ** 18); + assertEq(before2.earned, 0); + assertApproxEqAbs(before2.totalShares, _startingTotalShares, deltaThreshold * 10 ** 18); + + // Check after state + assertEq(aft1.shares, 0); + assertEq(aft1.earned, 0); + assertApproxEqAbs( + aft1.totalShares, _startingTotalShares - 2 * _redeemAmount, deltaThreshold * 10 ** 18 + ); + assertEq(aft2.shares, 0); + assertEq(aft2.earned, 0); + assertEq(aft2.totalShares, 0); + } + + // Test Reward - TODO: patch up + function testSharesRewardFraxBPInitModule() public { + uint256 usdcDepositAmount = ONE_HUNDRED_D6; + uint256 fraxDepositAmount = ONE_HUNDRED_D18; + _testSharesGetRewardAnyModule( + usdcDepositAmount, fraxDepositAmount, fraxBPInitModule, saleEndDate - 500, 1 + ); + } + + // Helper function to test FraxBPInit rewards from any module + function _testSharesGetRewardAnyModule( + uint256 _usdcDepositAmount, + uint256 _fraxDepositAmount, + FraxBPInitModule _module, + uint256 _depositTimestamp, + uint256 deltaThreshold + ) public { + uint256 usdcDepositAmount = _usdcDepositAmount; + uint256 fraxDepositAmount = _fraxDepositAmount; + address moduleRewardPool = _module.fraxBPInitModuleAMO(); + + FraxBPInitModuleAMO amo = FraxBPInitModuleAMO(_module.fraxBPInitModuleAMO()); + + // Shares tracking before - users 1 & 2 + SharesVars memory before1; + before1.shares = amo.sharesOf(user1); + before1.earned = amo.earned(user1); + before1.totalShares = amo.totalShares(); + SharesVars memory before2; + before2.shares = amo.sharesOf(user2); + before2.earned = amo.earned(user2); + before2.totalShares = amo.totalShares(); + + vm.warp(_depositTimestamp); + + // Deposit - user 1 and user 2 + vm.prank(user1); + _module.depositHelper(_usdcDepositAmount, _fraxDepositAmount); + vm.prank(user2); + _module.depositHelper(_usdcDepositAmount / 4, _fraxDepositAmount / 4); + + // Advance days to accrue rewards + vm.warp(block.timestamp + 10 days); + + // Get reward + vm.prank(owner); + uint256 rewardsFraxBPInit = amo.getRewardFraxBPInit(); + + // User gets the reward + vm.warp(block.timestamp + 1 days); + + // Shares tracking afterwards - user 1 + SharesVars memory aft1; + aft1.shares = amo.sharesOf(user1); + aft1.earned = amo.earned(user1); + aft1.totalShares = amo.totalShares(); + // Shares tracking afterwards - user 2 + SharesVars memory aft2; + aft2.shares = amo.sharesOf(user2); + aft2.earned = amo.earned(user2); + aft2.totalShares = amo.totalShares(); + + // TODO: modify + + // // Rewards for user 2 should be 1/5 of the rewards for user 1 + // // As per similar logic above, since user 2 has 1/5 total shares + // assertTrue(aft1.earned > 0 && aft2.earned > 0); + // assertApproxEqAbs(aft1.earned, 5 * aft2.earned, 1000 wei); + + // // Get actual rewards, earned() should reset to 0 + // vm.prank(user1); + // amo.getReward(user1); + // vm.prank(user2); + // amo.getReward(user2); + + // aft1.earned = amo.earned(user1); + // aft2.earned = amo.earned(user2); + + // assertEq(aft1.earned, 0); + // assertEq(aft2.earned, 0); + } }