diff --git a/.env.template b/.env.template index 4d05383..588da0c 100644 --- a/.env.template +++ b/.env.template @@ -65,6 +65,11 @@ RESERVE_PROFIT= INITIAL_INDEX= +####################################################### +## Address of Uniswap V2 ONLY! UniswapRouterV2 ONLY! ## +####################################################### +DEX_ROUTER= + ## Network specific native coefficient COEFFICIENT_NUMERATOR= COEFFICIENT_DENOMINATOR= diff --git a/src/Staking.sol b/src/Staking.sol index 31ce1e4..36ee273 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -35,6 +35,7 @@ contract GhostStaking is IStaking, GhostAccessControlled { address public distributor; address public gatekeeper; address public warmup; + uint96 public dexRouterIndex; uint256 private _lastRebaseBlock; @@ -233,9 +234,27 @@ contract GhostStaking is IStaking, GhostAccessControlled { _lastRebaseBlock = block.number; epoch.distribute = balance > extra ? balance - extra : 0; + + address treasury = ISTNK(stnk).treasury(); + // forge-lint: disable-next-line(unsafe-typecast) + ITreasury(treasury).capturePriceSnapshot(uint256(dexRouterIndex)); } } + function setRouterRegistryIndex(uint256 _dexRouterIndex) external onlyGovernor { + address treasury = ISTNK(stnk).treasury(); + address dexRouter = ITreasury(treasury).registeredAt(ITreasury.STATUS.DEXROUTER, _dexRouterIndex); + + bool updated = false; + if (ITreasury(treasury).permissioned(ITreasury.STATUS.DEXROUTER, dexRouter)) { + // forge-lint: disable-next-line(unsafe-typecast) + dexRouterIndex = uint96(_dexRouterIndex); + updated = true; + } + + emit RouterRegistryIndexSet(dexRouter, _dexRouterIndex, updated); + } + function setDistributor(address _distributor) external onlyGovernor { distributor = _distributor; emit DistributorSet(_distributor); diff --git a/src/StakingDistributor.sol b/src/StakingDistributor.sol index 01b9243..4558c7f 100644 --- a/src/StakingDistributor.sol +++ b/src/StakingDistributor.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; -import {IUniswapV2Pair} from "@uniswap-v2-core/interfaces/IUniswapV2Pair.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-contracts/interfaces/IUniswapV2Pair.sol"; import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {FullMath} from "./libraries/FullMath.sol"; diff --git a/src/StandardBondingCalculator.sol b/src/StandardBondingCalculator.sol index 98931d8..dbb63aa 100644 --- a/src/StandardBondingCalculator.sol +++ b/src/StandardBondingCalculator.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {FixedPoint} from "./libraries/FixedPoint.sol"; import {IERC20Metadata} from "@openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {IUniswapV2Pair} from "@uniswap-v2-core/interfaces/IUniswapV2Pair.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-contracts/interfaces/IUniswapV2Pair.sol"; import {IBondingCalculator} from "./interfaces/IBondingCalculator.sol"; import {FullMath} from "./libraries/FullMath.sol"; diff --git a/src/Treasury.sol b/src/Treasury.sol index 3022f60..e225d97 100644 --- a/src/Treasury.sol +++ b/src/Treasury.sol @@ -6,10 +6,10 @@ import {IERC20Metadata} from "@openzeppelin-contracts/token/ERC20/extensions/IER 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 {IUniswapV2Pair} from "@uniswap-v2-contracts/interfaces/IUniswapV2Pair.sol"; +import {IUniswapV2Factory} from "@uniswap-v2-contracts/interfaces/IUniswapV2Factory.sol"; +import {IUniswapV2Router} from "@uniswap-v2-contracts/interfaces/IUniswapV2Router.sol"; +import {IUniswapV2Router01} from "@uniswap-v2-contracts/interfaces/IUniswapV2Router01.sol"; import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {Babylonian} from "./libraries/FixedPoint.sol"; @@ -29,6 +29,10 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { address public bondDepository; uint256 public totalReserves; + mapping(address => uint32) public blockTimestampSnapshots; + mapping(address => uint256) public cummulativePriceSnapshots; + mapping(address => uint256) public capturedPriceSnapshots; + mapping(STATUS => EnumerableSet.AddressSet) private _registry; mapping(STATUS => mapping(address => bool)) private _permissions; mapping(address => address) public bondCalculator; @@ -123,6 +127,39 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { IERC20(reserveToken).safeTransfer(receiver, value); } + function capturePriceSnapshot(uint256 index) external override { + if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); + if (index >= _registry[STATUS.DEXROUTER].length()) return; + + address dexRouter = _registry[STATUS.DEXROUTER].at(index); + if (!_permissions[STATUS.DEXROUTER][dexRouter] || dexRouter == address(0)) return; + + address wethAddress = IUniswapV2Router01(dexRouter).WETH(); + address factory = IUniswapV2Router01(dexRouter).factory(); + if (wethAddress == address(0) || factory == address(0) || wethAddress == FTSO) return; + + address pairAddress = IUniswapV2Factory(factory).getPair(FTSO, wethAddress); + if (!_permissions[STATUS.LIQUIDITYTOKEN][pairAddress] || pairAddress == address(0)) return; + + (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = + IUniswapV2Pair(pairAddress).getReserves(); + + uint256 cummulativePrice; + if (wethAddress < FTSO) { cummulativePrice = IUniswapV2Pair(pairAddress).price0CumulativeLast(); } + else { cummulativePrice = IUniswapV2Pair(pairAddress).price1CumulativeLast(); } + + unchecked { + // overflow is desired + uint32 timeElapsed = blockTimestampLast - blockTimestampSnapshots[pairAddress]; + if (timeElapsed > 0 && reserve0 != 0 && reserve1 != 0) { + capturedPriceSnapshots[pairAddress] = + (cummulativePrice - cummulativePriceSnapshots[pairAddress]) / timeElapsed; + blockTimestampSnapshots[pairAddress] = blockTimestampLast; + cummulativePriceSnapshots[pairAddress] = cummulativePrice; + } + } + } + function auditReserves() external { if ( msg.sender != authority.governor() && @@ -169,13 +206,22 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { ) 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(); + if (_registry[status].contains(someAddress)) { + emit Permissioned(someAddress, status, true); + return; + } - _registry[status].add(someAddress); + bool isToken = status == STATUS.LIQUIDITYTOKEN || status == STATUS.RESERVETOKEN; + + if (isToken) { + if (calculatorAddress == address(0)) revert InvalidCalculator(); bondCalculator[someAddress] = calculatorAddress; } + + if (isToken || status == STATUS.DEXROUTER) { + _registry[status].add(someAddress); + } + emit Permissioned(someAddress, status, true); } @@ -202,6 +248,9 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { uint256 liquidity, bool destroyerMode ) external onlyGovernor { + if (!_registry[STATUS.DEXROUTER].contains(router)) return; + if (!_permissions[STATUS.DEXROUTER][router]) return; + address weth = IUniswapV2Router01(router).WETH(); address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(FTSO, weth); @@ -229,6 +278,9 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { address router, uint256 amount ) external onlyGovernor { + if (!_registry[STATUS.DEXROUTER].contains(router)) return; + if (!_permissions[STATUS.DEXROUTER][router]) return; + address weth = IUniswapV2Router01(router).WETH(); address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(FTSO, weth); @@ -246,8 +298,8 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { } uint256 amountIn = _quantityToBeSwapped(amount, reserve0); - uint256[] memory amounts = IUniswapV2Router02(router).getAmountsOut(amountIn, path); - IUniswapV2Router02(router).swapExactTokensForTokens( + uint256[] memory amounts = IUniswapV2Router(router).getAmountsOut(amountIn, path); + IUniswapV2Router(router).swapExactTokensForTokens( amountIn, amounts[1], path, @@ -262,14 +314,18 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { IUniswapV2Pair(pair).mint(address(this)); } - function permissioned(STATUS status, address someAddress) external view returns (bool) { + function permissioned(STATUS status, address someAddress) external override view returns (bool) { return _permissions[status][someAddress]; } - function registered(STATUS status, address someAddress) external override view returns (bool) { + function registered(STATUS status, address someAddress) external override view returns (bool isRegistered) { return _registry[status].contains(someAddress); } + function registeredAt(STATUS status, uint256 index) external override view returns (address) { + return _registry[status].at(index); + } + function originalCoefficient() external view returns (uint256) { address reserveToken = _registry[STATUS.RESERVETOKEN].at(0); return IBondingCalculator(bondCalculator[reserveToken]).fraction(); diff --git a/src/interfaces/IStaking.sol b/src/interfaces/IStaking.sol index 27e5b42..acb1171 100644 --- a/src/interfaces/IStaking.sol +++ b/src/interfaces/IStaking.sol @@ -23,6 +23,7 @@ interface IStaking { bool isRebase ); + event RouterRegistryIndexSet(address router, uint256 index, bool changed); event DistributorSet(address distributor); event GatekeeperSet(address gatekeeper); event WarmupSet(uint256 warmup); diff --git a/src/interfaces/ITreasury.sol b/src/interfaces/ITreasury.sol index c873d19..0375b5b 100644 --- a/src/interfaces/ITreasury.sol +++ b/src/interfaces/ITreasury.sol @@ -6,6 +6,7 @@ interface ITreasury { error NotAccepted(); error InvalidToken(); error GasExceedsBounty(); + error InvalidCalculator(); enum STATUS { RESERVEDEPOSITOR, @@ -13,7 +14,8 @@ interface ITreasury { LIQUIDITYDEPOSITOR, LIQUIDITYTOKEN, REWARDMANAGER, - STAKING + STAKING, + DEXROUTER } event Deposit(address indexed token, uint256 amount, uint256 value); @@ -35,11 +37,14 @@ interface ITreasury { uint256 _profit ) external returns (uint256); - function registered(STATUS status, address token) external view returns(bool); + function registered(STATUS status, address token) external view returns (bool); + function permissioned(STATUS status, address token) external view returns (bool); + function registeredAt(STATUS status, uint256 index) external view returns (address); function phantomRefund(address receiver, uint48 phantomExpiry, uint256 gasSpent, uint256 amount) external; function buyBack(address receiver, uint256 amount, uint256 index) external returns (address, uint256); function withdraw(address token, uint256 amount) external; function mint(address _recipient, uint256 _amount) external; + function capturePriceSnapshot(uint256 index) external; function tokenValue(address _token, uint256 _amount) external view returns (uint256 value_); function excessReserves() external view returns (uint256); function baseSupply() external view returns (uint256); diff --git a/test/bonding/BondDepositorty.t.sol b/test/bonding/BondDepositorty.t.sol index 9f7e8c6..966d2a3 100644 --- a/test/bonding/BondDepositorty.t.sol +++ b/test/bonding/BondDepositorty.t.sol @@ -100,6 +100,7 @@ contract GhostBondDepositoryTest is Test { treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(depository), address(0)); treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0)); treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator)); + treasury.enable(ITreasury.STATUS.STAKING, address(staking), address(0)); vm.stopPrank(); vm.startPrank(ALICE); diff --git a/test/staking/Staking.t.sol b/test/staking/Staking.t.sol index 34b3fc5..140aad1 100644 --- a/test/staking/Staking.t.sol +++ b/test/staking/Staking.t.sol @@ -113,6 +113,9 @@ contract StakingTest is Test { calculator = new GhostBondingCalculator(address(ftso), 1, 1); vm.stopPrank(); vm.roll(block.number + 1); + + vm.prank(GOVERNOR); + treasury.enable(ITreasury.STATUS.STAKING, address(staking), address(0)); } function test_correctAfterConstruction() public view { @@ -157,7 +160,7 @@ contract StakingTest is Test { assertEq(staking.warmupPeriod(), maybeWarmupPeriod); } - function test_emitsWarmupEvent(uint48 maybeWarmupPeriod) public { + function test_emitsWarmupEvent(uint256 maybeWarmupPeriod) public { vm.expectEmit(true, true, true, false, address(staking)); emit WarmupSet(maybeWarmupPeriod); vm.prank(GOVERNOR); diff --git a/test/staking/StakingDistributor.t.sol b/test/staking/StakingDistributor.t.sol index 0a0e3b5..c11a11b 100644 --- a/test/staking/StakingDistributor.t.sol +++ b/test/staking/StakingDistributor.t.sol @@ -79,6 +79,7 @@ contract StakingDistributorTest is Test { vm.startPrank(GOVERNOR); staking.setDistributor(address(distributor)); authority.pushVault(address(treasury)); + vm.stopPrank(); } @@ -241,6 +242,7 @@ contract StakingDistributorTest is Test { treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0)); treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, BOB, address(0)); treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator)); + treasury.enable(ITreasury.STATUS.STAKING, address(staking), address(0)); vm.stopPrank(); vm.startPrank(ALICE); diff --git a/test/treasury/Treasury.t.sol b/test/treasury/Treasury.t.sol index 9d26d49..67c4545 100644 --- a/test/treasury/Treasury.t.sol +++ b/test/treasury/Treasury.t.sol @@ -9,7 +9,7 @@ import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol"; import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol"; import {ITreasury} from "../../src/interfaces/ITreasury.sol"; -import {IUniswapV2Pair} from "@uniswap-v2-core/interfaces/IUniswapV2Pair.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-contracts/interfaces/IUniswapV2Pair.sol"; contract GhostTreasuryTest is Test { address public constant OWNER = 0x0000000000000000000000000000000000000001; diff --git a/test/treasury/TreasuryRedemption.t.sol b/test/treasury/TreasuryRedemption.t.sol index 47f7f94..49c3d94 100644 --- a/test/treasury/TreasuryRedemption.t.sol +++ b/test/treasury/TreasuryRedemption.t.sol @@ -10,7 +10,7 @@ import {ITreasury} from "../../src/interfaces/ITreasury.sol"; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; -import {IWETH} from "@uniswap-v2-periphery-1.1.0-beta.0/interfaces/IWETH.sol"; +import {IWETH} from "@uniswap-v2-contracts/interfaces/IWETH.sol"; contract GhostTreasuryRedemptionTest is Test { using SafeERC20 for IERC20;