// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol"; import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; import {IUniswapV2Router02} from "@uniswap-v2-periphery-1.1.0-beta.0/interfaces/IUniswapV2Router02.sol"; import {IUniswapV2Router01} from "@uniswap-v2-periphery-1.1.0-beta.0/interfaces/IUniswapV2Router01.sol"; import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {Babylonian} from "./libraries/FixedPoint.sol"; import {FullMath} from "./libraries/FullMath.sol"; import {IFTSO} from "./interfaces/IFTSO.sol"; import {IBondingCalculator} from "./interfaces/IBondingCalculator.sol"; import {ITreasury} from "./interfaces/ITreasury.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; contract GhostTreasury is GhostAccessControlled, ITreasury { using SafeERC20 for IERC20; address public immutable ftso; // forge-lint: disable-line(screaming-snake-case-immutable) uint256 public immutable blocksNeededForQueue; // forge-lint: disable-line(screaming-snake-case-immutable) uint256 public totalReserves; uint256 public totalDebt; uint256 public ftsoDebt; mapping(STATUS => address[]) public registry; mapping(STATUS => mapping(address => bool)) public permissions; mapping(address => address) public bondCalculator; constructor( address _ftso, uint256 _timelock, address _authority ) GhostAccessControlled(IGhostAuthority(_authority)) { ftso = _ftso; blocksNeededForQueue = _timelock; } function deposit( address token, uint256 amount, uint256 profit ) external override returns (uint256 send) { if (permissions[STATUS.RESERVETOKEN][token]) { if (!permissions[STATUS.RESERVEDEPOSITOR][msg.sender]) revert NotApproved(); } else if (permissions[STATUS.LIQUIDITYTOKEN][token]) { if (!permissions[STATUS.LIQUIDITYDEPOSITOR][msg.sender]) revert NotApproved(); } else revert InvalidToken(); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); uint256 value = tokenValue(token, amount); send = value - profit; IFTSO(ftso).mint(msg.sender, send); totalReserves = totalReserves + value; emit Deposit(token, amount, value); } function mint(address recipient, uint256 amount) external override { if (!permissions[STATUS.REWARDMANAGER][msg.sender]) revert NotApproved(); if (amount > excessReserves()) revert InsufficientReserves(); IFTSO(ftso).mint(recipient, amount); emit Minted(msg.sender, recipient, amount); } function withdraw(address token, uint256 amount) external onlyGovernor override { if (!permissions[STATUS.RESERVETOKEN][token]) revert NotAccepted(); uint256 value = tokenValue(token, amount); totalReserves = totalReserves - value; IERC20(token).safeTransfer(msg.sender, amount); emit Withdrawal(token, amount, value); } function auditReserves() external { if ( msg.sender != authority.governor() && msg.sender != authority.guardian() ) revert NotApproved(); uint256 reserves; address[] memory reserveTokens = registry[STATUS.RESERVETOKEN]; uint256 i; for (; i < reserveTokens.length;) { address reserveToken = reserveTokens[i]; if (permissions[STATUS.RESERVETOKEN][reserveToken]) { reserves = reserves + tokenValue( reserveToken, IERC20(reserveToken).balanceOf(address(this)) ); } unchecked { ++i; } } address[] memory liquidityTokens = registry[STATUS.LIQUIDITYTOKEN]; i = 0; for (; i < liquidityTokens.length;) { address liquidityToken = liquidityTokens[i]; if (permissions[STATUS.LIQUIDITYTOKEN][liquidityToken]) { reserves = reserves + tokenValue( liquidityToken, IERC20(liquidityToken).balanceOf(address(this)) ); } unchecked { ++i; } } totalReserves = reserves; emit ReservesAudited(reserves); } function enable( STATUS status, address someAddress, address calculatorAddress ) external onlyGovernor { permissions[status][someAddress] = true; (bool registered, ) = indexInRegistry(someAddress, status); if (!registered && (status == STATUS.LIQUIDITYTOKEN || status == STATUS.RESERVETOKEN)) { assert(calculatorAddress != address(0)); registry[status].push(someAddress); bondCalculator[someAddress] = calculatorAddress; } emit Permissioned(someAddress, status, true); } function disable(STATUS status, address toDisable) external { if ( msg.sender != authority.governor() && msg.sender != authority.guardian() ) revert NotApproved(); permissions[status][toDisable] = false; emit Permissioned(toDisable, status, false); } function forfeitReserves( address router, uint256 liquidity, bool destroyerMode ) external onlyGovernor { address weth = IUniswapV2Router01(router).WETH(); address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(ftso, weth); IERC20(pair).safeTransfer(pair, liquidity); (uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(address(this)); if (destroyerMode) { uint256 amountToDestroy; address token0 = IUniswapV2Pair(pair).token0(); address token1 = IUniswapV2Pair(pair).token1(); if (token0 == ftso) { amountToDestroy = amount0; } if (token1 == ftso) { amountToDestroy = amount1; } IFTSO(ftso).burn(amountToDestroy); } } function redeemReserve( address router, uint256 amount ) external onlyGovernor { address weth = IUniswapV2Router01(router).WETH(); address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(ftso, weth); IERC20(weth).approve(router, amount); (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pair).getReserves(); address[] memory path = new address[](2); path[0] = weth; path[1] = ftso; if (ftso < weth) { reserve0 = reserve1 ^ reserve0; reserve1 = reserve1 ^ reserve0; reserve0 = reserve1 ^ reserve0; } uint256 amountIn = _quantityToBeSwapped(amount, reserve0); uint256[] memory amounts = IUniswapV2Router02(router).getAmountsOut(amountIn, path); IUniswapV2Router02(router).swapExactTokensForTokens( amountIn, amounts[1], path, address(this), block.timestamp ); amountIn = amount - amountIn; IERC20(weth).safeTransfer(pair, amountIn); IERC20(ftso).safeTransfer(pair, amounts[1]); IUniswapV2Pair(pair).mint(address(this)); } function indexInRegistry( address someAddress, STATUS status ) public view override returns (bool, uint256) { address[] memory entries = registry[status]; uint256 i; for (; i < entries.length; ) { if (someAddress == entries[i]) { return (true, i); } unchecked { ++i; } } return (false, 0); } function originalCoefficient() external view returns (uint256) { address[] memory reserveTokens = registry[STATUS.RESERVETOKEN]; return IBondingCalculator(bondCalculator[reserveTokens[0]]).fraction(); } function excessReserves() public view override returns (uint256) { return totalReserves - (IFTSO(ftso).totalSupply() - totalDebt); } function tokenValue( address token, uint256 amount ) public view override returns (uint256 value) { if (permissions[STATUS.LIQUIDITYTOKEN][token]) { value = IBondingCalculator(bondCalculator[token]).valuation(token, amount); } else if (permissions[STATUS.RESERVETOKEN][token]) { value = FullMath.mulDiv(amount, 1e9, 10**IERC20Metadata(token).decimals()); value = FullMath.mulDiv(value, IBondingCalculator(bondCalculator[token]).fraction(), 1e18); } } function baseSupply() external view override returns (uint256) { return IFTSO(ftso).totalSupply() - ftsoDebt; } function _quantityToBeSwapped(uint256 xa, uint256 x1) internal pure returns (uint256) { uint256 y1 = x1 * 1994 / 1000; uint256 y2 = Babylonian.sqrt(y1**2 + 4 * xa * x1 * 997 / 1000); return (y2 - y1) * 1000 / 1994; } }