diff --git a/remappings.txt b/remappings.txt index efea71a1..78eeabcf 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,6 @@ aave-v3-core/=src/core/ aave-v3-periphery/=src/periphery/ solidity-utils/=lib/solidity-utils/src/ forge-std/=lib/forge-std/src/ -ds-test/=lib/forge-std/lib/ds-test/src/ \ No newline at end of file +ds-test/=lib/forge-std/lib/ds-test/src/ +openzeppelin-contracts-upgradeable/=lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ diff --git a/scripts/misc/DeployAaveV3MarketBatchedBase.sol b/scripts/misc/DeployAaveV3MarketBatchedBase.sol index 25af8793..d06d7e11 100644 --- a/scripts/misc/DeployAaveV3MarketBatchedBase.sol +++ b/scripts/misc/DeployAaveV3MarketBatchedBase.sol @@ -38,7 +38,7 @@ abstract contract DeployAaveV3MarketBatchedBase is DeployUtils, MarketInput, Scr metadataReporter.writeJsonReportMarket(report); } - function _loadWarnings(MarketConfig memory config, DeployFlags memory flags) internal view { + function _loadWarnings(MarketConfig memory config, DeployFlags memory flags) internal pure { if (config.paraswapAugustusRegistry == address(0)) { console.log( 'Warning: Paraswap Adapters will be skipped at deployment due missing config.paraswapAugustusRegistry' diff --git a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol index 6d4abb9f..01b456e1 100644 --- a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import '../../interfaces/IMarketReportTypes.sol'; import {TransparentProxyFactory, ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; -import {StaticATokenLM} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {StataTokenV2} from 'aave-v3-periphery/contracts/static-a-token/StataTokenV2.sol'; import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol'; import {IErrors} from '../../interfaces/IErrors.sol'; @@ -17,7 +17,7 @@ contract AaveV3HelpersProcedureTwo is IErrors { staticATokenReport.transparentProxyFactory = address(new TransparentProxyFactory()); staticATokenReport.staticATokenImplementation = address( - new StaticATokenLM(IPool(pool), IRewardsController(rewardsController)) + new StataTokenV2(IPool(pool), IRewardsController(rewardsController)) ); staticATokenReport.staticATokenFactoryImplementation = address( new StaticATokenFactory( diff --git a/src/periphery/contracts/libraries/RayMathExplicitRounding.sol b/src/periphery/contracts/libraries/RayMathExplicitRounding.sol deleted file mode 100644 index 8d3f3dcb..00000000 --- a/src/periphery/contracts/libraries/RayMathExplicitRounding.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.10; - -enum Rounding { - UP, - DOWN -} - -/** - * Simplified version of RayMath that instead of half-up rounding does explicit rounding in a specified direction. - * This is needed to have a 4626 complient implementation, that always predictable rounds in favor of the vault / static a token. - */ -library RayMathExplicitRounding { - uint256 internal constant RAY = 1e27; - uint256 internal constant WAD_RAY_RATIO = 1e9; - - function rayMulRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { - if (a == 0 || b == 0) { - return 0; - } - return (a * b) / RAY; - } - - function rayMulRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { - if (a == 0 || b == 0) { - return 0; - } - return ((a * b) + RAY - 1) / RAY; - } - - function rayDivRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * RAY) / b; - } - - function rayDivRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { - return ((a * RAY) + b - 1) / b; - } - - function rayToWadRoundDown(uint256 a) internal pure returns (uint256) { - return a / WAD_RAY_RATIO; - } -} diff --git a/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol new file mode 100644 index 00000000..651c2fa0 --- /dev/null +++ b/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; + +import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; +import {IERC20AaveLM} from './interfaces/IERC20AaveLM.sol'; + +/** + * @title ERC20AaveLMUpgradeable.sol + * @notice Wrapper smart contract that supports tracking and claiming liquidity mining rewards from the Aave system + * @dev ERC20 extension, so ERC20 initialization should be done by the children contract/s + * @author BGD labs + */ +abstract contract ERC20AaveLMUpgradeable is ERC20Upgradeable, IERC20AaveLM { + using SafeCast for uint256; + + /// @custom:storage-location erc7201:aave-dao.storage.ERC20AaveLM + struct ERC20AaveLMStorage { + address _referenceAsset; // a/v token to track rewards on INCENTIVES_CONTROLLER + address[] _rewardTokens; + mapping(address user => RewardIndexCache cache) _startIndex; + mapping(address user => mapping(address reward => UserRewardsData cache)) _userRewardsData; + } + + // keccak256(abi.encode(uint256(keccak256("aave-dao.storage.ERC20AaveLM")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ERC20AaveLMStorageLocation = + 0x4fad66563f105be0bff96185c9058c4934b504d3ba15ca31e86294f0b01fd200; + + function _getERC20AaveLMStorage() private pure returns (ERC20AaveLMStorage storage $) { + assembly { + $.slot := ERC20AaveLMStorageLocation + } + } + + IRewardsController public immutable INCENTIVES_CONTROLLER; + + constructor(IRewardsController rewardsController) { + INCENTIVES_CONTROLLER = rewardsController; + } + + function __ERC20AaveLM_init(address referenceAsset_) internal onlyInitializing { + __ERC20AaveLM_init_unchained(referenceAsset_); + } + function __ERC20AaveLM_init_unchained(address referenceAsset_) internal onlyInitializing { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._referenceAsset = referenceAsset_; + + if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { + refreshRewardTokens(); + } + } + + ///@inheritdoc IERC20AaveLM + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external { + address msgSender = _msgSender(); + if (msgSender != onBehalfOf && msgSender != INCENTIVES_CONTROLLER.getClaimer(onBehalfOf)) { + revert InvalidClaimer(msgSender); + } + + _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + ///@inheritdoc IERC20AaveLM + function claimRewards(address receiver, address[] memory rewards) external { + _claimRewardsOnBehalf(_msgSender(), receiver, rewards); + } + + ///@inheritdoc IERC20AaveLM + function claimRewardsToSelf(address[] memory rewards) external { + _claimRewardsOnBehalf(_msgSender(), _msgSender(), rewards); + } + + ///@inheritdoc IERC20AaveLM + function refreshRewardTokens() public override { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset($._referenceAsset); + for (uint256 i = 0; i < rewards.length; i++) { + _registerRewardToken(rewards[i]); + } + } + + ///@inheritdoc IERC20AaveLM + function collectAndUpdateRewards(address reward) public returns (uint256) { + if (reward == address(0)) { + return 0; + } + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory assets = new address[](1); + assets[0] = address($._referenceAsset); + + return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); + } + + ///@inheritdoc IERC20AaveLM + function isRegisteredRewardToken(address reward) public view override returns (bool) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._startIndex[reward].isRegistered; + } + + ///@inheritdoc IERC20AaveLM + function getCurrentRewardsIndex(address reward) public view returns (uint256) { + if (address(reward) == address(0)) { + return 0; + } + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex($._referenceAsset, reward); + return nextIndex; + } + + ///@inheritdoc IERC20AaveLM + function getTotalClaimableRewards(address reward) external view returns (uint256) { + if (reward == address(0)) { + return 0; + } + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory assets = new address[](1); + assets[0] = $._referenceAsset; + uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); + return IERC20(reward).balanceOf(address(this)) + freshRewards; + } + + ///@inheritdoc IERC20AaveLM + function getClaimableRewards(address user, address reward) external view returns (uint256) { + return _getClaimableRewards(user, reward, balanceOf(user), getCurrentRewardsIndex(reward)); + } + + ///@inheritdoc IERC20AaveLM + function getUnclaimedRewards(address user, address reward) external view returns (uint256) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._userRewardsData[user][reward].unclaimedRewards; + } + + ///@inheritdoc IERC20AaveLM + function getReferenceAsset() external view returns (address) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._referenceAsset; + } + + ///@inheritdoc IERC20AaveLM + function rewardTokens() external view returns (address[] memory) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._rewardTokens; + } + + /** + * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) + * @param from The address of the sender of tokens + * @param to The address of the receiver of tokens + */ + function _update(address from, address to, uint256 amount) internal virtual override { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + for (uint256 i = 0; i < $._rewardTokens.length; i++) { + address rewardToken = address($._rewardTokens[i]); + uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); + if (from != address(0)) { + _updateUser(from, rewardsIndex, rewardToken); + } + if (to != address(0) && from != to) { + _updateUser(to, rewardsIndex, rewardToken); + } + } + super._update(from, to, amount); + } + + /** + * @notice Adding the pending rewards to the unclaimed for specific user and updating user index + * @param user The address of the user to update + * @param currentRewardsIndex The current rewardIndex + * @param rewardToken The address of the reward token + */ + function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + uint256 balance = balanceOf(user); + if (balance > 0) { + $._userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( + user, + rewardToken, + balance, + currentRewardsIndex + ).toUint128(); + } + $._userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex + .toUint128(); + } + + /** + * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. + * @param balance The balance of the user + * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user + * @param currentRewardsIndex The current rewards index in the system + * @return The amount of pending rewards in WAD + */ + function _getPendingRewards( + uint256 balance, + uint256 rewardsIndexOnLastInteraction, + uint256 currentRewardsIndex + ) internal view returns (uint256) { + if (balance == 0) { + return 0; + } + return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / 10 ** decimals(); + } + + /** + * @notice Compute the claimable rewards for a user + * @param user The address of the user + * @param reward The address of the reward + * @param balance The balance of the user in WAD + * @param currentRewardsIndex The current rewards index + * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) + */ + function _getClaimableRewards( + address user, + address reward, + uint256 balance, + uint256 currentRewardsIndex + ) internal view returns (uint256) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + RewardIndexCache memory rewardsIndexCache = $._startIndex[reward]; + if (!rewardsIndexCache.isRegistered) { + revert RewardNotInitialized(reward); + } + + UserRewardsData memory currentUserRewardsData = $._userRewardsData[user][reward]; + return + currentUserRewardsData.unclaimedRewards + + _getPendingRewards( + balance, + currentUserRewardsData.rewardsIndexOnLastInteraction == 0 + ? rewardsIndexCache.lastUpdatedIndex + : currentUserRewardsData.rewardsIndexOnLastInteraction, + currentRewardsIndex + ); + } + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @param onBehalfOf The address to claim on behalf of + * @param rewards The addresses of the rewards + * @param receiver The address to receive the rewards + */ + function _claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) internal virtual { + for (uint256 i = 0; i < rewards.length; i++) { + if (address(rewards[i]) == address(0)) { + continue; + } + uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); + uint256 balance = balanceOf(onBehalfOf); + uint256 userReward = _getClaimableRewards( + onBehalfOf, + rewards[i], + balance, + currentRewardsIndex + ); + uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); + uint256 unclaimedReward = 0; + + if (userReward > totalRewardTokenBalance) { + totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); + } + + if (userReward > totalRewardTokenBalance) { + unclaimedReward = userReward - totalRewardTokenBalance; + userReward = totalRewardTokenBalance; + } + if (userReward > 0) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); + $ + ._userRewardsData[onBehalfOf][rewards[i]] + .rewardsIndexOnLastInteraction = currentRewardsIndex.toUint128(); + SafeERC20.safeTransfer(IERC20(rewards[i]), receiver, userReward); + } + } + } + + /** + * @notice Initializes a new rewardToken + * @param reward The reward token to be registered + */ + function _registerRewardToken(address reward) internal { + if (isRegisteredRewardToken(reward)) return; + uint256 startIndex = getCurrentRewardsIndex(reward); + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._rewardTokens.push(reward); + $._startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); + + emit RewardTokenRegistered(reward, startIndex); + } +} diff --git a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol new file mode 100644 index 00000000..097e22c5 --- /dev/null +++ b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import {ERC4626Upgradeable, Math, IERC4626} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; +import {SafeERC20, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol'; + +import {IPool, IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPool.sol'; +import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; +import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +import {IAToken} from './interfaces/IAToken.sol'; +import {IERC4626StataToken} from './interfaces/IERC4626StataToken.sol'; + +/** + * @title ERC4626StataTokenUpgradeable + * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive + * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. + * @dev ERC20 extension, so ERC20 initialization should be done by the children contract/s + * @author BGD labs + */ +abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626StataToken { + using Math for uint256; + + /// @custom:storage-location erc7201:aave-dao.storage.ERC4626StataToken + struct ERC4626StataTokenStorage { + IERC20 _aToken; + } + + // keccak256(abi.encode(uint256(keccak256("aave-dao.storage.ERC4626StataToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ERC4626StataTokenStorageLocation = + 0x55029d3f54709e547ed74b2fc842d93107ab1490ab7555dd9dd0bf6451101900; + + function _getERC4626StataTokenStorage() + private + pure + returns (ERC4626StataTokenStorage storage $) + { + assembly { + $.slot := ERC4626StataTokenStorageLocation + } + } + + uint256 public constant RAY = 1e27; + + IPool public immutable POOL; + IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER; + + constructor(IPool pool) { + POOL = pool; + POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); + } + + function __ERC4626StataToken_init(address newAToken) internal onlyInitializing { + IERC20 aTokenUnderlying = __ERC4626StataToken_init_unchained(newAToken); + __ERC4626_init_unchained(aTokenUnderlying); + } + + function __ERC4626StataToken_init_unchained( + address newAToken + ) internal onlyInitializing returns (IERC20) { + // sanity check, to be sure that we support that version of the aToken + address poolOfAToken = IAToken(newAToken).POOL(); + if (poolOfAToken != address(POOL)) revert PoolAddressMismatch(poolOfAToken); + + IERC20 aTokenUnderlying = IERC20(IAToken(newAToken).UNDERLYING_ASSET_ADDRESS()); + + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + $._aToken = IERC20(newAToken); + + SafeERC20.forceApprove(aTokenUnderlying, address(POOL), type(uint256).max); + + return aTokenUnderlying; + } + + ///@inheritdoc IERC4626StataToken + function depositATokens(uint256 assets, address receiver) public returns (uint256) { + uint256 shares = previewDeposit(assets); + _deposit(_msgSender(), receiver, assets, shares, false); + + return shares; + } + + ///@inheritdoc IERC4626StataToken + function depositWithPermit( + uint256 assets, + address receiver, + uint256 deadline, + SignatureParams memory sig, + bool depositToAave + ) public returns (uint256) { + IERC20Permit assetToDeposit = IERC20Permit( + depositToAave ? asset() : address(_getERC4626StataTokenStorage()._aToken) + ); + + try + assetToDeposit.permit(_msgSender(), address(this), assets, deadline, sig.v, sig.r, sig.s) + {} catch {} + + uint256 shares = previewDeposit(assets); + _deposit(_msgSender(), receiver, assets, shares, depositToAave); + return shares; + } + + ///@inheritdoc IERC4626StataToken + function redeemATokens(uint256 shares, address receiver, address owner) public returns (uint256) { + uint256 assets = previewRedeem(shares); + _withdraw(_msgSender(), receiver, owner, assets, shares, false); + + return assets; + } + + ///@inheritdoc IERC4626StataToken + function aToken() public view returns (IERC20) { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + return $._aToken; + } + + ///@inheritdoc IERC4626 + function maxMint(address) public view override returns (uint256) { + uint256 assets = maxDeposit(address(0)); + if (assets == type(uint256).max) return type(uint256).max; + return convertToShares(assets); + } + + ///@inheritdoc IERC4626 + function maxWithdraw(address owner) public view override returns (uint256) { + return convertToAssets(maxRedeem(owner)); + } + + ///@inheritdoc IERC4626 + function maxRedeem(address owner) public view override returns (uint256) { + DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(asset()); + + // if paused or inactive users cannot withdraw underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) + ) { + return 0; + } + + // otherwise users can withdraw up to the available amount + uint256 underlyingTokenBalanceInShares = convertToShares(reserveData.virtualUnderlyingBalance); + uint256 cachedUserBalance = balanceOf(owner); + return + underlyingTokenBalanceInShares >= cachedUserBalance + ? cachedUserBalance + : underlyingTokenBalanceInShares; + } + + ///@inheritdoc IERC4626 + function maxDeposit(address) public view override returns (uint256) { + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(asset()); + + // if inactive, paused or frozen users cannot deposit underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) || + ReserveConfiguration.getFrozen(reserveData.configuration) + ) { + return 0; + } + + uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * + (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); + // if no supply cap deposit is unlimited + if (supplyCap == 0) return type(uint256).max; + + // return remaining supply cap margin + uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + + reserveData.accruedToTreasury).mulDiv(_rate(), RAY, Math.Rounding.Ceil); + return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; + } + + ///@inheritdoc IERC4626StataToken + function latestAnswer() external view returns (int256) { + uint256 aTokenUnderlyingAssetPrice = IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()) + .getAssetPrice(asset()); + // @notice aTokenUnderlyingAssetPrice * rate / RAY + return int256(aTokenUnderlyingAssetPrice.mulDiv(_rate(), RAY, Math.Rounding.Floor)); + } + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares, + bool depositToAave + ) internal virtual { + if (shares == 0) { + revert StaticATokenInvalidZeroShares(); + } + // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the + // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, + // calls the vault, which is assumed not malicious. + // + // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the + // assets are transferred and before the shares are minted, which is a valid state. + // slither-disable-next-line reentrancy-no-eth + + if (depositToAave) { + address cachedAsset = asset(); + SafeERC20.safeTransferFrom(IERC20(cachedAsset), caller, address(this), assets); + POOL.deposit(cachedAsset, assets, address(this), 0); + } else { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + SafeERC20.safeTransferFrom($._aToken, caller, address(this), assets); + } + _mint(receiver, shares); + + emit Deposit(caller, receiver, assets, shares); + } + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal virtual override { + _deposit(caller, receiver, assets, shares, true); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares, + bool withdrawFromAave + ) internal virtual { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the + // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer, + // calls the vault, which is assumed not malicious. + // + // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the + // shares are burned and after the assets are transferred, which is a valid state. + _burn(owner, shares); + if (withdrawFromAave) { + POOL.withdraw(asset(), assets, receiver); + } else { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + SafeERC20.safeTransfer($._aToken, receiver, assets); + } + + emit Withdraw(caller, receiver, owner, assets, shares); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + _withdraw(caller, receiver, owner, assets, shares, true); + } + + function _convertToShares( + uint256 assets, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + // * @notice assets * RAY / exchangeRate + return assets.mulDiv(RAY, _rate(), rounding); + } + + function _convertToAssets( + uint256 shares, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + // * @notice share * exchangeRate / RAY + return shares.mulDiv(_rate(), RAY, rounding); + } + + function _rate() internal view returns (uint256) { + return POOL.getReserveNormalizedIncome(asset()); + } +} diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 1b5ca9f7..b6bd003e 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -17,7 +17,7 @@ The static-a-token contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-462 - **Upgradable by the Aave governance.** Similar to other contracts of the Aave ecosystem, the Level 1 executor (short executor) will be able to add new features to the deployed instances of the `stataTokens`. - **Powered by a stataToken Factory.** Whenever a token will be listed on Aave v3, anybody will be able to call the stataToken Factory to deploy an instance for the new asset, permissionless, but still assuring the code used and permissions are properly configured without any extra headache. -See [IStaticATokenLM.sol](./interfaces/IStaticATokenLM.sol) for detailed method documentation. +See [IStata4626LM.sol](./interfaces/IERC20AaveLM.sol) for detailed method documentation. ## Deployed Addresses @@ -37,71 +37,46 @@ For this project, the security procedures applied/being finished are: - The test suite of the codebase itself. - Certora audit/property checking for all the dynamics of the `stataToken`, including respecting all the specs of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626). -## Upgrade Notes Umbrella +## Upgrade Notes StataTokenV2 ### Inheritance -Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. -The current `Initializable` has been removed in favor of the new [Initializable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/proxy/utils/Initializable.sol) following the [`ERC-7201`](https://eips.ethereum.org/EIPS/eip-7201) standard. -To account for the shift in storage, a new `DeprecationGap` has been introduced to maintain the remaining storage at the current position. +The `StaticATokenLM`(v1) was based on solmate. +To allow more flexibility the new `StataTokenV2`(v2) is based on [open-zeppelin-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) which relies on [`ERC-7201`](https://eips.ethereum.org/EIPS/eip-7201) which isolates storage per contract. -### Misc +The `StataTokenV2` is seperated in 3 different contracts, where `StataTokenV2` inherits `ERC4626StataToken` and `ERC20AaveLM`. -Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. -Potential frontrunning of the permit via mempool observation is unavoidable, but due to wrapping the permit execution in a `try..catch` griefing is impossible. +- `ERC20AaveLM` is an abstract contract implementing the forwarding of liquidity mining from an underlying AaveERC20 - an ERC20 implementing `scaled` functions - to a wrapper contract. +- `ERC4626StataToken` is an abstract contract implementing the [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) methods for an underlying aToken. In addition it adds a `latestAnswer`. +- `StataTokenV2` is the main contract stritching things together, while adding `Pausability`, `Rescuability`, `Permit` and the actual initialization. -### Features +### MetaTransactions + +MetaTransactions have been removed as there was no clear use-case besides permit based deposits ever used. +To account for that specific use-case a dedicated `depositWithPermit` was added. + +### Direct AToken Interaction + +In v1 deposit was overleaded to allow underlying & aToken deposits. +While this appraoch was fine it seemed unclean and caused some confusion with integrators. +Therefore v2 introduces dedicated `depositATokens` and `redeemATokens` methods. #### Rescuable [Rescuable](https://github.com/bgd-labs/solidity-utils/blob/main/src/contracts/utils/Rescuable.sol) has been applied to -the `StaticATokenLM` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. +the `StataTokenV2` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. #### Pausability -The `StaticATokenLM` implements the [PausableUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/utils/PausableUpgradeable.sol) allowing any emergency admin to pause the vault in case of an emergency. +The `StataTokenV2` implements the [PausableUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/utils/PausableUpgradeable.sol) allowing any emergency admin to pause the vault in case of an emergency. As long as the vault is paused, minting, burning, transfers and claiming of rewards is impossible. #### LatestAnswer -While there are already mechanisms to price the `StaticATokenLM` implemented by 3th parties for improved UX/DX the `StaticATokenLM` now exposes `latestAnswer`. -`latestAnswer` returns the asset price priced as `underlying_price * excahngeRate`. +While there are already mechanisms to price the `StataTokenV2` implemented by 3th parties for improved UX/DX the `StataTokenV2` now exposes `latestAnswer`. +`latestAnswer` returns the asset price priced as `underlying_price * exchangeRate`. It is important to note that: - `underlying_price` is fetched from the AaveOracle, which means it is subject to mechanisms implemented by the DAO on top of the Chainlink price feeds. - the `latestAnswer` is a scaled response returning the price in the same denomination as `underlying_price` which means the sprice can be undervalued by up to 1 wei - while this should be obvious deviations in the price - even when limited to 1 wei per share - will compound per full share - -### Storage diff - -``` -git checkout main -forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageBefore.md -git checkout project-a -forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageAfter.md -make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff -``` - -```diff -diff --git a/reports/StaticATokenStorageBefore.md b/reports/StaticATokenStorageAfter.md -index a7e3105..89e0967 100644 ---- a/reports/StaticATokenStorageBefore.md -+++ b/reports/StaticATokenStorageAfter.md -@@ -1,7 +1,6 @@ - | Name | Type | Slot | Offset | Bytes | Contract | - | ------------------ | ------------------------------------------------------------------------------ | ---- | ------ | ----- | ------------------------------------------------------------------------ | --| \_initialized | uint8 | 0 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | --| \_initializing | bool | 0 | 1 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | -+| \_\_deprecated | uint256 | 0 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | name | string | 1 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | -``` - -### Umbrella upgrade plan - -The upgrade can be performed independent(before) from any umbrella changes as it has no dependencies. -The upgrade will need to: - -- upgrade the `StaticATokenFactory` with a new version, replacing the `STATIC_A_TOKEN_IMPL`. -- upgrade existing stata tokens via `upgradeToAndCall` to the new implementation. While the tokens are already initialized, due to changing the `Initializable` the corresponding storage is lost. diff --git a/src/periphery/contracts/static-a-token/StataOracle.sol b/src/periphery/contracts/static-a-token/StataOracle.sol deleted file mode 100644 index 1a715b07..00000000 --- a/src/periphery/contracts/static-a-token/StataOracle.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; -import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; -import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; -import {IStataOracle} from './interfaces/IStataOracle.sol'; - -/** - * @title StataOracle - * @author BGD Labs - * @notice Contract to get asset prices of stata tokens - */ -contract StataOracle is IStataOracle { - /// @inheritdoc IStataOracle - IPool public immutable POOL; - /// @inheritdoc IStataOracle - IAaveOracle public immutable AAVE_ORACLE; - - constructor(IPoolAddressesProvider provider) { - POOL = IPool(provider.getPool()); - AAVE_ORACLE = IAaveOracle(provider.getPriceOracle()); - } - - /// @inheritdoc IStataOracle - function getAssetPrice(address asset) public view returns (uint256) { - address underlying = IERC4626(asset).asset(); - return - (AAVE_ORACLE.getAssetPrice(underlying) * POOL.getReserveNormalizedIncome(underlying)) / 1e27; - } - - /// @inheritdoc IStataOracle - function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory) { - uint256[] memory prices = new uint256[](assets.length); - for (uint256 i = 0; i < assets.length; i++) { - prices[i] = getAssetPrice(assets[i]); - } - return prices; - } -} diff --git a/src/periphery/contracts/static-a-token/StataTokenV2.sol b/src/periphery/contracts/static-a-token/StataTokenV2.sol new file mode 100644 index 00000000..0142fff3 --- /dev/null +++ b/src/periphery/contracts/static-a-token/StataTokenV2.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ERC20Upgradeable, ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; + +import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; +import {ERC4626Upgradeable, ERC4626StataTokenUpgradeable, IPool} from './ERC4626StataTokenUpgradeable.sol'; +import {ERC20AaveLMUpgradeable, IRewardsController} from './ERC20AaveLMUpgradeable.sol'; +import {IStataTokenV2} from './interfaces/IStataTokenV2.sol'; + +contract StataTokenV2 is + ERC20PermitUpgradeable, + ERC20AaveLMUpgradeable, + ERC4626StataTokenUpgradeable, + PausableUpgradeable, + Rescuable, + IStataTokenV2 +{ + constructor( + IPool pool, + IRewardsController rewardsController + ) ERC20AaveLMUpgradeable(rewardsController) ERC4626StataTokenUpgradeable(pool) { + _disableInitializers(); + } + + modifier onlyPauseGuardian() { + if (!canPause(_msgSender())) revert OnlyPauseGuardian(_msgSender()); + _; + } + + function initialize( + address aToken, + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external initializer { + __ERC20_init(staticATokenName, staticATokenSymbol); + __ERC20Permit_init(staticATokenName); + __ERC20AaveLM_init(aToken); + __ERC4626StataToken_init(aToken); + __Pausable_init(); + } + + ///@inheritdoc IStataTokenV2 + function setPaused(bool paused) external onlyPauseGuardian { + if (paused) _pause(); + else _unpause(); + } + + /// @inheritdoc IRescuable + function whoCanRescue() public view override returns (address) { + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); + } + + ///@inheritdoc IStataTokenV2 + function canPause(address actor) public view returns (bool) { + return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); + } + + function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { + /// @notice The initialization of ERC4626Upgradeable already assures that decimal are + /// the same as the underlying asset of the StataTokenV2, e.g. decimals of WETH for stataWETH + return ERC4626Upgradeable.decimals(); + } + + function _claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) internal virtual override whenNotPaused { + super._claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + // @notice to merge inheritance with ERC20AaveLMUpgradeable.sol properly we put + // `whenNotPaused` here instead of using ERC20PausableUpgradeable + function _update( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20AaveLMUpgradeable, ERC20Upgradeable) whenNotPaused { + ERC20AaveLMUpgradeable._update(from, to, amount); + } +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenErrors.sol b/src/periphery/contracts/static-a-token/StaticATokenErrors.sol deleted file mode 100644 index bec417df..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenErrors.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -library StaticATokenErrors { - string public constant INVALID_OWNER = '1'; - string public constant INVALID_EXPIRATION = '2'; - string public constant INVALID_SIGNATURE = '3'; - string public constant INVALID_DEPOSITOR = '4'; - string public constant INVALID_RECIPIENT = '5'; - string public constant INVALID_CLAIMER = '6'; - string public constant ONLY_ONE_AMOUNT_FORMAT_ALLOWED = '7'; - string public constant INVALID_ZERO_AMOUNT = '8'; - string public constant REWARD_NOT_INITIALIZED = '9'; -} diff --git a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol index 4e0f8bd0..91af7f72 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol @@ -5,7 +5,7 @@ import {IPool, DataTypes} from '../../../core/contracts/interfaces/IPool.sol'; import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; -import {StaticATokenLM} from './StaticATokenLM.sol'; +import {StataTokenV2} from './StataTokenV2.sol'; import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; /** @@ -47,18 +47,22 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { address cachedStaticAToken = _underlyingToStaticAToken[underlyings[i]]; if (cachedStaticAToken == address(0)) { DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(underlyings[i]); - require(reserveData.aTokenAddress != address(0), 'UNDERLYING_NOT_LISTED'); + if (reserveData.aTokenAddress == address(0)) + revert NotListedUnderlying(reserveData.aTokenAddress); bytes memory symbol = abi.encodePacked( 'stat', - IERC20Metadata(reserveData.aTokenAddress).symbol() + IERC20Metadata(reserveData.aTokenAddress).symbol(), + 'v2' ); address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( STATIC_A_TOKEN_IMPL, PROXY_ADMIN, abi.encodeWithSelector( - StaticATokenLM.initialize.selector, + StataTokenV2.initialize.selector, reserveData.aTokenAddress, - string(abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name())), + string( + abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name(), ' v2') + ), string(symbol) ), bytes32(uint256(uint160(underlyings[i]))) diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol deleted file mode 100644 index d546fbb5..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ /dev/null @@ -1,622 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; - -import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol'; -import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; -import {ERC20PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PausableUpgradeable.sol'; -import {ERC4626Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; -import {IERC4626} from 'openzeppelin-contracts/contracts/interfaces/IERC4626.sol'; -import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; -import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; -import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; - -import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; -import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; -import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; -import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; -import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; -import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; -import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; -import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; - -import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; -import {IAToken} from './interfaces/IAToken.sol'; -import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATokenLM.sol'; -import {StaticATokenErrors} from './StaticATokenErrors.sol'; -import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.sol'; - -/** - * @title StaticATokenLM - * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive - * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. - * It supports claiming liquidity mining rewards from the Aave system. - * @author BGD labs - */ -contract StaticATokenLM is - ERC20Upgradeable, - ERC20PermitUpgradeable, - ERC20PausableUpgradeable, - ERC4626Upgradeable, - IStaticATokenLM, - Rescuable -{ - using SafeERC20 for IERC20; - using SafeCast for uint256; - using WadRayMath for uint256; - using RayMathExplicitRounding for uint256; - - IPool public immutable POOL; - IPoolAddressesProvider immutable POOL_ADDRESSES_PROVIDER; - IRewardsController public immutable INCENTIVES_CONTROLLER; - - IERC20 internal _aToken; - address internal _aTokenUnderlying; - uint8 internal _decimals; - address[] internal _rewardTokens; - mapping(address user => RewardIndexCache cache) internal _startIndex; - mapping(address user => mapping(address reward => UserRewardsData cache)) - internal _userRewardsData; - - constructor(IPool pool, IRewardsController rewardsController) { - _disableInitializers(); - POOL = pool; - INCENTIVES_CONTROLLER = rewardsController; - POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); - } - - ///@inheritdoc IInitializableStaticATokenLM - function initialize( - address newAToken, - string calldata staticATokenName, - string calldata staticATokenSymbol - ) external initializer { - require(IAToken(newAToken).POOL() == address(POOL)); - __ERC20_init(staticATokenName, staticATokenSymbol); - __ERC20Permit_init(staticATokenName); - _aToken = IERC20(newAToken); - _decimals = IERC20Metadata(address(_aToken)).decimals(); - - _aTokenUnderlying = IAToken(newAToken).UNDERLYING_ASSET_ADDRESS(); - IERC20(_aTokenUnderlying).forceApprove(address(POOL), type(uint256).max); - - if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { - refreshRewardTokens(); - } - - emit Initialized(newAToken, staticATokenName, staticATokenSymbol); - } - - modifier onlyPauseGuardian() { - if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender); - _; - } - - ///@inheritdoc IStaticATokenLM - function canPause(address actor) public view returns (bool) { - return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); - } - - /// @inheritdoc IRescuable - function whoCanRescue() public view override returns (address) { - return POOL_ADDRESSES_PROVIDER.getACLAdmin(); - } - - ///@inheritdoc IERC4626 - function deposit(uint256 assets, address receiver) public override returns (uint256) { - (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); - return shares; - } - - ///@inheritdoc IStaticATokenLM - function deposit( - uint256 assets, - address receiver, - uint16 referralCode, - bool depositToAave - ) external returns (uint256) { - (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, referralCode, depositToAave); - return shares; - } - - ///@inheritdoc IERC4626 - function mint(uint256 shares, address receiver) public override returns (uint256) { - (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); - - return assets; - } - - ///@inheritdoc IERC4626 - function withdraw( - uint256 assets, - address receiver, - address owner - ) public override returns (uint256) { - (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); - - return shares; - } - - ///@inheritdoc IERC4626 - function redeem( - uint256 shares, - address receiver, - address owner - ) public override returns (uint256) { - (, uint256 assets) = _withdraw(owner, receiver, shares, 0, true); - - return assets; - } - - ///@inheritdoc IStaticATokenLM - function redeem( - uint256 shares, - address receiver, - address owner, - bool withdrawFromAave - ) external returns (uint256, uint256) { - return _withdraw(owner, receiver, shares, 0, withdrawFromAave); - } - - ///@inheritdoc IStaticATokenLM - function claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) external { - require( - msg.sender == onBehalfOf || msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), - StaticATokenErrors.INVALID_CLAIMER - ); - _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); - } - - ///@inheritdoc IStaticATokenLM - function claimRewards(address receiver, address[] memory rewards) external { - _claimRewardsOnBehalf(msg.sender, receiver, rewards); - } - - ///@inheritdoc IStaticATokenLM - function claimRewardsToSelf(address[] memory rewards) external { - _claimRewardsOnBehalf(msg.sender, msg.sender, rewards); - } - - /// @inheritdoc IERC20Metadata - function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { - return _decimals; - } - - ///@inheritdoc IStaticATokenLM - function setPaused(bool paused) external onlyPauseGuardian { - if (paused) _pause(); - else _unpause(); - } - - ///@inheritdoc IStaticATokenLM - function refreshRewardTokens() public override { - address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); - for (uint256 i = 0; i < rewards.length; i++) { - _registerRewardToken(rewards[i]); - } - } - - ///@inheritdoc IStaticATokenLM - function collectAndUpdateRewards(address reward) public returns (uint256) { - if (reward == address(0)) { - return 0; - } - - address[] memory assets = new address[](1); - assets[0] = address(_aToken); - - return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); - } - - ///@inheritdoc IStaticATokenLM - function isRegisteredRewardToken(address reward) public view override returns (bool) { - return _startIndex[reward].isRegistered; - } - - ///@inheritdoc IStaticATokenLM - function getCurrentRewardsIndex(address reward) public view returns (uint256) { - if (address(reward) == address(0)) { - return 0; - } - (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex(address(_aToken), reward); - return nextIndex; - } - - ///@inheritdoc IStaticATokenLM - function getTotalClaimableRewards(address reward) external view returns (uint256) { - if (reward == address(0)) { - return 0; - } - - address[] memory assets = new address[](1); - assets[0] = address(_aToken); - uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); - return IERC20(reward).balanceOf(address(this)) + freshRewards; - } - - ///@inheritdoc IStaticATokenLM - function getClaimableRewards(address user, address reward) external view returns (uint256) { - return _getClaimableRewards(user, reward, balanceOf(user), getCurrentRewardsIndex(reward)); - } - - ///@inheritdoc IStaticATokenLM - function getUnclaimedRewards(address user, address reward) external view returns (uint256) { - return _userRewardsData[user][reward].unclaimedRewards; - } - - ///@inheritdoc IStaticATokenLM - function rate() public view returns (uint256) { - return POOL.getReserveNormalizedIncome(_aTokenUnderlying); - } - - ///@inheritdoc IERC4626 - function asset() public view override returns (address) { - return address(_aTokenUnderlying); - } - - ///@inheritdoc IERC4626 - function totalAssets() public view override returns (uint256) { - return _aToken.balanceOf(address(this)); - } - - ///@inheritdoc IStaticATokenLM - function aToken() external view returns (IERC20) { - return _aToken; - } - - ///@inheritdoc IStaticATokenLM - function rewardTokens() external view returns (address[] memory) { - return _rewardTokens; - } - - ///@inheritdoc IERC4626 - function convertToShares(uint256 assets) public view override returns (uint256) { - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function convertToAssets(uint256 shares) public view override returns (uint256) { - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxMint(address) public view override returns (uint256) { - uint256 assets = maxDeposit(address(0)); - if (assets == type(uint256).max) return type(uint256).max; - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxWithdraw(address owner) public view override returns (uint256) { - uint256 shares = maxRedeem(owner); - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxRedeem(address owner) public view override returns (uint256) { - address cachedATokenUnderlying = _aTokenUnderlying; - DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(cachedATokenUnderlying); - - // if paused or inactive users cannot withdraw underlying - if ( - !ReserveConfiguration.getActive(reserveData.configuration) || - ReserveConfiguration.getPaused(reserveData.configuration) - ) { - return 0; - } - - // otherwise users can withdraw up to the available amount - uint256 underlyingTokenBalanceInShares = _convertToShares( - reserveData.virtualUnderlyingBalance, - Rounding.DOWN - ); - uint256 cachedUserBalance = balanceOf(owner); - return - underlyingTokenBalanceInShares >= cachedUserBalance - ? cachedUserBalance - : underlyingTokenBalanceInShares; - } - - ///@inheritdoc IERC4626 - function maxDeposit(address) public view override returns (uint256) { - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(_aTokenUnderlying); - - // if inactive, paused or frozen users cannot deposit underlying - if ( - !ReserveConfiguration.getActive(reserveData.configuration) || - ReserveConfiguration.getPaused(reserveData.configuration) || - ReserveConfiguration.getFrozen(reserveData.configuration) - ) { - return 0; - } - - uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * - (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); - // if no supply cap deposit is unlimited - if (supplyCap == 0) return type(uint256).max; - // return remaining supply cap margin - uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + - reserveData.accruedToTreasury).rayMulRoundUp(_getNormalizedIncome(reserveData)); - return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; - } - - ///@inheritdoc IStaticATokenLM - function latestAnswer() external view returns (int256) { - return - int256( - (IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()).getAssetPrice(_aTokenUnderlying) * - POOL.getReserveNormalizedIncome(_aTokenUnderlying)) / 1e27 - ); - } - - function _deposit( - address depositor, - address receiver, - uint256 _shares, - uint256 _assets, - uint16 referralCode, - bool depositToAave - ) internal returns (uint256, uint256) { - require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); - require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); - - uint256 assets = _assets; - uint256 shares = _shares; - if (shares > 0) { - if (depositToAave) { - require(shares <= maxMint(receiver), 'ERC4626: mint more than max'); - } - assets = previewMint(shares); - } else { - if (depositToAave) { - require(assets <= maxDeposit(receiver), 'ERC4626: deposit more than max'); - } - shares = previewDeposit(assets); - } - require(shares != 0, StaticATokenErrors.INVALID_ZERO_AMOUNT); - - if (depositToAave) { - address cachedATokenUnderlying = _aTokenUnderlying; - IERC20(cachedATokenUnderlying).safeTransferFrom(depositor, address(this), assets); - POOL.deposit(cachedATokenUnderlying, assets, address(this), referralCode); - } else { - _aToken.safeTransferFrom(depositor, address(this), assets); - } - - _mint(receiver, shares); - - emit Deposit(depositor, receiver, assets, shares); - - return (shares, assets); - } - - function _withdraw( - address owner, - address receiver, - uint256 _shares, - uint256 _assets, - bool withdrawFromAave - ) internal returns (uint256, uint256) { - require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); - require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); - require(_shares != _assets, StaticATokenErrors.INVALID_ZERO_AMOUNT); - - uint256 assets = _assets; - uint256 shares = _shares; - - if (shares > 0) { - if (withdrawFromAave) { - require(shares <= maxRedeem(owner), 'ERC4626: redeem more than max'); - } - assets = previewRedeem(shares); - } else { - if (withdrawFromAave) { - require(assets <= maxWithdraw(owner), 'ERC4626: withdraw more than max'); - } - shares = previewWithdraw(assets); - } - - if (msg.sender != owner) { - _spendAllowance(owner, msg.sender, shares); - } - - _burn(owner, shares); - - emit Withdraw(msg.sender, receiver, owner, assets, shares); - - if (withdrawFromAave) { - POOL.withdraw(_aTokenUnderlying, assets, receiver); - } else { - _aToken.safeTransfer(receiver, assets); - } - - return (shares, assets); - } - - /** - * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) - * @param from The address of the sender of tokens - * @param to The address of the receiver of tokens - */ - function _update( - address from, - address to, - uint256 amount - ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) whenNotPaused { - for (uint256 i = 0; i < _rewardTokens.length; i++) { - address rewardToken = address(_rewardTokens[i]); - uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); - if (from != address(0)) { - _updateUser(from, rewardsIndex, rewardToken); - } - if (to != address(0) && from != to) { - _updateUser(to, rewardsIndex, rewardToken); - } - } - super._update(from, to, amount); - } - - /** - * @notice Adding the pending rewards to the unclaimed for specific user and updating user index - * @param user The address of the user to update - * @param currentRewardsIndex The current rewardIndex - * @param rewardToken The address of the reward token - */ - function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { - uint256 balance = balanceOf(user); - if (balance > 0) { - _userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( - user, - rewardToken, - balance, - currentRewardsIndex - ).toUint128(); - } - _userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex - .toUint128(); - } - - /** - * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. - * @param balance The balance of the user - * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user - * @param currentRewardsIndex The current rewards index in the system - * @return The amount of pending rewards in WAD - */ - function _getPendingRewards( - uint256 balance, - uint256 rewardsIndexOnLastInteraction, - uint256 currentRewardsIndex - ) internal view returns (uint256) { - if (balance == 0) { - return 0; - } - return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / 10 ** decimals(); - } - - /** - * @notice Compute the claimable rewards for a user - * @param user The address of the user - * @param reward The address of the reward - * @param balance The balance of the user in WAD - * @param currentRewardsIndex The current rewards index - * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) - */ - function _getClaimableRewards( - address user, - address reward, - uint256 balance, - uint256 currentRewardsIndex - ) internal view returns (uint256) { - RewardIndexCache memory rewardsIndexCache = _startIndex[reward]; - require(rewardsIndexCache.isRegistered == true, StaticATokenErrors.REWARD_NOT_INITIALIZED); - UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; - return - currentUserRewardsData.unclaimedRewards + - _getPendingRewards( - balance, - currentUserRewardsData.rewardsIndexOnLastInteraction == 0 - ? rewardsIndexCache.lastUpdatedIndex - : currentUserRewardsData.rewardsIndexOnLastInteraction, - currentRewardsIndex - ); - } - - /** - * @notice Claim rewards on behalf of a user and send them to a receiver - * @param onBehalfOf The address to claim on behalf of - * @param rewards The addresses of the rewards - * @param receiver The address to receive the rewards - */ - function _claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) internal whenNotPaused { - for (uint256 i = 0; i < rewards.length; i++) { - if (address(rewards[i]) == address(0)) { - continue; - } - uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); - uint256 balance = balanceOf(onBehalfOf); - uint256 userReward = _getClaimableRewards( - onBehalfOf, - rewards[i], - balance, - currentRewardsIndex - ); - uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); - uint256 unclaimedReward = 0; - - if (userReward > totalRewardTokenBalance) { - totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); - } - - if (userReward > totalRewardTokenBalance) { - unclaimedReward = userReward - totalRewardTokenBalance; - userReward = totalRewardTokenBalance; - } - if (userReward > 0) { - _userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); - _userRewardsData[onBehalfOf][rewards[i]].rewardsIndexOnLastInteraction = currentRewardsIndex - .toUint128(); - IERC20(rewards[i]).safeTransfer(receiver, userReward); - } - } - } - - function _convertToShares(uint256 assets, Rounding rounding) internal view returns (uint256) { - if (rounding == Rounding.UP) return assets.rayDivRoundUp(rate()); - return assets.rayDivRoundDown(rate()); - } - - function _convertToAssets(uint256 shares, Rounding rounding) internal view returns (uint256) { - if (rounding == Rounding.UP) return shares.rayMulRoundUp(rate()); - return shares.rayMulRoundDown(rate()); - } - - /** - * @notice Initializes a new rewardToken - * @param reward The reward token to be registered - */ - function _registerRewardToken(address reward) internal { - if (isRegisteredRewardToken(reward)) return; - uint256 startIndex = getCurrentRewardsIndex(reward); - - _rewardTokens.push(reward); - _startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); - - emit RewardTokenRegistered(reward, startIndex); - } - - /** - * Copy of https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ReserveLogic.sol#L47 with memory instead of calldata - * @notice Returns the ongoing normalized income for the reserve. - * @dev A value of 1e27 means there is no income. As time passes, the income is accrued - * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued - * @param reserve The reserve object - * @return The normalized income, expressed in ray - */ - function _getNormalizedIncome( - DataTypes.ReserveDataLegacy memory reserve - ) internal view returns (uint256) { - uint40 timestamp = reserve.lastUpdateTimestamp; - - //solium-disable-next-line - if (timestamp == block.timestamp) { - //if the index was updated in the same block, no need to perform any calculation - return reserve.liquidityIndex; - } else { - return - MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( - reserve.liquidityIndex - ); - } - } -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol b/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol new file mode 100644 index 00000000..79eb163c --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IERC20AaveLM { + struct UserRewardsData { + uint128 rewardsIndexOnLastInteraction; // (in RAYs) + uint128 unclaimedRewards; // (in RAYs) + } + + struct RewardIndexCache { + bool isRegistered; + uint248 lastUpdatedIndex; + } + + error InvalidClaimer(address claimer); + error RewardNotInitialized(address reward); + + event RewardTokenRegistered(address indexed reward, uint256 startIndex); + + /** + * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. + * @param reward The reward to claim + * @return uint256 Amount collected + */ + function collectAndUpdateRewards(address reward) external returns (uint256); + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @dev Only callable by if sender is onBehalfOf or sender is approved claimer + * @param onBehalfOf The address to claim on behalf of + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external; + + /** + * @notice Claim rewards and send them to a receiver + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewards(address receiver, address[] memory rewards) external; + + /** + * @notice Claim rewards + * @param rewards The rewards to claim + */ + function claimRewardsToSelf(address[] memory rewards) external; + + /** + * @notice Get the total claimable rewards of the contract. + * @param reward The reward to claim + * @return uint256 The current balance + pending rewards from the `_incentivesController` + */ + function getTotalClaimableRewards(address reward) external view returns (uint256); + + /** + * @notice Get the total claimable rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The claimable amount of rewards in WAD + */ + function getClaimableRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The unclaimed rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The unclaimed amount of rewards in WAD + */ + function getUnclaimedRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The underlying asset reward index in RAY + * @param reward The reward to claim + * @return uint256 The underlying asset reward index in RAY + */ + function getCurrentRewardsIndex(address reward) external view returns (uint256); + + /** + * @notice Returns reference a/v token address used on INCENTIVES_CONTROLLER for tracking + * @return address of reference token + */ + function getReferenceAsset() external view returns (address); + + /** + * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. + * @return IERC20 The IERC20s of the rewards. + */ + function rewardTokens() external view returns (address[] memory); + + /** + * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. + */ + function refreshRewardTokens() external; + + /** + * @notice Checks if the passed token is a registered reward. + * @param reward The reward to claim + * @return bool signaling if token is a registered reward. + */ + function isRegisteredRewardToken(address reward) external view returns (bool); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol b/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol new file mode 100644 index 00000000..3cc4e9ca --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; + +interface IERC4626StataToken { + struct SignatureParams { + uint8 v; + bytes32 r; + bytes32 s; + } + + error PoolAddressMismatch(address pool); + + error StaticATokenInvalidZeroShares(); + + error OnlyPauseGuardian(address caller); + + /** + * @notice Burns `shares` of static aToken, with receiver receiving the corresponding amount of aToken + * @param shares The shares to withdraw, in static balance of StaticAToken + * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol + * @return amountToWithdraw: aToken send to `receiver`, dynamic balance + **/ + function redeemATokens( + uint256 shares, + address receiver, + address owner + ) external returns (uint256); + + /** + * @notice Deposits aTokens and mints static aTokens to msg.sender + * @param assets The amount of aTokens to deposit (e.g. deposit of 100 aUSDC) + * @param receiver The address that will receive the static aTokens + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function depositATokens(uint256 assets, address receiver) external returns (uint256); + + /** + * @notice Universal deposit method for proving aToken or underlying liquidity with permit + * @param assets The amount of aTokens or underlying to deposit + * @param receiver The address that will receive the static aTokens + * @param deadline Must be a timestamp in the future + * @param sig A `secp256k1` signature params from `msgSender()` + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function depositWithPermit( + uint256 assets, + address receiver, + uint256 deadline, + SignatureParams memory sig, + bool depositToAave + ) external returns (uint256); + + /** + * @notice The aToken used inside the 4626 vault. + * @return IERC20 The aToken IERC20. + */ + function aToken() external view returns (IERC20); + + /** + * @notice Returns the current asset price of the stataToken. + * The price is calculated as `underlying_price * exchangeRate`. + * It is important to note that: + * - `underlying_price` is the price obtained by the aave-oracle and is subject to it's internal pricing mechanisms. + * - as the price is scaled over the exchangeRate, but maintains the same precision as the underlying the price might be underestimated by 1 unit. + * - when pricing multiple `shares` as `shares * price` keep in mind that the error compounds. + * @return price the current asset price. + */ + function latestAnswer() external view returns (int256); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol deleted file mode 100644 index 0eeb8955..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; -import {IAaveIncentivesController} from '../../../../core/contracts/interfaces/IAaveIncentivesController.sol'; - -/** - * @title IInitializableStaticATokenLM - * @notice Interface for the initialize function on StaticATokenLM - * @author Aave - **/ -interface IInitializableStaticATokenLM { - /** - * @dev Emitted when a StaticATokenLM is initialized - * @param aToken The address of the underlying aToken (aWETH) - * @param staticATokenName The name of the Static aToken - * @param staticATokenSymbol The symbol of the Static aToken - **/ - event Initialized(address indexed aToken, string staticATokenName, string staticATokenSymbol); - - /** - * @dev Initializes the StaticATokenLM - * @param aToken The address of the underlying aToken (aWETH) - * @param staticATokenName The name of the Static aToken - * @param staticATokenSymbol The symbol of the Static aToken - */ - function initialize( - address aToken, - string calldata staticATokenName, - string calldata staticATokenSymbol - ) external; -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol b/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol deleted file mode 100644 index acd4fc4f..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; -import {IAaveOracle} from '../../../../core/contracts/interfaces/IAaveOracle.sol'; - -interface IStataOracle { - /** - * @return The pool used for fetching the rate on the aggregator oracle - */ - function POOL() external view returns (IPool); - - /** - * @return The aave oracle used for fetching the price of the underlying - */ - function AAVE_ORACLE() external view returns (IAaveOracle); - - /** - * @notice Returns the prices of an asset address - * @param asset The asset address - * @return The prices of the given asset - */ - function getAssetPrice(address asset) external view returns (uint256); - - /** - * @notice Returns a list of prices from a list of assets addresses - * @param assets The list of assets addresses - * @return The prices of the given assets - */ - function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory); -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol b/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol new file mode 100644 index 00000000..6c5227a8 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC4626StataToken} from './IERC4626StataToken.sol'; +import {IERC20AaveLM} from './IERC20AaveLM.sol'; + +interface IStataTokenV2 is IERC4626StataToken, IERC20AaveLM { + /** + * @notice Checks if the passed actor is permissioned emergency admin. + * @param actor The reward to claim + * @return bool signaling if actor can pause the vault. + */ + function canPause(address actor) external view returns (bool); + + /** + * @notice Pauses/unpauses all system's operations + * @param paused boolean determining if the token should be paused or unpaused + */ + function setPaused(bool paused) external; +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol index 7532e92c..1aee13d4 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.10; interface IStaticATokenFactory { + error NotListedUnderlying(address underlying); + /** * @notice Creates new staticATokens * @param underlyings the addresses of the underlyings to create. diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol deleted file mode 100644 index 026cad99..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; -import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; - -interface IStaticATokenLM is IInitializableStaticATokenLM { - struct SignatureParams { - uint8 v; - bytes32 r; - bytes32 s; - } - - struct PermitParams { - uint256 value; - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; - } - - struct UserRewardsData { - uint128 rewardsIndexOnLastInteraction; // (in RAYs) - uint128 unclaimedRewards; // (in RAYs) - } - - struct RewardIndexCache { - bool isRegistered; - uint248 lastUpdatedIndex; - } - - error OnlyPauseGuardian(address caller); - - event RewardTokenRegistered(address indexed reward, uint256 startIndex); - - /** - * @notice Burns `amount` of static aToken, with receiver receiving the corresponding amount of `ASSET` - * @param shares The amount to withdraw, in static balance of StaticAToken - * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol - * @param withdrawFromAave bool - * - `true` for the receiver to get underlying tokens (e.g. USDC) - * - `false` for the receiver to get aTokens (e.g. aUSDC) - * @return amountToBurn: StaticATokens burnt, static balance - * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance - **/ - function redeem( - uint256 shares, - address receiver, - address owner, - bool withdrawFromAave - ) external returns (uint256, uint256); - - /** - * @notice Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender - * @param assets The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC) - * @param receiver The address that will receive the static aTokens - * @param referralCode Code used to register the integrator originating the operation, for potential rewards. - * 0 if the action is executed directly by the user, without any middle-man - * @param depositToAave bool - * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) - * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) - * @return uint256 The amount of StaticAToken minted, static balance - **/ - function deposit( - uint256 assets, - address receiver, - uint16 referralCode, - bool depositToAave - ) external returns (uint256); - - /** - * @notice Returns the Aave liquidity index of the underlying aToken, denominated rate here - * as it can be considered as an ever-increasing exchange rate - * @return The liquidity index - **/ - function rate() external view returns (uint256); - - /** - * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. - * @param reward The reward to claim - * @return uint256 Amount collected - */ - function collectAndUpdateRewards(address reward) external returns (uint256); - - /** - * @notice Claim rewards on behalf of a user and send them to a receiver - * @dev Only callable by if sender is onBehalfOf or sender is approved claimer - * @param onBehalfOf The address to claim on behalf of - * @param receiver The address to receive the rewards - * @param rewards The rewards to claim - */ - function claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) external; - - /** - * @notice Claim rewards and send them to a receiver - * @param receiver The address to receive the rewards - * @param rewards The rewards to claim - */ - function claimRewards(address receiver, address[] memory rewards) external; - - /** - * @notice Claim rewards - * @param rewards The rewards to claim - */ - function claimRewardsToSelf(address[] memory rewards) external; - - /** - * @notice Get the total claimable rewards of the contract. - * @param reward The reward to claim - * @return uint256 The current balance + pending rewards from the `_incentivesController` - */ - function getTotalClaimableRewards(address reward) external view returns (uint256); - - /** - * @notice Get the total claimable rewards for a user in WAD - * @param user The address of the user - * @param reward The reward to claim - * @return uint256 The claimable amount of rewards in WAD - */ - function getClaimableRewards(address user, address reward) external view returns (uint256); - - /** - * @notice The unclaimed rewards for a user in WAD - * @param user The address of the user - * @param reward The reward to claim - * @return uint256 The unclaimed amount of rewards in WAD - */ - function getUnclaimedRewards(address user, address reward) external view returns (uint256); - - /** - * @notice The underlying asset reward index in RAY - * @param reward The reward to claim - * @return uint256 The underlying asset reward index in RAY - */ - function getCurrentRewardsIndex(address reward) external view returns (uint256); - - /** - * @notice The aToken used inside the 4626 vault. - * @return IERC20 The aToken IERC20. - */ - function aToken() external view returns (IERC20); - - /** - * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. - * @return IERC20 The IERC20s of the rewards. - */ - function rewardTokens() external view returns (address[] memory); - - /** - * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. - */ - function refreshRewardTokens() external; - - /** - * @notice Checks if the passed token is a registered reward. - * @param reward The reward to claim - * @return bool signaling if token is a registered reward. - */ - function isRegisteredRewardToken(address reward) external view returns (bool); - - /** - * @notice Checks if the passed actor is permissioned emergency admin. - * @param actor The reward to claim - * @return bool signaling if actor can pause the vault. - */ - function canPause(address actor) external view returns (bool); - - /** - * @notice Pauses/unpauses all system's operations - * @param paused boolean determining if the token should be paused or unpaused - */ - function setPaused(bool paused) external; - - /** - * @notice Returns the current asset price of the stataToken. - * The price is calculated as `underlying_price * exchangeRate`. - * It is important to note that: - * - `underlying_price` is the price obtained by the aave-oracle and is subject to it's internal pricing mechanisms. - * - as the price is scaled over the exchangeRate, but maintains the same precision as the underlying the price might be underestimated by 1 unit. - * - when pricing multiple `shares` as `shares * price` keep in mind that the error compounds. - * @return price the current asset price. - */ - function latestAnswer() external view returns (int256); -} diff --git a/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index 28d5d8e4..c916c158 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -193,7 +193,7 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function testCheckInitCodeSizeBatchs() public view { + function testCheckInitCodeSizeBatches() public pure { uint16 maxInitCodeSize = 49152; console.log('AaveV3SetupBatch', type(AaveV3SetupBatch).creationCode.length); diff --git a/tests/core/Pool.t.sol b/tests/core/Pool.t.sol index 7ae63020..3f982b2b 100644 --- a/tests/core/Pool.t.sol +++ b/tests/core/Pool.t.sol @@ -667,31 +667,31 @@ contract PoolTests is TestnetProcedures { assertEq(50_000e6, virtualBalance); } - function test_getFlashLoanLogic() public { + function test_getFlashLoanLogic() public view { assertNotEq(pool.getFlashLoanLogic(), address(0)); } - function test_getBorrowLogic() public { + function test_getBorrowLogic() public view { assertNotEq(pool.getBorrowLogic(), address(0)); } - function test_getBridgeLogic() public { + function test_getBridgeLogic() public view { assertNotEq(pool.getBridgeLogic(), address(0)); } - function test_getEModeLogic() public { + function test_getEModeLogic() public view { assertNotEq(pool.getEModeLogic(), address(0)); } - function test_getLiquidationLogic() public { + function test_getLiquidationLogic() public view { assertNotEq(pool.getLiquidationLogic(), address(0)); } - function test_getPoolLogic() public { + function test_getPoolLogic() public view { assertNotEq(pool.getPoolLogic(), address(0)); } - function test_getSupplyLogic() public { + function test_getSupplyLogic() public view { assertNotEq(pool.getSupplyLogic(), address(0)); } diff --git a/tests/core/PoolConfigurator.upgradeabilty.t.sol b/tests/core/PoolConfigurator.upgradeabilty.t.sol index 2c84f556..840a4fff 100644 --- a/tests/core/PoolConfigurator.upgradeabilty.t.sol +++ b/tests/core/PoolConfigurator.upgradeabilty.t.sol @@ -53,7 +53,7 @@ contract PoolConfiguratorUpgradeabilityTests is TestnetProcedures { initTestEnvironment(); } - function test_getConfiguratorLogic() public { + function test_getConfiguratorLogic() public view { assertNotEq(contracts.poolConfiguratorProxy.getConfiguratorLogic(), address(0)); } diff --git a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol new file mode 100644 index 00000000..762c3229 --- /dev/null +++ b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20Errors} from 'openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; +import {ERC20AaveLMUpgradeable, IERC20AaveLM} from '../../../src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol'; +import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {PullRewardsTransferStrategy, ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; +import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +// Minimal mock as contract is abstract +contract MockERC20AaveLMUpgradeable is ERC20AaveLMUpgradeable { + constructor(IRewardsController rewardsController) ERC20AaveLMUpgradeable(rewardsController) {} + + function mockInit(address asset) external initializer { + __ERC20AaveLM_init(asset); + } + + function mint(address user, uint256 amount) external { + _mint(user, amount); + } +} + +contract MockScaledTestnetERC20 is TestnetERC20 { + constructor( + string memory name, + string memory symbol, + uint8 decimals, + address owner + ) TestnetERC20(name, symbol, decimals, owner) {} + + function scaledTotalSupply() external view returns (uint256) { + return totalSupply(); + } + + function scaledBalanceOf(address user) external view returns (uint256) { + return balanceOf(user); + } + + function getScaledUserBalanceAndSupply(address user) external view returns (uint256, uint256) { + return (balanceOf(user), totalSupply()); + } + + function mint(address user, uint256 amount) public override returns (bool) { + _mint(user, amount); + return true; + } +} + +contract ERC20AaveLMUpgradableTest is TestnetProcedures { + MockERC20AaveLMUpgradeable internal lmUpgradeable; + MockScaledTestnetERC20 internal underlying; + + address public user; + uint256 internal userPrivateKey; + + address internal rewardToken; + address internal emissionAdmin; + PullRewardsTransferStrategy strategy; + + function setUp() public virtual { + initTestEnvironment(false); + + emissionAdmin = vm.addr(1024); + + userPrivateKey = 0xA11CE; + user = address(vm.addr(userPrivateKey)); + + underlying = new MockScaledTestnetERC20('Mock underlying', 'UND', 18, poolAdmin); + + lmUpgradeable = new MockERC20AaveLMUpgradeable(contracts.rewardsControllerProxy); + lmUpgradeable.mockInit(address(underlying)); + + rewardToken = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, poolAdmin)); + strategy = new PullRewardsTransferStrategy( + report.rewardsControllerProxy, + emissionAdmin, + emissionAdmin + ); + + vm.prank(poolAdmin); + contracts.emissionManager.setEmissionAdmin(rewardToken, emissionAdmin); + } + + function test_2701() external view { + assertEq( + keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC20AaveLM')) - 1)) & + ~bytes32(uint256(0xff)), + 0x4fad66563f105be0bff96185c9058c4934b504d3ba15ca31e86294f0b01fd200 + ); + } + + function test_noRewardsInitialized() external { + vm.expectRevert( + abi.encodeWithSelector(IERC20AaveLM.RewardNotInitialized.selector, rewardToken) + ); + lmUpgradeable.getClaimableRewards(user, rewardToken); + } + + function test_noopWhenNotInitialized() external { + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + assertEq(lmUpgradeable.getTotalClaimableRewards(rewardToken), 0); + assertEq(lmUpgradeable.collectAndUpdateRewards(rewardToken), 0); + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + } + + function test_claimableRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertLe(claimable, env.emissionDuration * env.emissionPerSecond); + } + + function test_collectAndUpdateRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + uint256 claimable = lmUpgradeable.getTotalClaimableRewards(rewardToken); + lmUpgradeable.collectAndUpdateRewards(rewardToken); + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), claimable); + } + + function test_claimRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewards(address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsToSelf( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewardsToSelf(_getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(user), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsOnBehalfOf_shouldRevertForInvalidClaimer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + vm.expectRevert(abi.encodeWithSelector(IERC20AaveLM.InvalidClaimer.selector, address(this))); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + } + + function test_claimRewardsOnBehalfOf_self( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsOnBehalfOf_validClaimer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + vm.prank(poolAdmin); + contracts.emissionManager.setClaimer(user, address(this)); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_transfer_toSelf( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + uint256 claimableBefore = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), 0); + vm.prank(user); + lmUpgradeable.transfer(user, env.depositAmount); + uint256 claimableAfter = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), claimableAfter); + assertEq(claimableBefore, claimableAfter); + } + + function test_transfer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration, + address receiver, + uint256 sendAmount + ) external { + vm.assume(user != receiver); + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + if (sendAmount > env.depositAmount) { + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + user, + env.depositAmount, + sendAmount + ) + ); + vm.prank(user); + lmUpgradeable.transfer(receiver, sendAmount); + } else { + _fund(env.depositAmount, receiver); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), 0); + assertEq(lmUpgradeable.getUnclaimedRewards(receiver, rewardToken), 0); + + uint256 senderClaimableBefore = lmUpgradeable.getClaimableRewards(user, rewardToken); + uint256 receiverClaimableBefore = lmUpgradeable.getClaimableRewards(receiver, rewardToken); + + vm.prank(user); + lmUpgradeable.transfer(receiver, sendAmount); + // rewards should remain the same, but move to unclaimed + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), senderClaimableBefore); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), senderClaimableBefore); + assertEq(lmUpgradeable.getUnclaimedRewards(receiver, rewardToken), receiverClaimableBefore); + assertEq(lmUpgradeable.getClaimableRewards(receiver, rewardToken), receiverClaimableBefore); + } + } + + function test_isRegisteredRewardToken() external { + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), false); + _setupEmission(uint32(block.timestamp), 0); + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), false); + lmUpgradeable.refreshRewardTokens(); + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), true); + } + + function test_getReferenceAsset() external view { + address ref = lmUpgradeable.getReferenceAsset(); + assertEq(ref, address(underlying)); + } + + function test_rewardTokens() external { + _setupEmission(uint32(block.timestamp), 0); + lmUpgradeable.refreshRewardTokens(); + address[] memory assets = lmUpgradeable.rewardTokens(); + assertEq(assets.length, 1); + assertEq(assets[0], rewardToken); + } + + function test_correctAccountingForDelayedRegistration() external { + address earlyDepositor = address(0xB0B); + _fund(1 ether, earlyDepositor); + _setupEmission(uint32(block.timestamp + 2 days), 1 ether); + + vm.warp(block.timestamp + 1 days); + _fund(1 ether, user); + lmUpgradeable.refreshRewardTokens(); + // as the rewards were not tracked before they should be zero + assertEq(lmUpgradeable.getClaimableRewards(earlyDepositor, rewardToken), 0); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + + vm.warp(block.timestamp + 3 days); + uint256 claimableBob = lmUpgradeable.getClaimableRewards(earlyDepositor, rewardToken); + uint256 claimableUser = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(claimableBob, claimableUser); + assertEq(claimableBob + claimableUser, 1 days * 1 ether); + } + + // ### INTERNAL HELPER FUNCTIONS ### + struct TestEnv { + // @notice the amount deposited + uint256 depositAmount; + // @notice the timestamp at which emission stops + uint32 emissionEnd; + // @notice emission per second + uint88 emissionPerSecond; + // @notice the duration of emissions in the test environment (time passed) + uint32 emissionDuration; + } + + function _setupTestEnvironment( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) internal returns (TestEnv memory) { + TestEnv memory env; + env.depositAmount = bound(depositAmount, 1 ether, type(uint96).max); + env.emissionEnd = uint32(bound(emissionEnd, block.timestamp, 365 days * 100)); + uint32 endTimestamp = uint32(bound(waitDuration, block.timestamp, 365 days * 100)); + env.emissionDuration = env.emissionEnd > endTimestamp + ? endTimestamp - uint32(block.timestamp) + : env.emissionEnd - uint32(block.timestamp); + env.emissionPerSecond = uint88( + bound( + emissionPerSecond, + 0, + env.emissionDuration > 0 ? type(uint88).max / env.emissionDuration : type(uint88).max + ) + ); + _setupEmission(env.emissionEnd, env.emissionPerSecond); + lmUpgradeable.refreshRewardTokens(); + _fund(env.depositAmount, user); + + vm.warp(endTimestamp); + + return env; + } + + function _getRewardTokens() internal view returns (address[] memory) { + address[] memory rewardTokens = new address[](1); + rewardTokens[0] = rewardToken; + return rewardTokens; + } + + function _setupEmission(uint32 emissionEnd, uint88 emissionPerSecond) internal { + RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[]( + 1 + ); + config[0] = RewardsDataTypes.RewardsConfigInput( + emissionPerSecond, + 0, // totalSupply is overwritten internally + emissionEnd, + address(underlying), + rewardToken, + ITransferStrategyBase(strategy), + IEACAggregatorProxy(address(2)) + ); + + // configure asset + vm.prank(emissionAdmin); + contracts.emissionManager.configureAssets(config); + + // fund admin & approve transfers to allow claiming + uint256 fundsToEmit = (emissionEnd - block.timestamp) * emissionPerSecond; + deal(rewardToken, emissionAdmin, fundsToEmit, true); + vm.prank(emissionAdmin); + IERC20(rewardToken).approve(address(strategy), fundsToEmit); + } + + /** + * @dev funds the given user with the lm token and updates total supply. + * Maintains consistency by also funding the underlying to the lmUpgradeable + */ + function _fund(uint256 amount, address user) internal { + underlying.mint(user, amount); + lmUpgradeable.mint(user, amount); + vm.prank(user); + underlying.transfer(address(lmUpgradeable), amount); + } +} diff --git a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol new file mode 100644 index 00000000..e3977a2c --- /dev/null +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20Errors} from 'openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; +import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; +import {ERC4626Upgradeable, ERC4626StataTokenUpgradeable, IERC4626StataToken} from '../../../src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol'; +import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {PullRewardsTransferStrategy, ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; +import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; + +// Minimal mock as contract is abstract +contract MockERC4626StataTokenUpgradeable is ERC4626StataTokenUpgradeable { + constructor(IPool pool) ERC4626StataTokenUpgradeable(pool) {} + + function mockInit(address aToken) external initializer { + __ERC4626StataToken_init(aToken); + } +} + +contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { + MockERC4626StataTokenUpgradeable internal erc4626Upgradeable; + address internal underlying; + address internal aToken; + + address public user; + uint256 internal userPrivateKey; + + function setUp() public virtual { + initTestEnvironment(false); + + userPrivateKey = 0xA11CE; + user = address(vm.addr(userPrivateKey)); + + DataTypes.ReserveDataLegacy memory reserveData = contracts.poolProxy.getReserveData( + tokenList.usdx + ); + underlying = address(tokenList.usdx); + aToken = reserveData.aTokenAddress; + erc4626Upgradeable = new MockERC4626StataTokenUpgradeable(contracts.poolProxy); + erc4626Upgradeable.mockInit(address(reserveData.aTokenAddress)); + } + + function test_2701() external view { + assertEq( + keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC4626StataToken')) - 1)) & + ~bytes32(uint256(0xff)), + 0x55029d3f54709e547ed74b2fc842d93107ab1490ab7555dd9dd0bf6451101900 + ); + } + + // ### GETTERS TESTS ### + function test_convertersAndPreviews(uint128 assets) public view { + uint256 shares = erc4626Upgradeable.convertToShares(assets); + assertEq(shares, erc4626Upgradeable.previewDeposit(assets)); + assertEq(shares, erc4626Upgradeable.previewWithdraw(assets)); + assertEq(erc4626Upgradeable.convertToAssets(shares), assets); + assertEq(erc4626Upgradeable.previewMint(shares), assets); + assertEq(erc4626Upgradeable.previewRedeem(shares), assets); + } + + // ### DEPOSIT TESTS ### + function test_depositATokens(uint128 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + vm.startPrank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), env.amount); + uint256 shares = erc4626Upgradeable.depositATokens(env.amount, receiver); + vm.stopPrank(); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + function test_depositATokens_self() external { + test_depositATokens(1 ether, user); + } + + function test_deposit_shouldRevert_insufficientAllowance(uint128 assets) external { + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + vm.expectRevert(); // underflows + vm.prank(user); + erc4626Upgradeable.depositATokens(env.amount, user); + } + + function test_depositWithPermit_shouldRevert_emptyPermit_noPreApproval(uint128 assets) external { + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.expectRevert(); // will underflow + vm.prank(user); + erc4626Upgradeable.depositWithPermit(env.amount, user, block.timestamp + 1000, sig, false); + } + + function test_depositWithPermit_emptyPermit_underlying_preApproval( + uint128 assets, + address receiver + ) external { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.prank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), env.amount); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + block.timestamp + 1000, + sig, + true + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + function test_depositWithPermit_emptyPermit_aToken_preApproval( + uint128 assets, + address receiver + ) external { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.prank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), env.amount); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + block.timestamp + 1000, + sig, + false + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + function test_depositWithPermit_underlying(uint128 assets, address receiver) external { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(erc4626Upgradeable), + value: env.amount, + nonce: IERC20Permit(underlying).nonces(user), + deadline: block.timestamp + 100 + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + IERC20Permit(underlying).DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + IERC4626StataToken.SignatureParams memory sig = IERC4626StataToken.SignatureParams(v, r, s); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + permit.deadline, + sig, + true + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(underlying).balanceOf(user), 0); + } + + function test_depositWithPermit_aToken(uint128 assets, address receiver) external { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(erc4626Upgradeable), + value: env.amount, + nonce: IERC20Permit(aToken).nonces(user), + deadline: block.timestamp + 100 + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + IERC20Permit(aToken).DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + IERC4626StataToken.SignatureParams memory sig = IERC4626StataToken.SignatureParams(v, r, s); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + permit.deadline, + sig, + false + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + // ### REDEEM TESTS ### + function test_redeemATokens(uint256 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 redeemedAssets = erc4626Upgradeable.redeemATokens(shares, receiver, user); + + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertEq(IERC20(aToken).balanceOf(receiver), redeemedAssets); + } + + function test_redeemATokens_onBehalf_shouldRevert_insufficientAllowance( + uint256 assets, + uint256 allowance + ) external { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + allowance = bound(allowance, 0, shares - 1); + vm.prank(user); + erc4626Upgradeable.approve(address(this), allowance); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientAllowance.selector, + address(this), + allowance, + env.amount + ) + ); + erc4626Upgradeable.redeemATokens(env.amount, address(this), user); + } + + function test_redeemATokens_onBehalf(uint256 assets) external { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + erc4626Upgradeable.approve(address(this), shares); + uint256 redeemedAssets = erc4626Upgradeable.redeemATokens(shares, address(this), user); + + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertEq(IERC20(aToken).balanceOf(address(this)), redeemedAssets); + } + + function test_redeem(uint256 assets, address receiver) external { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 redeemedAssets = erc4626Upgradeable.redeem(shares, receiver, user); + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertLe(IERC20(underlying).balanceOf(receiver), redeemedAssets); + } + + // ### withdraw TESTS ### + function test_withdraw(uint256 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 withdrawnShares = erc4626Upgradeable.withdraw(env.amount, receiver, user); + assertEq(withdrawnShares, shares); + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertLe(IERC20(underlying).balanceOf(receiver), env.amount); + assertApproxEqAbs(IERC20(underlying).balanceOf(receiver), env.amount, 1); + } + + function test_withdraw_shouldRevert_moreThenAvailable(uint256 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fund4626(env.amount, user); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + ERC4626Upgradeable.ERC4626ExceededMaxWithdraw.selector, + address(user), + env.amount + 1, + env.amount + ) + ); + erc4626Upgradeable.withdraw(env.amount + 1, receiver, user); + } + + // ### mint TESTS ### + function test_mint(uint256 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + vm.startPrank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), env.amount); + uint256 shares = erc4626Upgradeable.previewDeposit(env.amount); + uint256 assetsUsedForMinting = erc4626Upgradeable.mint(shares, receiver); + assertEq(assetsUsedForMinting, env.amount); + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + } + + function test_mint_shouldRevert_mintMoreThenBalance(uint256 assets, address receiver) public { + vm.assume(receiver != address(0)); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + vm.startPrank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), type(uint256).max); + uint256 shares = erc4626Upgradeable.previewDeposit(env.amount); + + vm.expectRevert(); + uint256 assetsUsedForMinting = erc4626Upgradeable.mint(shares + 1, receiver); + } + + // ### maxDeposit TESTS ### + function test_maxDeposit_freeze() public { + vm.prank(roleList.marketOwner); + contracts.poolConfiguratorProxy.setReserveFreeze(underlying, true); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_paused() public { + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(underlying, true); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_noCap() public { + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(underlying, 0); + + uint256 maxDeposit = erc4626Upgradeable.maxDeposit(address(0)); + uint256 maxMint = erc4626Upgradeable.maxMint(address(0)); + + assertEq(maxDeposit, type(uint256).max); + assertEq(maxMint, type(uint256).max); + } + + function test_maxDeposit_cap(uint256 cap) public { + cap = bound(cap, 1, type(uint32).max); + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(underlying, cap); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + assertEq(max, cap * 10 ** erc4626Upgradeable.decimals()); + } + + // TODO: perhaps makes sense to add maxDeposit test with accruedToTreasury etc + + // ### maxRedeem TESTS ### + function test_maxRedeem_paused(uint128 assets) public { + TestEnv memory env = _setupTestEnv(assets); + _fund4626(env.amount, user); + + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(underlying, true); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, 0); + } + + function test_maxRedeem_sufficientAvailableLiquidity(uint128 assets) public { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, shares); + } + + function test_maxRedeem_inSufficientAvailableLiquidity(uint256 amountToBorrow) public { + uint128 assets = 1e8; + amountToBorrow = bound(amountToBorrow, 1, assets); + uint256 shares = _fund4626(assets, user); + + // borrow out some assets + address borrowUser = address(99); + vm.startPrank(borrowUser); + deal(address(weth), borrowUser, 2_000 ether); + weth.approve(address(contracts.poolProxy), 2_000 ether); + contracts.poolProxy.deposit(address(weth), 2_000 ether, borrowUser, 0); + contracts.poolProxy.borrow(underlying, amountToBorrow, 2, 0, borrowUser); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, erc4626Upgradeable.previewRedeem(assets - amountToBorrow)); + } + + // ### lastestAnswer TESTS ### + function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { + vm.mockCall( + address(contracts.poolProxy), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(1e27) + ); + uint256 stataPrice = uint256(erc4626Upgradeable.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); + assertEq(stataPrice, underlyingPrice); + } + + function test_latestAnswer_priceShouldReflectIndexAccrual(uint256 liquidityIndex) public { + liquidityIndex = bound(liquidityIndex, 1e27, 1e29); + vm.mockCall( + address(contracts.poolProxy), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(liquidityIndex) + ); + uint256 stataPrice = uint256(erc4626Upgradeable.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); + uint256 expectedStataPrice = (underlyingPrice * liquidityIndex) / 1e27; + assertEq(stataPrice, expectedStataPrice); + + // reverse the math to ensure precision loss is within bounds + uint256 reversedUnderlying = (stataPrice * 1e27) / liquidityIndex; + assertApproxEqAbs(underlyingPrice, reversedUnderlying, 1); + } + + struct TestEnv { + uint256 amount; + } + + function _setupTestEnv(uint256 amount) internal returns (TestEnv memory) { + TestEnv memory env; + env.amount = bound(amount, 1, type(uint96).max); + return env; + } + + function _fundUnderlying(uint256 assets, address user) internal { + deal(underlying, user, assets); + } + + function _fundAToken(uint256 assets, address user) internal { + _fundUnderlying(assets, user); + vm.startPrank(user); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, user, 0); + vm.stopPrank(); + } + + function _fund4626(uint256 assets, address user) internal returns (uint256) { + _fundAToken(assets, user); + vm.startPrank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), assets); + uint256 shares = erc4626Upgradeable.depositATokens(assets, user); + vm.stopPrank(); + return shares; + } +} diff --git a/tests/periphery/static-a-token/Pausable.t.sol b/tests/periphery/static-a-token/Pausable.t.sol deleted file mode 100644 index 59a24dec..00000000 --- a/tests/periphery/static-a-token/Pausable.t.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {UpgradableOwnableWithGuardian} from 'solidity-utils/contracts/access-control/UpgradableOwnableWithGuardian.sol'; -import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; -import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {RayMathExplicitRounding} from '../../../src/periphery/contracts/libraries/RayMathExplicitRounding.sol'; -import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; -import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; -import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; -import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; -import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; -import {SigUtils} from '../../utils/SigUtils.sol'; -import {BaseTest, TestnetERC20} from './TestBase.sol'; - -contract StataPausableTest is BaseTest { - using RayMathExplicitRounding for uint256; - - function test_setPaused_shouldRevertForInvalidCaller(address actor) external { - vm.assume(actor != poolAdmin && actor != proxyAdmin); - vm.expectRevert(abi.encodeWithSelector(IStaticATokenLM.OnlyPauseGuardian.selector, actor)); - _setPaused(actor, true); - } - - function test_setPaused_shouldSuceedForOwner() external { - assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), false); - _setPaused(poolAdmin, true); - assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), true); - } - - function test_deposit_shouldRevert() external { - vm.startPrank(user); - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.deposit(amountToDeposit, user, 0, true); - } - - function test_mint_shouldRevert() external { - vm.startPrank(user); - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - vm.stopPrank(); - - uint256 sharesToMint = staticATokenLM.previewDeposit(amountToDeposit); - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.mint(sharesToMint, user); - } - - function test_redeem_shouldRevert() external { - uint128 amountToDeposit = 5 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - - _setPausedAsAclAdmin(true); - uint256 maxRedeem = staticATokenLM.maxRedeem(user); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.redeem(maxRedeem, user, user); - } - - function test_withdraw_shouldRevert() external { - uint128 amountToDeposit = 5 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 maxWithdraw = staticATokenLM.maxWithdraw(user); - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.withdraw(maxWithdraw, user, user); - } - - function test_transfer_shouldRevert() external { - uint128 amountToDeposit = 10 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.transfer(user1, amountToDeposit); - } - - function test_claimingRewards_shouldRevert() external { - _configureLM(); - uint128 amountToDeposit = 10 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.claimRewardsToSelf(rewardTokens); - } - - function _setPausedAsAclAdmin(bool paused) internal { - _setPaused(poolAdmin, paused); - } - - function _setPaused(address actor, bool paused) internal { - vm.prank(actor); - staticATokenLM.setPaused(paused); - } -} diff --git a/tests/periphery/static-a-token/Rewards.t.sol b/tests/periphery/static-a-token/Rewards.t.sol deleted file mode 100644 index 36a13dd8..00000000 --- a/tests/periphery/static-a-token/Rewards.t.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; -import {IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {BaseTest} from './TestBase.sol'; - -contract StataRewardsTest is BaseTest { - function setUp() public override { - super.setUp(); - - _configureLM(); - - vm.startPrank(user); - } - - function test_claimableRewards() external { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - - vm.warp(block.timestamp + 200); - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(claimable, 200 * 0.00385 ether); - } - - // test rewards - function test_collectAndUpdateRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - uint256 claimable = staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN); - staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), claimable); - } - - function test_claimRewardsToSelf() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - } - - function test_claimRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewards(user, rewardTokens); - assertEq(claimable, IERC20(REWARD_TOKEN).balanceOf(user)); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - } - - // should fail as user1 is not a valid claimer - function testFail_claimRewardsOnBehalfOf() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - vm.stopPrank(); - vm.startPrank(user1); - - staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewardsOnBehalf(user, user1, rewardTokens); - } - - function test_depositATokenClaimWithdrawClaim() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - // deposit aweth - _depositAToken(amountToDeposit, user); - - // forward time - _skipBlocks(60); - - // claim - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); - assertGt(claimable0, 0); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); - - // forward time - _skipBlocks(60); - - // redeem - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); - assertGt(claimable1, 0); - - // claim on behalf of other user - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertGe(AToken(UNDERLYING).balanceOf(user), 5 ether); - } - - function test_depositWETHClaimWithdrawClaim() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - // forward time - _skipBlocks(60); - - // claim - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); - assertGt(claimable0, 0); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); - - // forward time - _skipBlocks(60); - - // redeem - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); - assertGt(claimable1, 0); - - // claim on behalf of other user - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertGe(AToken(UNDERLYING).balanceOf(user), 5 ether); - } - - function test_transfer() public { - uint128 amountToDeposit = 10 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - // transfer to 2nd user - staticATokenLM.transfer(user1, amountToDeposit / 2); - assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); - - // forward time - _skipBlocks(60); - - // redeem for both - uint256 claimableUser = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimableUser); - vm.stopPrank(); - vm.startPrank(user1); - uint256 claimableUser1 = staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user1), user1, user1); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user1), claimableUser1); - assertGt(claimableUser1, 0); - - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); - } - - // getUnclaimedRewards - function test_getUnclaimedRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - uint256 shares = _depositAToken(amountToDeposit, user); - assertEq(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); - _skipBlocks(1000); - staticATokenLM.redeem(shares, user, user); - assertGt(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); - } -} diff --git a/tests/periphery/static-a-token/StataOracle.t.sol b/tests/periphery/static-a-token/StataOracle.t.sol deleted file mode 100644 index df569d9c..00000000 --- a/tests/periphery/static-a-token/StataOracle.t.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {StataOracle} from '../../../src/periphery/contracts/static-a-token/StataOracle.sol'; -import {StaticATokenLM} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {BaseTest} from './TestBase.sol'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; - -contract StataOracleTest is BaseTest { - StataOracle public oracle; - - function setUp() public override { - super.setUp(); - oracle = new StataOracle(contracts.poolAddressesProvider); - - vm.prank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 1_000_000); - } - - // ### tests for the dedicated oracle aggregator - function test_assetPrice() public view { - uint256 stataPrice = oracle.getAssetPrice(address(staticATokenLM)); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); - assertGe(stataPrice, underlyingPrice); - assertEq(stataPrice, (underlyingPrice * staticATokenLM.convertToAssets(1e18)) / 1e18); - } - - function test_assetsPrices() public view { - address[] memory staticATokens = factory.getStaticATokens(); - uint256[] memory stataPrices = oracle.getAssetsPrices(staticATokens); - - for (uint256 i = 0; i < staticATokens.length; i++) { - address staticAToken = staticATokens[i]; - uint256 stataPrice = stataPrices[i]; - - address underlying = StaticATokenLM(staticAToken).asset(); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); - - assertGe(stataPrice, underlyingPrice); - assertEq( - stataPrice, - (underlyingPrice * StaticATokenLM(staticAToken).convertToAssets(1e18)) / 1e18 - ); - } - } - - function test_error(uint256 shares) public view { - vm.assume(shares <= staticATokenLM.maxMint(address(0))); - uint256 pricePerShare = oracle.getAssetPrice(address(staticATokenLM)); - uint256 pricePerAsset = contracts.aaveOracle.getAssetPrice(UNDERLYING); - uint256 assets = staticATokenLM.convertToAssets(shares); - - assertApproxEqAbs( - (pricePerShare * shares) / 1e18, - (pricePerAsset * assets) / 1e18, - (assets / 1e18) + 1 // there can be imprecision of 1 wei, which will accumulate for each asset - ); - } - - // ### tests for the token internal oracle - function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(1e27) - ); - uint256 stataPrice = uint256(staticATokenLM.latestAnswer()); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); - assertEq(stataPrice, underlyingPrice); - } - - function test_latestAnswer_priceShouldReflectIndexAccrual(uint256 liquidityIndex) public { - liquidityIndex = bound(liquidityIndex, 1e27, 1e29); - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(liquidityIndex) - ); - uint256 stataPrice = uint256(staticATokenLM.latestAnswer()); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); - uint256 expectedStataPrice = (underlyingPrice * liquidityIndex) / 1e27; - assertEq(stataPrice, expectedStataPrice); - - // reverse the math to ensure precision loss is within bounds - uint256 reversedUnderlying = (stataPrice * 1e27) / liquidityIndex; - assertApproxEqAbs(underlyingPrice, reversedUnderlying, 1); - } -} diff --git a/tests/periphery/static-a-token/StataTokenV2Getters.sol b/tests/periphery/static-a-token/StataTokenV2Getters.sol new file mode 100644 index 00000000..425ada34 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Getters.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; +import {StataTokenV2} from '../../../src/periphery/contracts/static-a-token/StataTokenV2.sol'; // TODO: change import to isolate to 4626 +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2GettersTest is BaseTest { + function test_initializeShouldRevert() public { + address impl = factory.STATIC_A_TOKEN_IMPL(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + StataTokenV2(impl).initialize(aToken, 'hey', 'ho'); + } + + function test_getters() public view { + assertEq(stataTokenV2.name(), 'Static Aave Local WETH v2'); + assertEq(stataTokenV2.symbol(), 'stataLocWETHv2'); + + address referenceAsset = stataTokenV2.getReferenceAsset(); + assertEq(referenceAsset, aToken); + + address underlyingAddress = address(stataTokenV2.asset()); + assertEq(underlyingAddress, underlying); + + IERC20Metadata underlying = IERC20Metadata(underlyingAddress); + assertEq(stataTokenV2.decimals(), underlying.decimals()); + + assertEq( + address(stataTokenV2.INCENTIVES_CONTROLLER()), + address(AToken(aToken).getIncentivesController()) + ); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol new file mode 100644 index 00000000..06d90b95 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC4626StataToken} from '../../../src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2PausableTest is BaseTest { + function test_canPause() external { + assertEq(stataTokenV2.canPause(poolAdmin), true); + } + + function test_canPause_shouldReturnFalse(address actor) external { + vm.assume(actor != poolAdmin); + assertEq(stataTokenV2.canPause(actor), false); + } + + function test_setPaused_shouldRevertForInvalidCaller(address actor) external { + vm.assume(actor != poolAdmin && actor != proxyAdmin); + vm.expectRevert(abi.encodeWithSelector(IERC4626StataToken.OnlyPauseGuardian.selector, actor)); + _setPaused(actor, true); + } + + function test_setPaused_shouldSucceedForOwner() external { + assertEq(PausableUpgradeable(address(stataTokenV2)).paused(), false); + _setPaused(poolAdmin, true); + assertEq(PausableUpgradeable(address(stataTokenV2)).paused(), true); + } + + function test_deposit_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fundUnderlying(amountToDeposit, user); + vm.prank(user); + IERC20(underlying).approve(address(stataTokenV2), amountToDeposit); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.deposit(amountToDeposit, user); + } + + function test_mint_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fundUnderlying(amountToDeposit, user); + vm.prank(user); + IERC20(underlying).approve(address(stataTokenV2), amountToDeposit); + + uint256 sharesToMint = stataTokenV2.previewDeposit(amountToDeposit); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.mint(sharesToMint, user); + } + + function test_redeem_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fund4626(amountToDeposit, user); + + assertEq(stataTokenV2.maxRedeem(user), stataTokenV2.balanceOf(user)); + + _setPausedAsAclAdmin(true); + uint256 maxRedeem = stataTokenV2.maxRedeem(user); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.redeem(maxRedeem, user, user); + } + + function test_withdraw_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fund4626(amountToDeposit, user); + + uint256 maxWithdraw = stataTokenV2.maxWithdraw(user); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.withdraw(maxWithdraw, user, user); + } + + function test_transfer_shouldRevert() external { + uint128 amountToDeposit = 10 ether; + _fund4626(amountToDeposit, user); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.transfer(user1, amountToDeposit); + } + + function test_claimingRewards_shouldRevert() external { + uint128 amountToDeposit = 10 ether; + _fund4626(amountToDeposit, user); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.claimRewardsToSelf(rewardTokens); + } + + function _setPausedAsAclAdmin(bool paused) internal { + _setPaused(poolAdmin, paused); + } + + function _setPaused(address actor, bool paused) internal { + vm.prank(actor); + stataTokenV2.setPaused(paused); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Permit.sol b/tests/periphery/static-a-token/StataTokenV2Permit.sol new file mode 100644 index 00000000..d24b1ab7 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Permit.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2PermitTest is BaseTest { + function test_permit() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + + assertEq(stataTokenV2.allowance(permit.owner, spender), permit.value); + } + + function test_permit_expired() public { + // as the default timestamp is 0, we move ahead in time a bit + vm.warp(10 days); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp - 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612ExpiredSignature.selector, + permit.deadline + ) + ); + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } + + function test_permit_invalidSigner() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: address(424242), + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612InvalidSigner.selector, + user, + permit.owner + ) + ); + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Rescuable.sol b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol new file mode 100644 index 00000000..e43b14d8 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2RescuableTest is BaseTest { + function test_whoCanRescue() external view { + assertEq(IRescuable(address(stataTokenV2)).whoCanRescue(), poolAdmin); + } + + function test_rescuable_shouldRevertForInvalidCaller() external { + deal(tokenList.usdx, address(stataTokenV2), 1 ether); + vm.expectRevert('ONLY_RESCUE_GUARDIAN'); + IRescuable(address(stataTokenV2)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether + ); + } + + function test_rescuable_shouldSuceedForOwner() external { + deal(tokenList.usdx, address(stataTokenV2), 1 ether); + vm.startPrank(poolAdmin); + IRescuable(address(stataTokenV2)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether + ); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol deleted file mode 100644 index 57009857..00000000 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ /dev/null @@ -1,427 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; -import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; -import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; -import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; -import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {RayMathExplicitRounding} from '../../../src/periphery/contracts/libraries/RayMathExplicitRounding.sol'; -import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; -import {SigUtils} from '../../utils/SigUtils.sol'; -import {BaseTest, TestnetERC20} from './TestBase.sol'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; - -contract StaticATokenLMTest is BaseTest { - using RayMathExplicitRounding for uint256; - - function setUp() public override { - super.setUp(); - - _configureLM(); - _openSupplyAndBorrowPositions(); - - vm.startPrank(user); - } - - function test_initializeShouldRevert() public { - address impl = factory.STATIC_A_TOKEN_IMPL(); - vm.expectRevert(Initializable.InvalidInitialization.selector); - IStaticATokenLM(impl).initialize(A_TOKEN, 'hey', 'ho'); - } - - function test_getters() public view { - assertEq(staticATokenLM.name(), 'Static Aave Local WETH'); - assertEq(staticATokenLM.symbol(), 'stataLocWETH'); - - IERC20 aToken = staticATokenLM.aToken(); - assertEq(address(aToken), A_TOKEN); - - address underlyingAddress = address(staticATokenLM.asset()); - assertEq(underlyingAddress, UNDERLYING); - - IERC20Metadata underlying = IERC20Metadata(underlyingAddress); - assertEq(staticATokenLM.decimals(), underlying.decimals()); - - assertEq( - address(staticATokenLM.INCENTIVES_CONTROLLER()), - address(AToken(A_TOKEN).getIncentivesController()) - ); - } - - function test_convertersAndPreviews() public view { - uint128 amount = 5 ether; - uint256 shares = staticATokenLM.convertToShares(amount); - assertLe(shares, amount, 'SHARES LOWER'); - assertEq(shares, staticATokenLM.previewDeposit(amount), 'PREVIEW_DEPOSIT'); - assertLe(shares, staticATokenLM.previewWithdraw(amount), 'PREVIEW_WITHDRAW'); - uint256 assets = staticATokenLM.convertToAssets(amount); - assertGe(assets, shares, 'ASSETS GREATER'); - assertLe(assets, staticATokenLM.previewMint(amount), 'PREVIEW_MINT'); - assertEq(assets, staticATokenLM.previewRedeem(amount), 'PREVIEW_REDEEM'); - } - - // Redeem tests - function test_redeem() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); - } - - function test_redeemAToken() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user, false); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(A_TOKEN).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(A_TOKEN).balanceOf(user), amountToDeposit, 1); - } - - function test_redeemAllowance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user)); - vm.stopPrank(); - vm.startPrank(user1); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit, 1); - } - - function testFail_redeemOverflowAllowance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user) / 2); - vm.stopPrank(); - vm.startPrank(user1); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(IERC20(A_TOKEN).balanceOf(user1), amountToDeposit); - } - - function testFail_redeemAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user) + 1, user, user); - } - - // Withdraw tests - function test_withdraw() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertLe(staticATokenLM.maxWithdraw(user), amountToDeposit); - staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user), user, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); - } - - function testFail_withdrawAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - _fundUser(amountToDeposit, user1); - - _depositAToken(amountToDeposit, user); - _depositAToken(amountToDeposit, user1); - - assertEq(staticATokenLM.maxWithdraw(user), amountToDeposit); - staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user) + 1, user, user); - } - - // mint - function test_mint() public { - vm.stopPrank(); - - // set supply cap to non-zero - vm.startPrank(poolAdmin); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 15_000); - vm.stopPrank(); - - vm.startPrank(user); - - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - uint256 shares = 1 ether; - staticATokenLM.mint(shares, user); - assertEq(shares, staticATokenLM.balanceOf(user)); - } - - function testFail_mintAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _underlyingToAToken(amountToDeposit, user); - IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); - staticATokenLM.mint(amountToDeposit, user); - } - - /** - * maxDeposit test - */ - function test_maxDeposit_freeze() public { - vm.stopPrank(); - vm.startPrank(roleList.marketOwner); - contracts.poolConfiguratorProxy.setReserveFreeze(UNDERLYING, true); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - - assertEq(max, 0); - } - - function test_maxDeposit_paused() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - - assertEq(max, 0); - } - - function test_maxDeposit_noCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 0); - - uint256 maxDeposit = staticATokenLM.maxDeposit(address(0)); - uint256 maxMint = staticATokenLM.maxMint(address(0)); - - assertEq(maxDeposit, type(uint256).max); - assertEq(maxMint, type(uint256).max); - } - - // should be 0 as supply is ~5k - function test_maxDeposit_5kCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 5_000); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - assertEq(max, 0); - } - - function test_maxDeposit_50kCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 50_000); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(UNDERLYING); - assertEq( - max, - 50_000 * - (10 ** IERC20Metadata(UNDERLYING).decimals()) - - (IERC20Metadata(A_TOKEN).totalSupply() + - uint256(reserveData.accruedToTreasury).rayMulRoundUp(staticATokenLM.rate())) - ); - } - - /** - * maxRedeem test - */ - function test_maxRedeem_paused() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); - - uint256 max = staticATokenLM.maxRedeem(address(user)); - - assertEq(max, 0); - } - - function test_maxRedeem_allAvailable() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - uint256 max = staticATokenLM.maxRedeem(address(user)); - - assertEq(max, staticATokenLM.balanceOf(user)); - } - - function test_maxRedeem_partAvailable() public { - uint128 amountToDeposit = 50 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 maxRedeemBefore = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); - uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); - - // create rich user - address borrowUser = address(99); - vm.startPrank(borrowUser); - deal(address(wbtc), borrowUser, 2_000e8); - wbtc.approve(address(POOL), 2_000e8); - POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); - - // borrow all available - POOL.borrow(UNDERLYING, underlyingBalanceBefore - (maxRedeemBefore / 2), 2, 0, borrowUser); - - uint256 maxRedeemAfter = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); - assertApproxEqAbs(maxRedeemAfter, (maxRedeemBefore / 2), 1); - } - - function test_maxRedeem_nonAvailable() public { - uint128 amountToDeposit = 50 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); - // create rich user - address borrowUser = address(99); - vm.startPrank(borrowUser); - deal(address(wbtc), borrowUser, 2_000e8); - wbtc.approve(address(POOL), 2_000e8); - POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); - - // borrow all available - contracts.poolProxy.borrow(UNDERLYING, underlyingBalanceBefore, 2, 0, borrowUser); - - uint256 maxRedeemAfter = staticATokenLM.maxRedeem(address(user)); - assertEq(maxRedeemAfter, 0); - } - - function test_permit() public { - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - PERMIT_TYPEHASH, - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - - assertEq(staticATokenLM.allowance(permit.owner, spender), permit.value); - } - - function test_permit_expired() public { - // as the default timestamp is 0, we move ahead in time a bit - vm.warp(10 days); - - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp - 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - PERMIT_TYPEHASH, - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert( - abi.encodeWithSelector( - ERC20PermitUpgradeable.ERC2612ExpiredSignature.selector, - permit.deadline - ) - ); - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - } - - function test_permit_invalidSigner() public { - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: address(424242), - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - PERMIT_TYPEHASH, - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert( - abi.encodeWithSelector( - ERC20PermitUpgradeable.ERC2612InvalidSigner.selector, - user, - permit.owner - ) - ); - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - } - - function test_rescuable_shouldRevertForInvalidCaller() external { - deal(tokenList.usdx, address(staticATokenLM), 1 ether); - vm.expectRevert('ONLY_RESCUE_GUARDIAN'); - IRescuable(address(staticATokenLM)).emergencyTokenTransfer( - tokenList.usdx, - address(this), - 1 ether - ); - } - - function test_rescuable_shouldSuceedForOwner() external { - deal(tokenList.usdx, address(staticATokenLM), 1 ether); - vm.startPrank(poolAdmin); - IRescuable(address(staticATokenLM)).emergencyTokenTransfer( - tokenList.usdx, - address(this), - 1 ether - ); - } - - function _openSupplyAndBorrowPositions() internal { - // this is to open borrow positions so that the aToken balance increases - address whale = address(79); - vm.startPrank(whale); - _fundUser(5_000 ether, whale); - - weth.approve(address(POOL), 5_000 ether); - POOL.deposit(address(weth), 5_000 ether, whale, 0); - - POOL.borrow(address(weth), 1_000 ether, 2, 0, whale); - vm.stopPrank(); - } -} diff --git a/tests/periphery/static-a-token/StaticATokenNoLM.t.sol b/tests/periphery/static-a-token/StaticATokenNoLM.t.sol deleted file mode 100644 index 84ddbd33..00000000 --- a/tests/periphery/static-a-token/StaticATokenNoLM.t.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {BaseTest, IERC20} from './TestBase.sol'; - -/** - * Testing the static token wrapper on a pool that never had LM enabled - * This is a slightly different assumption than a pool that doesn't have LM enabled any more as incentivesController.rewardTokens() will have length=0 - */ -contract StaticATokenNoLMTest is BaseTest { - function setUp() public override { - super.setUp(); - - vm.startPrank(user); - } - - // test rewards - function test_collectAndUpdateRewardsWithLMDisabled() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertEq(staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN), 0); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - } - - function test_claimRewardsToSelfWithLMDisabled() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - try staticATokenLM.getClaimableRewards(user, REWARD_TOKEN) {} catch Error( - string memory reason - ) { - require(keccak256(bytes(reason)) == keccak256(bytes('9'))); - } - - try staticATokenLM.claimRewardsToSelf(rewardTokens) {} catch Error(string memory reason) { - require(keccak256(bytes(reason)) == keccak256(bytes('9'))); - } - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - } -} diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index aa00e0ca..4a098285 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -1,17 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; -import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; -import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; -import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; -import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; -import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; import {StaticATokenFactory} from '../../../src/periphery/contracts/static-a-token/StaticATokenFactory.sol'; -import {StaticATokenLM, IStaticATokenLM, IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {IAToken} from '../../../src/core/contracts/interfaces/IAToken.sol'; +import {StataTokenV2} from '../../../src/periphery/contracts/static-a-token/StataTokenV2.sol'; +import {IERC20AaveLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol'; import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; @@ -29,17 +24,16 @@ abstract contract BaseTest is TestnetProcedures { uint256 internal userPrivateKey; uint256 internal spenderPrivateKey; - StaticATokenLM public staticATokenLM; + StataTokenV2 public stataTokenV2; address public proxyAdmin; ITransparentProxyFactory public proxyFactory; StaticATokenFactory public factory; address[] rewardTokens; - address public UNDERLYING; - address public A_TOKEN; - address public REWARD_TOKEN; - IPool public POOL; + address public underlying; + address public aToken; + address public rewardToken; function setUp() public virtual { userPrivateKey = 0xA11CE; @@ -48,67 +42,24 @@ abstract contract BaseTest is TestnetProcedures { user1 = address(vm.addr(2)); spender = vm.addr(spenderPrivateKey); - initTestEnvironment(); + initTestEnvironment(false); DataTypes.ReserveDataLegacy memory reserveDataWETH = contracts.poolProxy.getReserveData( tokenList.weth ); - UNDERLYING = address(weth); - REWARD_TOKEN = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, OWNER)); - A_TOKEN = reserveDataWETH.aTokenAddress; - POOL = contracts.poolProxy; + underlying = address(weth); + rewardToken = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, OWNER)); + aToken = reserveDataWETH.aTokenAddress; - rewardTokens.push(REWARD_TOKEN); + rewardTokens.push(rewardToken); proxyFactory = ITransparentProxyFactory(report.transparentProxyFactory); proxyAdmin = report.proxyAdmin; factory = StaticATokenFactory(report.staticATokenFactoryProxy); - factory.createStaticATokens(POOL.getReservesList()); + factory.createStaticATokens(contracts.poolProxy.getReservesList()); - staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); - } - - function _configureLM() internal { - PullRewardsTransferStrategy strat = new PullRewardsTransferStrategy( - report.rewardsControllerProxy, - EMISSION_ADMIN, - EMISSION_ADMIN - ); - - vm.startPrank(poolAdmin); - contracts.emissionManager.setEmissionAdmin(REWARD_TOKEN, EMISSION_ADMIN); - vm.stopPrank(); - - vm.startPrank(EMISSION_ADMIN); - IERC20(REWARD_TOKEN).approve(address(strat), 10_000 ether); - vm.stopPrank(); - - vm.startPrank(OWNER); - TestnetERC20(REWARD_TOKEN).mint(EMISSION_ADMIN, 10_000 ether); - vm.stopPrank(); - - RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[]( - 1 - ); - config[0] = RewardsDataTypes.RewardsConfigInput( - 0.00385 ether, - 10_000 ether, - uint32(block.timestamp + 30 days), - A_TOKEN, - REWARD_TOKEN, - ITransferStrategyBase(strat), - IEACAggregatorProxy(address(2)) - ); - - vm.prank(EMISSION_ADMIN); - contracts.emissionManager.configureAssets(config); - - staticATokenLM.refreshRewardTokens(); - } - - function _fundUser(uint128 amountToDeposit, address targetUser) internal { - deal(UNDERLYING, targetUser, amountToDeposit); + stataTokenV2 = StataTokenV2(factory.getStaticAToken(underlying)); } function _skipBlocks(uint128 blocks) internal { @@ -116,22 +67,32 @@ abstract contract BaseTest is TestnetProcedures { vm.warp(block.timestamp + blocks * 12); // assuming a block is around 12seconds } - function _underlyingToAToken(uint256 amountToDeposit, address targetUser) internal { - IERC20(UNDERLYING).approve(address(POOL), amountToDeposit); - POOL.deposit(UNDERLYING, amountToDeposit, targetUser, 0); + function testAdmin() public { + vm.stopPrank(); + vm.startPrank(proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(stataTokenV2))).admin(), proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); + vm.stopPrank(); } - function _depositAToken(uint256 amountToDeposit, address targetUser) internal returns (uint256) { - _underlyingToAToken(amountToDeposit, targetUser); - IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); - return staticATokenLM.deposit(amountToDeposit, targetUser, 10, false); + function _fundUnderlying(uint256 assets, address user) internal { + deal(underlying, user, assets); } - function testAdmin() public { + function _fundAToken(uint256 assets, address user) internal { + _fundUnderlying(assets, user); + vm.startPrank(user); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, user, 0); vm.stopPrank(); - vm.startPrank(proxyAdmin); - assertEq(TransparentUpgradeableProxy(payable(address(staticATokenLM))).admin(), proxyAdmin); - assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); + } + + function _fund4626(uint256 assets, address user) internal returns (uint256) { + _fundAToken(assets, user); + vm.startPrank(user); + IERC20(aToken).approve(address(stataTokenV2), assets); + uint256 shares = stataTokenV2.depositATokens(assets, user); vm.stopPrank(); + return shares; } } diff --git a/tests/utils/SigUtils.sol b/tests/utils/SigUtils.sol index 311a256d..a41339fd 100644 --- a/tests/utils/SigUtils.sol +++ b/tests/utils/SigUtils.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.10; -import {IStaticATokenLM} from '../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; +import {IERC20AaveLM} from '../../src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol'; library SigUtils { + bytes32 internal constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); + struct Permit { address owner; address spender; @@ -12,26 +15,6 @@ library SigUtils { uint256 deadline; } - struct MetaWithdrawParams { - address owner; - address spender; - uint256 staticAmount; - uint256 dynamicAmount; - bool toUnderlying; - uint256 nonce; - uint256 deadline; - } - - struct MetaDepositParams { - address depositor; - address receiver; - uint256 assets; - uint16 referralCode; - bool fromUnderlying; - uint256 nonce; - uint256 deadline; - } - // computes the hash of a permit function getStructHash(Permit memory _permit, bytes32 typehash) internal pure returns (bytes32) { return @@ -47,44 +30,6 @@ library SigUtils { ); } - function getWithdrawHash( - MetaWithdrawParams memory permit, - bytes32 typehash - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - typehash, - permit.owner, - permit.spender, - permit.staticAmount, - permit.dynamicAmount, - permit.toUnderlying, - permit.nonce, - permit.deadline - ) - ); - } - - function getDepositHash( - MetaDepositParams memory params, - bytes32 typehash - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - typehash, - params.depositor, - params.receiver, - params.assets, - params.referralCode, - params.fromUnderlying, - params.nonce, - params.deadline - ) - ); - } - // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer function getTypedDataHash( Permit memory permit, @@ -94,22 +39,4 @@ library SigUtils { return keccak256(abi.encodePacked('\x19\x01', domainSeparator, getStructHash(permit, typehash))); } - - function getTypedWithdrawHash( - MetaWithdrawParams memory params, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(params, typehash))); - } - - function getTypedDepositHash( - MetaDepositParams memory params, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(params, typehash))); - } }