implement TWAP through cumulative price from the DEX pair

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-10-23 17:35:40 +03:00
parent b06f6c115f
commit 6d64494797
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
12 changed files with 111 additions and 19 deletions

View File

@ -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=

View File

@ -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);

View File

@ -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";

View File

@ -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";

View File

@ -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();

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;