diff --git a/src/modules/liquityModule/LiquityDepositModule.sol b/src/modules/liquityModule/LiquityDepositModule.sol new file mode 100644 index 0000000..540f0fb --- /dev/null +++ b/src/modules/liquityModule/LiquityDepositModule.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "../interfaces/IModuleAMO.sol"; +import "./LiquityModuleAMO.sol"; +import "@modules/stablecoinDepositModule/StablecoinDepositModuleBase.sol"; + +/// @title LiquityDepositModule +/// @author Ekonomia: https://github.com/ekonomia-tech +/// @notice Accepts LUSD 1:1 and uses Liquity StabilityPool for AMO +contract LiquityDepositModule is StablecoinDepositModuleBase { + using SafeERC20 for IERC20Metadata; + + /// Errors + error CannotDepositZero(); + error CannotRedeemZeroTokens(); + + /// Events + event Deposited(address indexed depositor, uint256 depositAmount, uint256 phoMinted); + event Redeemed(address indexed redeemer, uint256 redeemAmount); + + /// State vars + address public liquityModuleAMO; + address public stakingToken = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; // LUSD + address rewardToken = 0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D; // LQTY + + IStabilityPool public stabilityPool = IStabilityPool(0x66017D22b0f8556afDd19FC67041899Eb65a21bb); + + /// Constructor + constructor(address _moduleManager, address _stablecoin, address _pho) + StablecoinDepositModuleBase(_moduleManager, _stablecoin, _pho) + { + LiquityModuleAMO liquityModuleAMOInstance = new LiquityModuleAMO( + "LQTY-AMO", + "LQTYAMO", + stakingToken, + rewardToken, + msg.sender, + address(this), + _stablecoin + ); + + liquityModuleAMO = address(liquityModuleAMOInstance); + } + + /// @notice user deposits their stablecoin + /// @param depositAmount deposit amount (in stablecoin decimals) + function deposit(uint256 depositAmount) external override nonReentrant { + if (depositAmount == 0) { + revert CannotDepositZero(); + } + uint256 scaledDepositAmount = depositAmount; + + // Call AMO - which transfers LUSD from caller + IModuleAMO(liquityModuleAMO).stakeFor(msg.sender, depositAmount); + + issuedAmount[msg.sender] += scaledDepositAmount; + + // mint PHO + moduleManager.mintPHO(msg.sender, scaledDepositAmount); + + emit Deposited(msg.sender, depositAmount, scaledDepositAmount); + } + + /// @notice user redeems PHO for LUSD + function redeem() external nonReentrant { + uint256 redeemAmount = issuedAmount[msg.sender]; + if (redeemAmount == 0) { + revert CannotRedeemZeroTokens(); + } + + issuedAmount[msg.sender] -= redeemAmount; + + // burn PHO + moduleManager.burnPHO(msg.sender, redeemAmount); + + // scale if decimals < 18 + uint256 scaledRedeemAmount = redeemAmount; + scaledRedeemAmount = redeemAmount / (10 ** (18 - stablecoinDecimals)); + + // Note: Always a full withdrawal + IModuleAMO(liquityModuleAMO).withdrawAllFor(msg.sender); + + emit Redeemed(msg.sender, redeemAmount); + } +} diff --git a/src/modules/liquityModule/LiquityModuleAMO.sol b/src/modules/liquityModule/LiquityModuleAMO.sol new file mode 100644 index 0000000..1318886 --- /dev/null +++ b/src/modules/liquityModule/LiquityModuleAMO.sol @@ -0,0 +1,220 @@ +// 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/IStabilityPool.sol"; +import "forge-std/console2.sol"; + +/// @title LiquityModuleAMO +/// @notice Liquity Module AMO +/// @author Ekonomia: https://github.com/Ekonomia +contract LiquityModuleAMO is IModuleAMO, ERC20 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // Errors + error CannotReceiveZeroMPT(); + 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 LQTY + uint256 private _totalEthRewards; // rewards in ETH + + 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 lqty = IERC20(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D); + IStabilityPool public stabilityPool = IStabilityPool(0x66017D22b0f8556afDd19FC67041899Eb65a21bb); + + // 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 LiquityRewardsReceived(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 + ) ERC20(_name, _symbol) { + if ( + _stakingToken == address(0) || _rewardToken == address(0) || _operator == address(0) + || _module == address(0) || _depositToken == address(0) + ) { + revert ZeroAddressDetected(); + } + stakingToken = _stakingToken; + rewardToken = _rewardToken; + operator = _operator; + module = _module; + depositToken = _depositToken; + } + + /// @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 user + IERC20(depositToken).safeTransferFrom(account, address(this), amount); + + stabilityPool.provideToSP(amount, address(0)); + + 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 pool + stabilityPool.withdrawFromSP(amount); + + uint256 shares = _trackWithdrawShares(account); + _totalDeposits -= depositAmount; + + // Transfer depositToken to caller + IERC20(depositToken).transfer(account, depositAmount); + 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 from Liquity + function getRewardLiquity() external onlyOperator returns (uint256) { + uint256 liquityBalanceBefore = lqty.balanceOf(address(this)); + uint256 ethBalanceBefore = address(this).balance; + // Withdraw minimum amount to force LQTY and ETH to be claimed + if (stabilityPool.getCompoundedLUSDDeposit(address(this)) > 0) { + stabilityPool.withdrawFromSP(0); + } + uint256 liquityBalanceAfter = lqty.balanceOf(address(this)); + uint256 ethBalanceAfter = address(this).balance; + _totalRewards = liquityBalanceAfter - liquityBalanceBefore; + emit LiquityRewardsReceived(_totalRewards); + _totalEthRewards = ethBalanceAfter - ethBalanceBefore; + 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/liquityModule/interfaces/IStabilityPool.sol b/src/modules/liquityModule/interfaces/IStabilityPool.sol new file mode 100644 index 0000000..3f3d0c2 --- /dev/null +++ b/src/modules/liquityModule/interfaces/IStabilityPool.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +// Inspired from: https://github.com/liquity/dev/blob/172b7e743dd1859cdadab6e38075ddc4f70b7599/packages/contracts/contracts/Interfaces/IStabilityPool.sol + +/* + * The Stability Pool holds LUSD tokens deposited by Stability Pool depositors. + * + * When a trove is liquidated, then depending on system conditions, some of its LUSD debt gets offset with + * LUSD in the Stability Pool: that is, the offset debt evaporates, and an equal amount of LUSD tokens in the Stability Pool is burned. + * + * Thus, a liquidation causes each depositor to receive a LUSD loss, in proportion to their deposit as a share of total deposits. + * They also receive an ETH gain, as the ETH collateral of the liquidated trove is distributed among Stability depositors, + * in the same proportion. + * + * When a liquidation occurs, it depletes every deposit by the same fraction: for example, a liquidation that depletes 40% + * of the total LUSD in the Stability Pool, depletes 40% of each deposit. + * + * A deposit that has experienced a series of liquidations is termed a "compounded deposit": each liquidation depletes the deposit, + * multiplying it by some factor in range ]0,1[ + * + * Please see the implementation spec in the proof document, which closely follows on from the compounded deposit / ETH gain derivations: + * https://github.com/liquity/liquity/blob/master/papers/Scalable_Reward_Distribution_with_Compounding_Stakes.pdf + * + * --- LQTY ISSUANCE TO STABILITY POOL DEPOSITORS --- + * + * An LQTY issuance event occurs at every deposit operation, and every liquidation. + * + * Each deposit is tagged with the address of the front end through which it was made. + * + * All deposits earn a share of the issued LQTY in proportion to the deposit as a share of total deposits. The LQTY earned + * by a given deposit, is split between the depositor and the front end through which the deposit was made, based on the front end's kickbackRate. + * + * Please see the system Readme for an overview: + * https://github.com/liquity/dev/blob/main/README.md#lqty-issuance-to-stability-providers + */ +interface IStabilityPool { + // --- Events --- + + event StabilityPoolETHBalanceUpdated(uint256 _newBalance); + event StabilityPoolLUSDBalanceUpdated(uint256 _newBalance); + + event BorrowerOperationsAddressChanged(address _newBorrowerOperationsAddress); + event TroveManagerAddressChanged(address _newTroveManagerAddress); + event ActivePoolAddressChanged(address _newActivePoolAddress); + event DefaultPoolAddressChanged(address _newDefaultPoolAddress); + event LUSDTokenAddressChanged(address _newLUSDTokenAddress); + event SortedTrovesAddressChanged(address _newSortedTrovesAddress); + event PriceFeedAddressChanged(address _newPriceFeedAddress); + event CommunityIssuanceAddressChanged(address _newCommunityIssuanceAddress); + + event P_Updated(uint256 _P); + event S_Updated(uint256 _S, uint128 _epoch, uint128 _scale); + event G_Updated(uint256 _G, uint128 _epoch, uint128 _scale); + event EpochUpdated(uint128 _currentEpoch); + event ScaleUpdated(uint128 _currentScale); + + event FrontEndRegistered(address indexed _frontEnd, uint256 _kickbackRate); + event FrontEndTagSet(address indexed _depositor, address indexed _frontEnd); + + event DepositSnapshotUpdated(address indexed _depositor, uint256 _P, uint256 _S, uint256 _G); + event FrontEndSnapshotUpdated(address indexed _frontEnd, uint256 _P, uint256 _G); + event UserDepositChanged(address indexed _depositor, uint256 _newDeposit); + event FrontEndStakeChanged( + address indexed _frontEnd, uint256 _newFrontEndStake, address _depositor + ); + + event ETHGainWithdrawn(address indexed _depositor, uint256 _ETH, uint256 _LUSDLoss); + event LQTYPaidToDepositor(address indexed _depositor, uint256 _LQTY); + event LQTYPaidToFrontEnd(address indexed _frontEnd, uint256 _LQTY); + event EtherSent(address _to, uint256 _amount); + + // --- Functions --- + + /* + * Called only once on init, to set addresses of other Liquity contracts + * Callable only by owner, renounces ownership at the end + */ + function setAddresses( + address _borrowerOperationsAddress, + address _troveManagerAddress, + address _activePoolAddress, + address _lusdTokenAddress, + address _sortedTrovesAddress, + address _priceFeedAddress, + address _communityIssuanceAddress + ) external; + + /* + * Initial checks: + * - Frontend is registered or zero address + * - Sender is not a registered frontend + * - _amount is not zero + * --- + * - Triggers a LQTY issuance, based on time passed since the last issuance. The LQTY issuance is shared between *all* depositors and front ends + * - Tags the deposit with the provided front end tag param, if it's a new deposit + * - Sends depositor's accumulated gains (LQTY, ETH) to depositor + * - Sends the tagged front end's accumulated LQTY gains to the tagged front end + * - Increases deposit and tagged front end's stake, and takes new snapshots for each. + */ + function provideToSP(uint256 _amount, address _frontEndTag) external; + + /* + * Initial checks: + * - _amount is zero or there are no under collateralized troves left in the system + * - User has a non zero deposit + * --- + * - Triggers a LQTY issuance, based on time passed since the last issuance. The LQTY issuance is shared between *all* depositors and front ends + * - Removes the deposit's front end tag if it is a full withdrawal + * - Sends all depositor's accumulated gains (LQTY, ETH) to depositor + * - Sends the tagged front end's accumulated LQTY gains to the tagged front end + * - Decreases deposit and tagged front end's stake, and takes new snapshots for each. + * + * If _amount > userDeposit, the user withdraws all of their compounded deposit. + */ + function withdrawFromSP(uint256 _amount) external; + + /* + * Initial checks: + * - User has a non zero deposit + * - User has an open trove + * - User has some ETH gain + * --- + * - Triggers a LQTY issuance, based on time passed since the last issuance. The LQTY issuance is shared between *all* depositors and front ends + * - Sends all depositor's LQTY gain to depositor + * - Sends all tagged front end's LQTY gain to the tagged front end + * - Transfers the depositor's entire ETH gain from the Stability Pool to the caller's trove + * - Leaves their compounded deposit in the Stability Pool + * - Updates snapshots for deposit and tagged front end stake + */ + function withdrawETHGainToTrove(address _upperHint, address _lowerHint) external; + + /* + * Initial checks: + * - Frontend (sender) not already registered + * - User (sender) has no deposit + * - _kickbackRate is in the range [0, 100%] + * --- + * Front end makes a one-time selection of kickback rate upon registering + */ + function registerFrontEnd(uint256 _kickbackRate) external; + + /* + * Initial checks: + * - Caller is TroveManager + * --- + * Cancels out the specified debt against the LUSD contained in the Stability Pool (as far as possible) + * and transfers the Trove's ETH collateral from ActivePool to StabilityPool. + * Only called by liquidation functions in the TroveManager. + */ + function offset(uint256 _debt, uint256 _coll) external; + + /* + * Returns the total amount of ETH held by the pool, accounted in an internal variable instead of `balance`, + * to exclude edge cases like ETH received from a self-destruct. + */ + function getETH() external view returns (uint256); + + /* + * Returns LUSD held in the pool. Changes when users deposit/withdraw, and when Trove debt is offset. + */ + function getTotalLUSDDeposits() external view returns (uint256); + + /* + * Calculates the ETH gain earned by the deposit since its last snapshots were taken. + */ + function getDepositorETHGain(address _depositor) external view returns (uint256); + + /* + * Calculate the LQTY gain earned by a deposit since its last snapshots were taken. + * If not tagged with a front end, the depositor gets a 100% cut of what their deposit earned. + * Otherwise, their cut of the deposit's earnings is equal to the kickbackRate, set by the front end through + * which they made their deposit. + */ + function getDepositorLQTYGain(address _depositor) external view returns (uint256); + + /* + * Return the LQTY gain earned by the front end. + */ + function getFrontEndLQTYGain(address _frontEnd) external view returns (uint256); + + /* + * Return the user's compounded deposit. + */ + function getCompoundedLUSDDeposit(address _depositor) external view returns (uint256); + + /* + * Return the front end's compounded stake. + * + * The front end's compounded stake is equal to the sum of its depositors' compounded deposits. + */ + function getCompoundedFrontEndStake(address _frontEnd) external view returns (uint256); + + /* + * Fallback function + * Only callable by Active Pool, it just accounts for ETH received + * receive() external payable; + */ +} diff --git a/src/modules/stablecoinDepositModule/StablecoinDepositModule.sol b/src/modules/stablecoinDepositModule/StablecoinDepositModule.sol index 1a5604a..73cdecd 100644 --- a/src/modules/stablecoinDepositModule/StablecoinDepositModule.sol +++ b/src/modules/stablecoinDepositModule/StablecoinDepositModule.sol @@ -2,62 +2,28 @@ pragma solidity ^0.8.13; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "@protocol/interfaces/IPHO.sol"; -import "@protocol/interfaces/IModuleManager.sol"; +import "@modules/stablecoinDepositModule/StablecoinDepositModuleBase.sol"; /// @title StablecoinDepositModule /// @author Ekonomia: https://github.com/ekonomia-tech -/// @notice Accepts specific stablecoin 1:1 i.e. LUSD, DAI, etc. -contract StablecoinDepositModule is Ownable, ReentrancyGuard { +/// @notice Accepts specific stablecoin 1:1 i.e. USDC, DAI, etc. +contract StablecoinDepositModule is StablecoinDepositModuleBase { using SafeERC20 for IERC20Metadata; - /// Errors - error ZeroAddressDetected(); - error CannotRedeemMoreThanDeposited(); - error OverEighteenDecimals(); - /// Events - event StablecoinDeposited(address indexed depositor, uint256 depositAmount); - event PHORedeemed(address indexed redeemer, uint256 redeemAmount); - - /// State vars - IModuleManager public moduleManager; - IERC20Metadata public stablecoin; - uint256 public stablecoinDecimals; - address public kernel; - IPHO public pho; - mapping(address => uint256) public issuedAmount; - - modifier onlyModuleManager() { - require(msg.sender == address(moduleManager), "Only ModuleManager"); - _; - } + event Deposited(address indexed depositor, uint256 depositAmount, uint256 phoMinted); + event Redeemed(address indexed redeemer, uint256 redeemAmount, uint256 stablecoinTransferred); /// Constructor - constructor(address _moduleManager, address _stablecoin, address _kernel, address _pho) { - if ( - _moduleManager == address(0) || _stablecoin == address(0) || _kernel == address(0) - || _pho == address(0) - ) { - revert ZeroAddressDetected(); - } - moduleManager = IModuleManager(_moduleManager); - stablecoin = IERC20Metadata(_stablecoin); - stablecoinDecimals = stablecoin.decimals(); - if (stablecoinDecimals > 18) { - revert OverEighteenDecimals(); - } - kernel = _kernel; - pho = IPHO(_pho); - } + constructor(address _moduleManager, address _stablecoin, address _pho) + StablecoinDepositModuleBase(_moduleManager, _stablecoin, _pho) + {} /// @notice user deposits their stablecoin /// @param depositAmount deposit amount (in stablecoin decimals) - function depositStablecoin(uint256 depositAmount) external nonReentrant { + function deposit(uint256 depositAmount) external override nonReentrant { // scale if decimals < 18 uint256 scaledDepositAmount = depositAmount; scaledDepositAmount = depositAmount * (10 ** (18 - stablecoinDecimals)); @@ -69,13 +35,12 @@ contract StablecoinDepositModule is Ownable, ReentrancyGuard { // mint PHO moduleManager.mintPHO(msg.sender, scaledDepositAmount); - - emit StablecoinDeposited(msg.sender, depositAmount); + emit Deposited(msg.sender, depositAmount, scaledDepositAmount); } /// @notice user redeems PHO for their original stablecoin /// @param redeemAmount redeem amount in terms of PHO, which is 18 decimals - function redeemStablecoin(uint256 redeemAmount) external nonReentrant { + function redeem(uint256 redeemAmount) external override nonReentrant { if (redeemAmount > issuedAmount[msg.sender]) { revert CannotRedeemMoreThanDeposited(); } @@ -91,7 +56,6 @@ contract StablecoinDepositModule is Ownable, ReentrancyGuard { // transfer stablecoin to caller stablecoin.transfer(msg.sender, scaledRedeemAmount); - - emit PHORedeemed(msg.sender, redeemAmount); + emit Redeemed(msg.sender, redeemAmount, scaledRedeemAmount); } } diff --git a/src/modules/stablecoinDepositModule/StablecoinDepositModuleBase.sol b/src/modules/stablecoinDepositModule/StablecoinDepositModuleBase.sol new file mode 100644 index 0000000..8d8e6af --- /dev/null +++ b/src/modules/stablecoinDepositModule/StablecoinDepositModuleBase.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@protocol/interfaces/IPHO.sol"; +import "@protocol/interfaces/IModuleManager.sol"; + +/// @title StablecoinDepositModuleBase +/// @author Ekonomia: https://github.com/ekonomia-tech +/// @notice Accepts specific stablecoin 1:1 i.e. USDC, DAI, etc. +abstract contract StablecoinDepositModuleBase is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20Metadata; + + /// Errors + error ZeroAddressDetected(); + error CannotRedeemMoreThanDeposited(); + error OverEighteenDecimals(); + + /// State vars + IModuleManager public moduleManager; + IERC20Metadata public stablecoin; + uint256 public stablecoinDecimals; + IPHO public pho; + mapping(address => uint256) public issuedAmount; + + modifier onlyModuleManager() { + require(msg.sender == address(moduleManager), "Only ModuleManager"); + _; + } + + /// Constructor + constructor(address _moduleManager, address _stablecoin, address _pho) { + if (_moduleManager == address(0) || _stablecoin == address(0) || _pho == address(0)) { + revert ZeroAddressDetected(); + } + moduleManager = IModuleManager(_moduleManager); + stablecoin = IERC20Metadata(_stablecoin); + stablecoinDecimals = stablecoin.decimals(); + if (stablecoinDecimals > 18) { + revert OverEighteenDecimals(); + } + pho = IPHO(_pho); + } + + /// @notice user deposits their stablecoin + /// @param depositAmount deposit amount (in stablecoin decimals) + function deposit(uint256 depositAmount) external virtual nonReentrant {} + + /// @notice user redeems PHO for their original stablecoin + /// @param redeemAmount redeem amount in terms of PHO, which is 18 decimals + function redeem(uint256 redeemAmount) external virtual nonReentrant {} +} diff --git a/test/BaseSetup.t.sol b/test/BaseSetup.t.sol index 2eaffd9..7fe1b7d 100644 --- a/test/BaseSetup.t.sol +++ b/test/BaseSetup.t.sol @@ -29,6 +29,7 @@ abstract contract BaseSetup is Test { DummyOracle public priceOracle; ChainlinkPriceFeed public priceFeed; IERC20 dai; + IERC20 lusd; IUSDC usdc; IERC20 frax; IERC20 mpl; @@ -53,6 +54,7 @@ abstract contract BaseSetup is Test { address public richGuy = 0x72A53cDBBcc1b9efa39c834A540550e23463AAcB; address public mplWhale = 0xd6d4Bcde6c816F17889f1Dd3000aF0261B03a196; address public daiWhale = 0xc08a8a9f809107c5A7Be6d90e315e4012c99F39a; + address public lusdWhale = 0x7C22547779c8aa41bAE79E03E8383a0BefBCecf0; address public wethWhale = 0x2F0b23f53734252Bda2277357e97e1517d6B042A; address public fraxBPLPToken = 0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC; address public fraxBPAddress = 0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2; @@ -70,6 +72,7 @@ abstract contract BaseSetup is Test { address public constant FRAXBP_LUSD = 0x497CE58F34605B9944E6b15EcafE6b001206fd25; address public constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant DAI_ADDRESS = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address public constant LUSD_ADDRESS = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; address public constant ETH_NULL_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public constant PRICEFEED_ETHUSD = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; @@ -145,6 +148,7 @@ abstract contract BaseSetup is Test { dai = IERC20(DAI_ADDRESS); usdc = IUSDC(USDC_ADDRESS); dai = IERC20(DAI_ADDRESS); + lusd = IERC20(LUSD_ADDRESS); frax = IERC20(FRAX_ADDRESS); mpl = IERC20(MPL_ADDRESS); @@ -209,6 +213,26 @@ abstract contract BaseSetup is Test { _approveDAI(_owner, _spender, _amountOut); } + function _getLUSD(address to, uint256 _amount) internal { + vm.prank(lusdWhale); + lusd.transfer(to, _amount); + } + + function _approveLUSD(address _owner, address _spender, uint256 _amount) internal { + vm.prank(_owner); + lusd.approve(_spender, _amount); + } + + function _fundAndApproveLUSD( + address _owner, + address _spender, + uint256 _amountIn, + uint256 _amountOut + ) internal { + _getLUSD(_owner, _amountIn); + _approveLUSD(_owner, _spender, _amountOut); + } + function _getTON(address _to, uint256 _amount) internal { vm.prank(owner); ton.transfer(_to, _amount); diff --git a/test/modules/liquityModule/LiquityDepositModule.t.sol b/test/modules/liquityModule/LiquityDepositModule.t.sol new file mode 100644 index 0000000..d9c13bc --- /dev/null +++ b/test/modules/liquityModule/LiquityDepositModule.t.sol @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.13; + +import "../../BaseSetup.t.sol"; +import "@modules/liquityModule/LiquityDepositModule.sol"; +import "@modules/liquityModule/LiquityModuleAMO.sol"; +import "@modules/interfaces/IModuleAMO.sol"; + +contract LiquityDepositModuleTest is BaseSetup { + /// Errors + error ZeroAddressDetected(); + error CannotDepositZero(); + error CannotRedeemZeroTokens(); + + /// Events + event Deposited(address indexed depositor, uint256 depositAmount, uint256 phoMinted); + event Redeemed(address indexed redeemer, uint256 redeemAmount); + + // Track balance for stablecoins and PHO + struct LiquityBalance { + uint256 userStablecoinBalance; + uint256 moduleStablecoinBalance; + uint256 userPHOBalance; + uint256 userIssuedAmount; + uint256 userStakedAmount; + uint256 totalPHOSupply; + uint256 liquityPoolDeposits; + uint256 liquityPoolDepositorLQTYGain; + } + + struct RewardsVars { + uint256 rewardPerToken; + uint256 userRewardPerTokenPaid; + uint256 lastUpdateTime; + uint256 lastTimeRewardApplicable; + uint256 periodFinish; + uint256 blockTimestamp; + } + + struct SharesVars { + uint256 shares; + uint256 earned; + uint256 totalShares; + } + + // Module + LiquityDepositModule public liquityDepositModule; + + // Global + uint256 public constant mplGlobalLpCooldownPeriod = 864000; + uint256 public constant mplGlobalLpWithdrawWindow = 172800; + uint256 public moduleDelay; + address public stakingToken = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; // LUSD + address rewardToken = 0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D; // LQTY + + function setUp() public { + // Liquity module + vm.prank(owner); + liquityDepositModule = new LiquityDepositModule( + address(moduleManager), + address(lusd), + address(pho) + ); + + // Add module to ModuleManager + vm.startPrank(PHOGovernance); + moduleManager.addModule(address(liquityDepositModule)); + vm.stopPrank(); + + // Increase PHO ceilings for modules + vm.startPrank(TONGovernance); + moduleManager.setPHOCeilingForModule(address(liquityDepositModule), ONE_MILLION_D18); + vm.stopPrank(); + + moduleDelay = moduleManager.moduleDelay(); + + vm.warp(block.timestamp + moduleDelay); + + moduleManager.executeCeilingUpdate(address(liquityDepositModule)); + + // Fund users 1 & 2 with LUSD + vm.startPrank(lusdWhale); + lusd.transfer(user1, TEN_THOUSAND_D18); + lusd.transfer(user2, TEN_THOUSAND_D18); + + // Also fund module with some LUSD + lusd.approve(liquityDepositModule.liquityModuleAMO(), TEN_THOUSAND_D18); + lusd.transfer(liquityDepositModule.liquityModuleAMO(), TEN_THOUSAND_D18); + vm.stopPrank(); + + // Mint PHO to users 1 & 2 + vm.prank(address(moduleManager)); + kernel.mintPHO(address(user1), ONE_HUNDRED_D18); + vm.prank(address(moduleManager)); + kernel.mintPHO(address(user2), ONE_HUNDRED_D18); + + // Approve sending LUSD to LiquityDeposit contract - user 1 + vm.startPrank(user1); + lusd.approve(address(liquityDepositModule), TEN_THOUSAND_D18); + + // Do same for liquity AMO + lusd.approve(address(liquityDepositModule.liquityModuleAMO()), TEN_THOUSAND_D18); + + // Allow sending PHO (redemptions) to LiquityDeposit contracts + pho.approve(address(liquityDepositModule), TEN_THOUSAND_D18); + + // Approve PHO burnFrom() via moduleManager calling kernel + pho.approve(address(kernel), ONE_MILLION_D18); + vm.stopPrank(); + + // Approve sending LUSD to LiquityDeposit contract - user 2 + vm.startPrank(user2); + lusd.approve(address(liquityDepositModule), TEN_THOUSAND_D18); + + // Do same for liquity AMO + lusd.approve(address(liquityDepositModule.liquityModuleAMO()), TEN_THOUSAND_D18); + + // Allow sending PHO (redemptions) to LiquityDeposit contracts + pho.approve(address(liquityDepositModule), TEN_THOUSAND_D18); + + // Approve PHO burnFrom() via moduleManager calling kernel + pho.approve(address(kernel), ONE_MILLION_D18); + vm.stopPrank(); + + // Allow sending PHO (redemptions) to LiquityDeposit contracts + pho.approve(address(liquityDepositModule), TEN_THOUSAND_D18); + + // Approve PHO burnFrom() via moduleManager calling kernel + pho.approve(address(kernel), ONE_MILLION_D18); + vm.stopPrank(); + } + + // Cannot set any 0 addresses for constructor + function testCannotMakeLiquityDepositModuleWithZeroAddress() public { + vm.startPrank(user1); + + vm.expectRevert(abi.encodeWithSelector(ZeroAddressDetected.selector)); + liquityDepositModule = new LiquityDepositModule( + address(0), + address(lusd), + address(pho) + ); + + vm.expectRevert(abi.encodeWithSelector(ZeroAddressDetected.selector)); + liquityDepositModule = new LiquityDepositModule( + address(moduleManager), + address(0), + address(pho) + ); + + vm.expectRevert(abi.encodeWithSelector(ZeroAddressDetected.selector)); + liquityDepositModule = new LiquityDepositModule( + address(moduleManager), + address(lusd), + address(0) + ); + + vm.stopPrank(); + } + + // Cannot deposit 0 + function testCannotDepositZero() public { + uint256 depositAmount = 0; + vm.expectRevert(abi.encodeWithSelector(CannotDepositZero.selector)); + vm.prank(user1); + liquityDepositModule.deposit(depositAmount); + } + + // Test basic deposit + function testDepositLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + _testDepositAnyModule(depositAmount, liquityDepositModule); + } + + // Helper function to test Liquity deposit from any module + function _testDepositAnyModule(uint256 _depositAmount, LiquityDepositModule _module) public { + // Convert expected issue amount based on stablecoin decimals + uint256 scaledDepositAmount = _depositAmount; + + uint256 expectedIssuedAmount = scaledDepositAmount; + + address moduleRewardPool = _module.liquityModuleAMO(); + + // LUSD and PHO balances before + LiquityBalance memory before; + before.userStablecoinBalance = lusd.balanceOf(address(user1)); + before.moduleStablecoinBalance = lusd.balanceOf(address(_module)); + before.userPHOBalance = pho.balanceOf(user1); + before.userIssuedAmount = _module.issuedAmount(user1); + before.userStakedAmount = LiquityModuleAMO(moduleRewardPool).stakedAmount(user1); + before.totalPHOSupply = pho.totalSupply(); + + // Check stability pool + before.liquityPoolDeposits = + _module.stabilityPool().getCompoundedLUSDDeposit(moduleRewardPool); + before.liquityPoolDepositorLQTYGain = + _module.stabilityPool().getDepositorLQTYGain(moduleRewardPool); + + // Deposit + vm.expectEmit(true, true, true, true); + emit Deposited(user1, _depositAmount, expectedIssuedAmount); + vm.prank(user1); + _module.deposit(_depositAmount); + + // DepositToken and PHO balances after + LiquityBalance memory aft; + aft.userStablecoinBalance = lusd.balanceOf(address(user1)); + aft.moduleStablecoinBalance = lusd.balanceOf(address(_module)); + aft.userPHOBalance = pho.balanceOf(user1); + aft.userIssuedAmount = _module.issuedAmount(user1); + aft.userStakedAmount = LiquityModuleAMO(moduleRewardPool).stakedAmount(user1); + aft.totalPHOSupply = pho.totalSupply(); + + // Check stability pool + aft.liquityPoolDeposits = _module.stabilityPool().getCompoundedLUSDDeposit(moduleRewardPool); + aft.liquityPoolDepositorLQTYGain = + _module.stabilityPool().getDepositorLQTYGain(moduleRewardPool); + + // User balance - depositToken down and PHO up + assertEq(aft.userStablecoinBalance + _depositAmount, before.userStablecoinBalance); + assertEq(aft.userPHOBalance, before.userPHOBalance + expectedIssuedAmount); + + // Deposit module balance - depositToken same (goes to Liquity pool) + assertEq(aft.moduleStablecoinBalance, before.moduleStablecoinBalance); + + // Check issued amount goes up + assertEq(aft.userIssuedAmount, before.userIssuedAmount + expectedIssuedAmount); + + // Check staked amount goes up + assertEq(aft.userStakedAmount, before.userStakedAmount + scaledDepositAmount); + + // Check PHO total supply goes up + assertEq(aft.totalPHOSupply, before.totalPHOSupply + expectedIssuedAmount); + + // Check Liquity pool balance goes up + assertEq(aft.liquityPoolDeposits, before.liquityPoolDeposits + scaledDepositAmount); + + // Check Liquity pool LQTY gain is same as before + assertEq(aft.liquityPoolDepositorLQTYGain, before.liquityPoolDepositorLQTYGain); + } + + // Cannot redeem 0 + function testCannotRedeemZero() public { + vm.expectRevert(abi.encodeWithSelector(CannotRedeemZeroTokens.selector)); + vm.prank(user1); + liquityDepositModule.redeem(); + } + + // Test Redeem + function testRedeemLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + uint256 redeemAmount = ONE_HUNDRED_D18; + uint256 withdrawTimestamp = block.timestamp + 10000; + _testDepositAnyModule(depositAmount, liquityDepositModule); + _testRedeemAnyModule(redeemAmount, liquityDepositModule, withdrawTimestamp); + } + + // Helper function to test Liquity redeem from any module + function _testRedeemAnyModule( + uint256 _redeemAmount, + LiquityDepositModule _module, + uint256 withdrawTimestamp + ) public { + // Convert expected issue amount based on stablecoin decimals + uint256 scaledRedeemAmount = _redeemAmount; + + uint256 expectedRedeemAmount = scaledRedeemAmount; + + address moduleRewardPool = _module.liquityModuleAMO(); + + // LUSD and PHO balances before + LiquityBalance memory before; + before.userStablecoinBalance = lusd.balanceOf(address(user1)); + before.moduleStablecoinBalance = lusd.balanceOf(address(_module)); + before.userPHOBalance = pho.balanceOf(user1); + before.userIssuedAmount = _module.issuedAmount(user1); + before.userStakedAmount = LiquityModuleAMO(moduleRewardPool).stakedAmount(user1); + before.totalPHOSupply = pho.totalSupply(); + + // Check stability pool + before.liquityPoolDeposits = + _module.stabilityPool().getCompoundedLUSDDeposit(moduleRewardPool); + before.liquityPoolDepositorLQTYGain = + _module.stabilityPool().getDepositorLQTYGain(moduleRewardPool); + + // Redeem + vm.warp(withdrawTimestamp); + vm.expectEmit(true, true, true, true); + emit Redeemed(user1, before.userIssuedAmount); + vm.prank(user1); + _module.redeem(); + + // DepositToken and PHO balances after + LiquityBalance memory aft; + aft.userStablecoinBalance = lusd.balanceOf(address(user1)); + aft.moduleStablecoinBalance = lusd.balanceOf(address(_module)); + aft.userPHOBalance = pho.balanceOf(user1); + aft.userIssuedAmount = _module.issuedAmount(user1); + aft.userStakedAmount = LiquityModuleAMO(moduleRewardPool).stakedAmount(user1); + aft.totalPHOSupply = pho.totalSupply(); + + // Check stability pool + aft.liquityPoolDeposits = _module.stabilityPool().getCompoundedLUSDDeposit(moduleRewardPool); + aft.liquityPoolDepositorLQTYGain = + _module.stabilityPool().getDepositorLQTYGain(moduleRewardPool); + + // User balance - depositToken up and PHO down + assertEq(aft.userStablecoinBalance, before.userStablecoinBalance + _redeemAmount); + assertEq(aft.userPHOBalance + expectedRedeemAmount, before.userPHOBalance); + + // // Deposit module balance - depositToken same (goes to Liquity pool) + assertEq(aft.moduleStablecoinBalance, before.moduleStablecoinBalance); + + // Check issued amount goes down + assertEq(aft.userIssuedAmount + expectedRedeemAmount, before.userIssuedAmount); + + // Check staked amount goes down + assertEq(aft.userStakedAmount + scaledRedeemAmount, before.userStakedAmount); + + // Check PHO total supply goes down + assertEq(aft.totalPHOSupply + expectedRedeemAmount, before.totalPHOSupply); + + // Check Liquity pool balance goes down + assertEq(aft.liquityPoolDeposits + scaledRedeemAmount, before.liquityPoolDeposits); + + // Check Liquity pool LQTY gain is same as before + assertEq(aft.liquityPoolDepositorLQTYGain, before.liquityPoolDepositorLQTYGain); + } + + // Test Reward + function testRewardLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + _testGetRewardAnyModule(depositAmount, liquityDepositModule); + } + + // Helper function to test Liquity rewards from any module + function _testGetRewardAnyModule(uint256 _depositAmount, LiquityDepositModule _module) public { + address moduleRewardPool = _module.liquityModuleAMO(); + + vm.prank(user1); + _module.deposit(_depositAmount); + + // Advance days to accrue rewards + vm.warp(block.timestamp + 7 days); + + // Get reward + vm.prank(owner); + uint256 rewardsLiquity = LiquityModuleAMO(moduleRewardPool).getRewardLiquity(); + + // User gets the reward + vm.warp(block.timestamp + 1 days); + + vm.prank(user1); + LiquityModuleAMO(moduleRewardPool).getReward(user1); + + uint256 finalUserRewardsBalance = + IERC20(LiquityModuleAMO(moduleRewardPool).rewardToken()).balanceOf(user1); + + // Check that user got rewards and protocol has none + assertTrue(finalUserRewardsBalance > 0); + } + + // Testing shares + + // Test basic shares for deposit - USDC + function testSharesDepositLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + _testSharesDepositAnyModule(depositAmount, address(lusd), liquityDepositModule); + } + + // Helper function to test shares for Liquity deposit from any module + function _testSharesDepositAnyModule( + uint256 _depositAmount, + address _depositToken, + LiquityDepositModule _module + ) public { + LiquityModuleAMO amo = LiquityModuleAMO(_module.liquityModuleAMO()); + + // 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(); + + // Deposit - user 1 + vm.prank(user1); + _module.deposit(_depositAmount); + + // 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 + assertEq(aft1.shares, _depositAmount); + assertEq(aft1.earned, 0); + assertEq(aft1.totalShares, _depositAmount); + + // Deposit - user 2 + vm.prank(user2); + _module.deposit(_depositAmount / 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 + assertEq(aft2.shares, _depositAmount / 5); + assertEq(aft2.earned, 0); + assertEq(aft2.totalShares, _depositAmount + _depositAmount / 5); + } + + // Test Redeem + function testSharesRedeemLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + uint256 redeemAmount = ONE_HUNDRED_D18; + uint256 withdrawTimestamp = block.timestamp + 10 days; + _testSharesDepositAnyModule(depositAmount, address(lusd), liquityDepositModule); + uint256 startingTotalDeposits = depositAmount + depositAmount / 4; + uint256 startingTotalShares = depositAmount + depositAmount / 5; + _testSharesRedeemAnyModule( + redeemAmount, + address(lusd), + liquityDepositModule, + withdrawTimestamp, + startingTotalDeposits, + startingTotalShares + ); + } + + // Helper function to test shares for Liquity redeem from any module + function _testSharesRedeemAnyModule( + uint256 _redeemAmount, + address _depositToken, + LiquityDepositModule _module, + uint256 withdrawTimestamp, + uint256 _startingTotalDeposits, + uint256 _startingTotalShares + ) public { + // Convert expected issue amount based on stablecoin decimals + address moduleRewardPool = _module.liquityModuleAMO(); + + LiquityModuleAMO amo = LiquityModuleAMO(_module.liquityModuleAMO()); + + // 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(withdrawTimestamp); + + // Redeem for user 1 + vm.warp(withdrawTimestamp); + 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 + assertEq(before1.shares, _redeemAmount); + assertEq(before1.earned, 0); + assertEq(before1.totalShares, _startingTotalShares); + assertEq(before2.shares, _redeemAmount / 5); + assertEq(before2.earned, 0); + assertEq(before2.totalShares, _startingTotalShares); + + // Check after state + assertEq(aft1.shares, 0); + assertEq(aft1.earned, 0); + assertEq(aft1.totalShares, _startingTotalShares - _redeemAmount); + assertEq(aft2.shares, 0); + assertEq(aft2.earned, 0); + assertEq(aft2.totalShares, 0); + } + + // Test Reward - USDC + function testSharesRewardLiquityModule() public { + uint256 depositAmount = ONE_HUNDRED_D18; + _testSharesGetRewardAnyModule(depositAmount, liquityDepositModule); + } + + // Helper function to test Liquity rewards from any module + function _testSharesGetRewardAnyModule(uint256 _depositAmount, LiquityDepositModule _module) + public + { + address moduleRewardPool = _module.liquityModuleAMO(); + + LiquityModuleAMO amo = LiquityModuleAMO(_module.liquityModuleAMO()); + + // 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(); + + // Deposit - user 1 and user 2 + vm.prank(user1); + _module.deposit(_depositAmount); + vm.prank(user2); + _module.deposit(_depositAmount / 4); + + // Advance days to accrue rewards + vm.warp(block.timestamp + 10 days); + + // Get reward + vm.prank(owner); + uint256 rewardsLiquity = amo.getRewardLiquity(); + + // 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(); + + // 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); + } +} diff --git a/test/modules/stablecoinDepositModule/StablecoinDepositModule.t.sol b/test/modules/stablecoinDepositModule/StablecoinDepositModule.t.sol index 69e8c9c..1d135a9 100644 --- a/test/modules/stablecoinDepositModule/StablecoinDepositModule.t.sol +++ b/test/modules/stablecoinDepositModule/StablecoinDepositModule.t.sol @@ -12,8 +12,8 @@ contract StablecoinDepositModuleTest is BaseSetup { error OverEighteenDecimals(); /// Events - event StablecoinDeposited(address indexed depositor, uint256 depositAmount); - event PHORedeemed(address indexed redeemer, uint256 redeemAmount); + event Deposited(address indexed depositor, uint256 depositAmount, uint256 phoMinted); + event Redeemed(address indexed redeemer, uint256 redeemAmount, uint256 stablecoinTransferred); // Track balance for stablecoins and PHO struct StablecoinBalance { @@ -34,7 +34,6 @@ contract StablecoinDepositModuleTest is BaseSetup { usdcStablecoinDepositModule = new StablecoinDepositModule( address(moduleManager), address(usdc), - address(kernel), address(pho) ); @@ -42,7 +41,6 @@ contract StablecoinDepositModuleTest is BaseSetup { daiStablecoinDepositModule = new StablecoinDepositModule( address(moduleManager), address(dai), - address(kernel), address(pho) ); @@ -96,7 +94,6 @@ contract StablecoinDepositModuleTest is BaseSetup { usdcStablecoinDepositModule = new StablecoinDepositModule( address(0), address(usdc), - address(kernel), address(pho) ); @@ -105,7 +102,6 @@ contract StablecoinDepositModuleTest is BaseSetup { usdcStablecoinDepositModule = new StablecoinDepositModule( address(moduleManager), address(0), - address(kernel), address(pho) ); @@ -114,16 +110,6 @@ contract StablecoinDepositModuleTest is BaseSetup { usdcStablecoinDepositModule = new StablecoinDepositModule( address(moduleManager), address(usdc), - address(0), - address(pho) - ); - - vm.expectRevert(abi.encodeWithSelector(ZeroAddressDetected.selector)); - vm.prank(user1); - usdcStablecoinDepositModule = new StablecoinDepositModule( - address(moduleManager), - address(usdc), - address(kernel), address(0) ); } @@ -159,9 +145,9 @@ contract StablecoinDepositModuleTest is BaseSetup { // Deposit vm.warp(block.timestamp + moduleDelay + 1); vm.expectEmit(true, true, true, true); - emit StablecoinDeposited(user1, _depositAmount); + emit Deposited(user1, _depositAmount, expectedIssuedAmount); vm.prank(user1); - _module.depositStablecoin(_depositAmount); + _module.deposit(_depositAmount); // Stablecoin and PHO balances after StablecoinBalance memory aft; // note that after is a reserved keyword @@ -190,10 +176,10 @@ contract StablecoinDepositModuleTest is BaseSetup { uint256 depositAmount = ONE_HUNDRED_D6; uint256 redeemAmount = depositAmount * 10 ** 12; vm.prank(user1); - usdcStablecoinDepositModule.depositStablecoin(depositAmount); + usdcStablecoinDepositModule.deposit(depositAmount); vm.expectRevert(abi.encodeWithSelector(CannotRedeemMoreThanDeposited.selector)); vm.prank(user1); - usdcStablecoinDepositModule.redeemStablecoin(2 * redeemAmount); + usdcStablecoinDepositModule.redeem(2 * redeemAmount); } // Test basic redeem with USDC @@ -227,9 +213,9 @@ contract StablecoinDepositModuleTest is BaseSetup { before.totalPHOSupply = pho.totalSupply(); vm.expectEmit(true, true, true, true); - emit PHORedeemed(user1, _redeemAmount); + emit Redeemed(user1, _redeemAmount, expectedStablecoinReturn); vm.prank(user1); - _module.redeemStablecoin(_redeemAmount); + _module.redeem(_redeemAmount); // Stablecoin and PHO balances after StablecoinBalance memory aft; // note that after is a reserved keyword