fluid-contracts-public/test/foundry/lending/big-test/fTokenHandler.sol
2024-07-11 13:05:09 +00:00

439 lines
17 KiB
Solidity

pragma solidity >=0.8.0 <0.9.0;
import "forge-std/Test.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol";
import { MockERC20 } from "solmate/src/test/utils/mocks/MockERC20.sol";
import { LiquidityCalcs } from "../../../../contracts/libraries/liquidityCalcs.sol";
import { IFluidLendingRewardsRateModel } from "../../../../contracts/protocols/lending/interfaces/iLendingRewardsRateModel.sol";
import { IFToken } from "../../../../contracts/protocols/lending/interfaces/iFToken.sol";
import { FluidLiquidityAdminModule } from "../../../../contracts/liquidity/adminModule/main.sol";
import { MockProtocol } from "../../../../contracts/mocks/mockProtocol.sol";
import { IFluidLiquidity } from "../../../../contracts/liquidity/interfaces/iLiquidity.sol";
import { FluidLiquidityResolver } from "../../../../contracts/periphery/resolvers/liquidity/main.sol";
import { Structs as FluidLiquidityResolverStructs } from "../../../../contracts/periphery/resolvers/liquidity/structs.sol";
import { Structs as ResolverStructs } from "../../../../contracts/periphery/resolvers/liquidity/structs.sol";
import { ErrorTypes } from "../../../../contracts/liquidity/errorTypes.sol";
import { Error } from "../../../../contracts/liquidity/error.sol";
import { ErrorTypes as LendingErrorTypes } from "../../../../contracts/protocols/lending/errorTypes.sol";
import { Error as LendingError } from "../../../../contracts/protocols/lending/error.sol";
contract FTokenHandler is Test {
using FixedPointMathLib for uint256;
IFToken token;
MockERC20 underlying;
address[] public actors;
address public rateModel;
address public admin;
address public liquidity;
MockProtocol mockProtocol;
FluidLiquidityResolver resolver;
address internal currentActor;
uint256 internal liquidityExchangePrice;
uint256 internal tokenExchangePrice;
uint256 internal borrowExchangePrice;
uint256 internal supplyExchangePrice;
uint256 public ghost_sumBalanceOf;
modifier useActor(uint256 actorIndexSeed) {
currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
vm.startPrank(currentActor);
_;
vm.stopPrank();
}
modifier useAdminActor() {
vm.startPrank(admin);
_;
vm.stopPrank();
}
// function randomly warps time between 0 and 10 days
modifier randomTimeWarp() {
uint256 randomSeconds = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))) % (10 days);
vm.warp(block.timestamp + randomSeconds);
address[] memory tokens = new address[](1);
tokens[0] = address(underlying);
// update exchange prices at liquidity (open method)
FluidLiquidityAdminModule(address(liquidity)).updateExchangePrices(tokens);
_;
}
// random supply
modifier randomSupplyAndBorrow() {
underlying.mint(address(liquidity), 100 * 1e6);
// actor supplies asset liquidity
uint256 randomSupplyAmount = (uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao, currentActor))
) & (0.8 ether)) + 0.2 ether;
underlying.mint(currentActor, randomSupplyAmount);
underlying.approve(address(mockProtocol), type(uint256).max);
// _supply
mockProtocol.operate(
address(underlying),
int256(randomSupplyAmount),
0,
address(0),
address(0),
abi.encode(currentActor)
);
(ResolverStructs.UserBorrowData memory userBorrowData_, ) = resolver.getUserBorrowData(
address(mockProtocol),
address(underlying)
);
uint256 randomBorrowAmount = randomSupplyAmount > 0
? uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, currentActor))) % randomSupplyAmount
: 0;
if (randomBorrowAmount > userBorrowData_.borrowable) randomBorrowAmount = userBorrowData_.borrowable;
mockProtocol.operate(
address(underlying),
0,
int256(randomBorrowAmount),
address(0),
currentActor,
new bytes(0)
);
_;
}
constructor(
IFToken token_,
address underlying_,
address[] memory actors_,
address admin_,
address liquidity_,
MockProtocol mockProtocol_
) {
token = token_;
underlying = MockERC20(underlying_);
actors = actors_;
admin = admin_;
liquidity = liquidity_;
mockProtocol = mockProtocol_;
resolver = new FluidLiquidityResolver(IFluidLiquidity(address(liquidity_)));
}
function _assertLiquidityExchangePrices() internal {
(, , , , , , , uint256 newLiquidityExchangePrice, uint256 newTokenExchangePrice) = IFToken(address(token))
.getData();
FluidLiquidityResolverStructs.OverallTokenData memory overallTokenData = resolver.getOverallTokenData(
address(underlying)
);
assertGe(overallTokenData.borrowExchangePrice, borrowExchangePrice); // new borrow exchange price >= borrow exchange price
assertGe(overallTokenData.supplyExchangePrice, supplyExchangePrice); // new borrow exchange price >= borrow exchange price
assertGe(newLiquidityExchangePrice, liquidityExchangePrice);
assertGe(newTokenExchangePrice, tokenExchangePrice);
borrowExchangePrice = overallTokenData.borrowExchangePrice;
supplyExchangePrice = overallTokenData.supplyExchangePrice;
liquidityExchangePrice = newLiquidityExchangePrice;
tokenExchangePrice = newTokenExchangePrice;
}
struct DepositState {
uint256 assetsBefore;
uint256 sharesBefore;
uint256 assetsAfter;
uint256 sharesAfter;
uint256 currentTokenExchangePrice;
uint256 expectedShares;
}
function deposit(
uint256 assets,
address receiver,
uint256 actorIndexSeed
) external randomTimeWarp useActor(actorIndexSeed) randomSupplyAndBorrow returns (uint256 shares) {
assets = bound(assets, 1e4, 1e18);
underlying.mint(currentActor, assets);
underlying.approve(address(token), type(uint256).max);
DepositState memory state;
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
state.currentTokenExchangePrice = currentTokenExchangePrice;
// solidity rounds down by default
state.expectedShares = (assets * LiquidityCalcs.EXCHANGE_PRICES_PRECISION) / state.currentTokenExchangePrice;
state.assetsBefore = underlying.balanceOf(currentActor);
state.sharesBefore = token.balanceOf(currentActor);
shares = _executeDeposit(assets, state);
ghost_sumBalanceOf += shares;
_assertLiquidityExchangePrices();
return shares;
}
function _executeDeposit(uint256 assets, DepositState memory state) internal returns (uint256 shares) {
shares = token.deposit(assets, currentActor);
state.assetsAfter = underlying.balanceOf(currentActor);
state.sharesAfter = token.balanceOf(currentActor);
// equal because expectedAssetsReceived is already rounded down as default in Solidity
assertEq(state.sharesAfter - state.sharesBefore, state.expectedShares);
assertEq(state.assetsBefore - state.assetsAfter, assets);
// shares * tokenExchangePrice / 1e12
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
uint256 maxWithdrawAmount = token.maxWithdraw(currentActor);
assertLe(maxWithdrawAmount, ((token.balanceOf(currentActor) * currentTokenExchangePrice) / 1e12));
uint256 maxRedeemAmount = token.maxRedeem(currentActor);
assertLe(maxRedeemAmount, token.balanceOf(currentActor));
return shares;
}
struct MintState {
uint256 assetsBefore;
uint256 sharesBefore;
uint256 assetsAfter;
uint256 sharesAfter;
uint256 currentTokenExchangePrice;
uint256 expectedAssets;
}
function mint(
uint256 shares,
uint256 actorIndexSeed
) external randomTimeWarp useActor(actorIndexSeed) randomSupplyAndBorrow {
shares = bound(shares, 1e4, 1e18);
uint256 consumeAssets = token.previewMint(shares);
underlying.mint(currentActor, consumeAssets);
underlying.approve(address(token), type(uint256).max);
MintState memory state;
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
state.currentTokenExchangePrice = currentTokenExchangePrice;
state.expectedAssets = (shares * state.currentTokenExchangePrice) / LiquidityCalcs.EXCHANGE_PRICES_PRECISION;
state.assetsBefore = underlying.balanceOf(currentActor);
state.sharesBefore = token.balanceOf(currentActor);
_executeMint(shares, state);
ghost_sumBalanceOf += shares;
_assertLiquidityExchangePrices();
}
function _executeMint(uint256 shares, MintState memory state) internal {
try token.mint(shares, currentActor) {} catch (bytes memory lowLevelData) {
// we ignore cases when there is arithmetic error or withdrawal limit is reached
if (keccak256(abi.encodePacked(lowLevelData)) != keccak256(abi.encodePacked(stdError.arithmeticError))) {
assertEq(true, false);
}
}
state.assetsAfter = underlying.balanceOf(currentActor);
state.sharesAfter = token.balanceOf(currentActor);
assertEq(state.sharesAfter - state.sharesBefore, shares);
assertTrue(state.assetsBefore - state.assetsAfter >= state.expectedAssets);
// shares * tokenExchangePrice / 1e12
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
uint256 maxWithdrawAmount = token.maxWithdraw(currentActor);
assertLe(maxWithdrawAmount, ((token.balanceOf(currentActor) * currentTokenExchangePrice) / 1e12));
uint256 maxRedeemAmount = token.maxRedeem(currentActor);
assertLe(maxRedeemAmount, token.balanceOf(currentActor));
}
struct WithdrawState {
uint256 assetsBefore;
uint256 sharesBefore;
uint256 assetsAfter;
uint256 sharesAfter;
uint256 currentTokenExchangePrice;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 actorIndexSeed
) external randomTimeWarp useActor(actorIndexSeed) randomSupplyAndBorrow {
assets = bound(assets, 1e4, 1e18);
underlying.mint(currentActor, assets);
underlying.approve(address(token), type(uint256).max);
WithdrawState memory state;
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
state.currentTokenExchangePrice = currentTokenExchangePrice;
uint256 sharesToBurn_ = assets.mulDivUp(
LiquidityCalcs.EXCHANGE_PRICES_PRECISION,
state.currentTokenExchangePrice
);
if (token.balanceOf(currentActor) < sharesToBurn_) return;
state.assetsBefore = underlying.balanceOf(currentActor);
state.sharesBefore = token.balanceOf(currentActor);
_executeWithdraw(assets, state);
_assertLiquidityExchangePrices();
}
function _executeWithdraw(uint256 assets, WithdrawState memory state) internal {
try token.withdraw(assets, currentActor, currentActor) returns (uint256 shares) {
ghost_sumBalanceOf -= shares;
state.assetsAfter = underlying.balanceOf(currentActor);
state.sharesAfter = token.balanceOf(currentActor);
uint256 expectedBurnedShares = (assets * LiquidityCalcs.EXCHANGE_PRICES_PRECISION) /
state.currentTokenExchangePrice;
// assert that the actual burned shares are greater than or equal to the expected amount, confirming rounding up
assertTrue(state.sharesBefore - state.sharesAfter >= expectedBurnedShares);
assertEq(state.assetsAfter - state.assetsBefore, assets);
// shares * tokenExchangePrice / 1e12
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
uint256 maxWithdrawAmount = token.maxWithdraw(currentActor);
assertLe(maxWithdrawAmount, ((token.balanceOf(currentActor) * currentTokenExchangePrice) / 1e12));
uint256 maxRedeemAmount = token.maxRedeem(currentActor);
assertLe(maxRedeemAmount, token.balanceOf(currentActor));
} catch (bytes memory lowLevelData) {
// we ignore cases when there is arithmetic error or withdrawal limit is reached
if (keccak256(abi.encodePacked(lowLevelData)) != keccak256(abi.encodePacked(stdError.arithmeticError))) {
assertEq(true, false);
}
}
}
struct RedeemState {
uint256 assetsBefore;
uint256 sharesBefore;
uint256 assetsAfter;
uint256 sharesAfter;
}
function redeem(
uint256 shares,
address receiver,
address owner,
uint256 actorIndexSeed
) external randomTimeWarp useActor(actorIndexSeed) randomSupplyAndBorrow {
shares = bound(shares, 1e4, 1e18);
uint256 sharesBalance = token.balanceOf(currentActor);
if (shares > sharesBalance) {
shares = sharesBalance;
}
if (token.previewRedeem(sharesBalance) == 0) return;
RedeemState memory state;
state.assetsBefore = underlying.balanceOf(currentActor);
state.sharesBefore = token.balanceOf(currentActor);
_executeRedeem(shares, state);
_assertLiquidityExchangePrices();
}
function _executeRedeem(uint256 shares, RedeemState memory state) internal {
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
try token.redeem(shares, currentActor, currentActor) {
ghost_sumBalanceOf -= shares;
state.assetsAfter = underlying.balanceOf(currentActor);
state.sharesAfter = token.balanceOf(currentActor);
uint256 expectedAssetsReceived = (shares * currentTokenExchangePrice) /
LiquidityCalcs.EXCHANGE_PRICES_PRECISION;
// equal because expectedAssetsReceived is already rounded down as default in Solidity
assertEq(state.assetsAfter - state.assetsBefore, expectedAssetsReceived);
// the combination of rounding down in previewRedeem and rounding up in executeWithdraw does not lead to a situation where the amount of burned shares is higher than the input shares.
assertEq(state.sharesBefore - state.sharesAfter, shares);
// shares * tokenExchangePrice / 1e12
(, , , , , , , , uint256 currentTokenExchangePrice) = IFToken(address(token)).getData();
uint256 maxWithdrawAmount = token.maxWithdraw(currentActor);
assertLe(maxWithdrawAmount, ((token.balanceOf(currentActor) * currentTokenExchangePrice) / 1e12));
uint256 maxRedeemAmount = token.maxRedeem(currentActor);
assertLe(maxRedeemAmount, token.balanceOf(currentActor));
} catch (bytes memory lowLevelData) {
// we ignore cases when there is arithmetic error or withdrawal limit is reached
if (keccak256(abi.encodePacked(lowLevelData)) != keccak256(abi.encodePacked(stdError.arithmeticError))) {
assertEq(true, false);
}
}
}
function updateRewards() external randomTimeWarp useAdminActor {
//NOTE: function changes rateModel address to zero address in order to get the lowest rewards in this case 0 .
token.updateRewards(IFluidLendingRewardsRateModel(address(0)));
_assertLiquidityExchangePrices();
}
function updateRates()
public
randomTimeWarp
returns (uint256 tokenExchangePrice_, uint256 liquidityExchangePrice_)
{
token.updateRates();
_assertLiquidityExchangePrices();
}
function rebalance()
public
randomTimeWarp
useAdminActor
returns (uint256 tokenExchangePrice_, uint256 liquidityExchangePrice_)
{
underlying.mint(admin, 1e70); // assume rebalancer owns and approves enough for executing rebalance
underlying.approve(address(token), 1e70);
IFToken(address(token)).updateRebalancer(admin); //giving access to rebalance
try IFToken(address(token)).rebalance() {
_assertLiquidityExchangePrices();
} catch (bytes memory lowLevelData) {
if (
keccak256(abi.encodePacked(lowLevelData)) != keccak256(abi.encodePacked(stdError.arithmeticError)) &&
keccak256(abi.encodePacked(lowLevelData)) !=
keccak256(
abi.encodeWithSelector(
Error.FluidLiquidityError.selector,
ErrorTypes.UserModule__OperateAmountsZero
)
) &&
keccak256(abi.encodePacked(lowLevelData)) !=
keccak256(
abi.encodeWithSelector(
Error.FluidLiquidityError.selector,
ErrorTypes.UserModule__OperateAmountInsufficient
)
)
) {
assertEq(true, false);
}
}
}
}