// 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 {EnumerableSet} from "@openzeppelin-contracts/utils/structs/EnumerableSet.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 {IBondDepository} from "./interfaces/IBondDepository.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; contract GhostTreasury is GhostAccessControlled, ITreasury { using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.AddressSet; address public immutable FTSO; address public bondDepository; uint256 public totalReserves; mapping(STATUS => EnumerableSet.AddressSet) private _registry; mapping(STATUS => mapping(address => bool)) private _permissions; mapping(address => address) public bondCalculator; constructor( address _ftso, address _authority ) GhostAccessControlled(IGhostAuthority(_authority)) { FTSO = _ftso; } 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()) { 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 phantomRefund( address receiver, uint48 phantomRelease, uint256 gasSpent, uint256 registryIndex ) external override returns (uint256 gasValue) { if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); address reserveToken = _registry[STATUS.RESERVETOKEN].at(registryIndex); if (!_permissions[STATUS.RESERVETOKEN][reserveToken]) revert NotApproved(); uint256 totalSupply = IERC20(FTSO).totalSupply(); gasValue = tokenValue(reserveToken, gasSpent); gasValue = FullMath.mulDiv(gasValue, totalSupply, totalReserves); if (gasValue > 0 && gasValue <= excessReserves()) { IBondDepository(bondDepository).addNote(receiver, gasValue, phantomRelease); } } function buyBack( address receiver, uint256 amount, uint256 index ) external override returns (address reserveToken, uint256 value) { if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); reserveToken = _registry[STATUS.RESERVETOKEN].at(index); if (!_permissions[STATUS.RESERVETOKEN][reserveToken]) revert NotApproved(); uint256 totalSupply = IERC20(FTSO).totalSupply(); uint256 reservesToSend = FullMath.mulDiv(amount, totalReserves, totalSupply); totalReserves = totalReserves - reservesToSend; value = FullMath.mulDiv(reservesToSend, 1e18, IBondingCalculator(bondCalculator[reserveToken]).fraction()); value = FullMath.mulDiv(value, 10**IERC20Metadata(reserveToken).decimals(), 1e9); IERC20(reserveToken).safeTransfer(receiver, value); } function auditReserves() external { if ( msg.sender != authority.governor() && msg.sender != authority.guardian() ) revert NotApproved(); uint256 reserves; uint256 reserveTokensLength = _registry[STATUS.RESERVETOKEN].length(); uint256 i; for (; i < reserveTokensLength;) { address reserveToken = _registry[STATUS.RESERVETOKEN].at(i); if (_permissions[STATUS.RESERVETOKEN][reserveToken]) { reserves = reserves + tokenValue( reserveToken, IERC20(reserveToken).balanceOf(address(this)) ); } unchecked { ++i; } } uint256 liquidityTokensLength = _registry[STATUS.LIQUIDITYTOKEN].length(); i = 0; for (; i < liquidityTokensLength;) { address liquidityToken = _registry[STATUS.LIQUIDITYTOKEN].at(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 alreadyRegistered = _registry[status].contains(someAddress); if (!alreadyRegistered && (status == STATUS.LIQUIDITYTOKEN || status == STATUS.RESERVETOKEN)) { if (calculatorAddress == address(0)) revert(); _registry[status].add(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 setBondDepository(address newBondDepository) external { if ( msg.sender != authority.governor() && msg.sender != authority.guardian() ) revert NotApproved(); bondDepository = newBondDepository; emit BondDepositoryUpdated(newBondDepository); } 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 permissioned(STATUS status, address someAddress) external view returns (bool) { return _permissions[status][someAddress]; } function registered(STATUS status, address someAddress) external override view returns (bool) { return _registry[status].contains(someAddress); } function originalCoefficient() external view returns (uint256) { address reserveToken = _registry[STATUS.RESERVETOKEN].at(0); return IBondingCalculator(bondCalculator[reserveToken]).fraction(); } function excessReserves() public view override returns (uint256) { uint256 ftsoSupply = IFTSO(FTSO).totalSupply(); uint256 currentTotalReserves = totalReserves; return currentTotalReserves > ftsoSupply ? currentTotalReserves - ftsoSupply : 0; } 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(); } 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; } }