From f79b32554d5f8531d48ce53168734b831c40a758 Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Tue, 27 Oct 2026 17:06:47 +0300 Subject: [PATCH] use TWAP price from DEX for phantom refunds Signed-off-by: Uncle Fatso --- dependencies/uniswap-v2-contracts | 2 +- src/Gatekeeper.sol | 6 +- src/Staking.sol | 5 +- src/Treasury.sol | 18 ++-- src/interfaces/IStaking.sol | 2 +- src/interfaces/ITreasury.sol | 2 +- test/gatekeeper/GatekeeperRecall.t.sol | 81 ++++++++++++++++-- test/treasury/TreasuryCumulativePrice.t.soll | 88 ++++++++++++++++++++ 8 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 test/treasury/TreasuryCumulativePrice.t.soll diff --git a/dependencies/uniswap-v2-contracts b/dependencies/uniswap-v2-contracts index 5f4365a..b131a06 160000 --- a/dependencies/uniswap-v2-contracts +++ b/dependencies/uniswap-v2-contracts @@ -1 +1 @@ -Subproject commit 5f4365a3a872a0df3f6026402bba7ac71baa9b33 +Subproject commit b131a064dbcfe6b2750d0f4c3d576b23c561fdb9 diff --git a/src/Gatekeeper.sol b/src/Gatekeeper.sol index 520d61f..be2208a 100644 --- a/src/Gatekeeper.sol +++ b/src/Gatekeeper.sol @@ -26,7 +26,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { uint256 public constant FULL_GAS_RESTORATION_TIME = 3600; // 1 hour uint256 public constant MAX_ALLOWED_GAS_PRICE = 3e9; // 3 gwei uint256 public constant PHANTOM_EXPIRY = 432000; // seconds, 5 days - uint256 public constant GAS_EXECUTION_BUFFER = 21000 + 5969 + 50955; + uint256 public constant GAS_EXECUTION_BUFFER = 21000 + 5969 + 67585; uint256 public constant REGISTRY_INDEX = 0; address public override staking; @@ -117,6 +117,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { } function ghost(bytes32 receiver, uint256 amount) external override { + // TODO: if (!_initialized) revert SomeCoolError(); if (msg.sender != staking) revert NotStaking(); if (amount < EXISTENTIAL_DEPOSIT) revert NonExistentAmount(); @@ -129,6 +130,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { uint256 rx, uint256 s ) external nonReentrant { + // TODO: if (!_initialized) revert SomeCoolError(); uint256 gasStart = gasleft(); uint256 px = _extractPublicKey(call); @@ -151,7 +153,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { _lastVerificationTime = block.timestamp; uint256 gasSpent = (gasStart - gasleft() + GAS_EXECUTION_BUFFER) * currentGasPrice; - try IStaking(staking).phantomRefund(msg.sender, gasSpent, REGISTRY_INDEX, PHANTOM_EXPIRY) {} + try IStaking(staking).phantomRefund(msg.sender, gasSpent, PHANTOM_EXPIRY) {} catch { emit VoluntaryVerification(msg.sender, gasSpent); } diff --git a/src/Staking.sol b/src/Staking.sol index 36ee273..a679d9e 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -178,16 +178,17 @@ contract GhostStaking is IStaking, GhostAccessControlled { function phantomRefund( address receiver, uint256 amount, - uint256 registryIndex, uint256 phantomExpiry ) external override { if (gatekeeper != msg.sender) revert NotGatekeeper(); + // forge-lint: disable-next-line(unsafe-typecast) + uint96 routerIndex = uint96(dexRouterIndex); // forge-lint: disable-next-line(unsafe-typecast) uint48 phantomRelease = uint48(block.timestamp + phantomExpiry); address treasury = ISTNK(stnk).treasury(); - ITreasury(treasury).phantomRefund(receiver, phantomRelease, amount, registryIndex); + ITreasury(treasury).phantomRefund(receiver, phantomRelease, routerIndex, amount); } function recall(address receiver, uint256 amount) external override { diff --git a/src/Treasury.sol b/src/Treasury.sol index e225d97..6c51580 100644 --- a/src/Treasury.sol +++ b/src/Treasury.sol @@ -88,19 +88,23 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { function phantomRefund( address receiver, uint48 phantomRelease, - uint256 gasSpent, - uint256 registryIndex + uint96 dexRouterIndex, + uint256 gasSpent ) external override { if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); - address reserveToken = _registry[STATUS.RESERVETOKEN].at(registryIndex); - if (!_permissions[STATUS.RESERVETOKEN][reserveToken]) revert NotApproved(); + address dexRouter = _registry[STATUS.DEXROUTER].at(dexRouterIndex); + if (!_permissions[STATUS.DEXROUTER][dexRouter] || dexRouter == address(0)) { + revert NotApproved(); + } - uint256 totalSupply = IERC20(FTSO).totalSupply(); - uint256 gasValue = tokenValue(reserveToken, gasSpent); - gasValue = FullMath.mulDiv(gasValue, totalSupply, totalReserves); + address wethAddress = IUniswapV2Router01(dexRouter).WETH(); + address factory = IUniswapV2Router01(dexRouter).factory(); + address pairAddress = IUniswapV2Factory(factory).getPair(FTSO, wethAddress); + uint256 gasValue = (capturedPriceSnapshots[pairAddress] * gasSpent) >> 112; uint256 currentExcessReserves = excessReserves(); + if (gasValue > 0 && gasValue <= currentExcessReserves) { IBondDepository(bondDepository).addNote(receiver, gasValue, phantomRelease); } else { diff --git a/src/interfaces/IStaking.sol b/src/interfaces/IStaking.sol index acb1171..0019a85 100644 --- a/src/interfaces/IStaking.sol +++ b/src/interfaces/IStaking.sol @@ -71,7 +71,7 @@ interface IStaking { function ghost(bytes32 receiver, uint256 amount) external; function recall(address receiver, uint256 amount) external; function recall(uint256 amount, uint256 registryIndex) external returns (address, uint256); - function phantomRefund(address receiver, uint256 amount, uint256 registryIndex, uint256 phantomExpiry) external; + function phantomRefund(address receiver, uint256 amount, uint256 phantomExpiry) external; function rebase() external returns (uint256); function index() external view returns (uint256); diff --git a/src/interfaces/ITreasury.sol b/src/interfaces/ITreasury.sol index 0375b5b..8468e5f 100644 --- a/src/interfaces/ITreasury.sol +++ b/src/interfaces/ITreasury.sol @@ -40,7 +40,7 @@ interface ITreasury { 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 phantomRefund(address receiver, uint48 phantomExpiry, uint96 dexRouterIndex, uint256 gasSpent) 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; diff --git a/test/gatekeeper/GatekeeperRecall.t.sol b/test/gatekeeper/GatekeeperRecall.t.sol index d725bc6..87d949c 100644 --- a/test/gatekeeper/GatekeeperRecall.t.sol +++ b/test/gatekeeper/GatekeeperRecall.t.sol @@ -13,6 +13,12 @@ import {WETH9} from "../../src/mocks/WETH9.sol"; import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol"; import {Gatekeeper} from "../../src/Gatekeeper.sol"; import {ITreasury} from "../../src/interfaces/ITreasury.sol"; +import {IGatekeeper} from "../../src/interfaces/IGatekeeper.sol"; + +import {UniswapV2Router} from "@uniswap-v2-contracts/UniswapV2Router.sol"; +import {UniswapV2Factory} from "@uniswap-v2-contracts/UniswapV2Factory.sol"; + +import {IUniswapV2Pair} from "@uniswap-v2-contracts/interfaces/IUniswapV2Pair.sol"; contract GatekeeperRecallTest is Test { uint256 public constant TOTAL_INITIAL_SUPPLY = 5000000000000000; @@ -50,9 +56,14 @@ contract GatekeeperRecallTest is Test { GhostBondDepository bondDepository; WETH9 reserve; + UniswapV2Factory factory; + UniswapV2Router router; + function setUp() public { vm.startPrank(INITIALIZER, INITIALIZER); reserve = new WETH9(); + factory = new UniswapV2Factory(address(0)); + router = new UniswapV2Router(address(factory), address(reserve)); authority = new GhostAuthority( GOVERNOR, GUARDIAN, @@ -91,24 +102,77 @@ contract GatekeeperRecallTest is Test { 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)); + treasury.enable(ITreasury.STATUS.DEXROUTER, address(router), address(0)); treasury.setBondDepository(address(bondDepository)); vm.stopPrank(); - vm.deal(ALICE, INITIAL_MINT); + vm.deal(ALICE, INITIAL_MINT * 2); + + uint256 tokenValue = treasury.tokenValue(address(reserve), INITIAL_MINT / 2); vm.startPrank(ALICE); reserve.deposit{value: INITIAL_MINT}(); reserve.approve(address(treasury), type(uint256).max); - treasury.deposit(address(reserve), INITIAL_MINT, treasury.tokenValue(address(reserve), INITIAL_MINT) / 2); + treasury.deposit(address(reserve), INITIAL_MINT, tokenValue); assertEq(ftso.totalSupply(), treasury.baseSupply()); vm.stopPrank(); + + uint256 ftsoAmount = ftso.balanceOf(ALICE) / 2; + uint256 wethAmount = ftsoAmount / 2000; + + vm.startPrank(ALICE); + ftso.approve(address(router), ftso.balanceOf(ALICE)); + router.addLiquidityETH{value: INITIAL_MINT}( + address(ftso), + ftsoAmount, + ftsoAmount, + wethAmount, + ALICE, + 1234 + ); + vm.stopPrank(); + + address pair = factory.getPair(address(reserve), address(ftso)); + vm.prank(GOVERNOR); + treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, pair, address(calculator)); + + (,, uint48 endBefore,) = staking.epoch(); + vm.warp(endBefore); + vm.roll(block.number + 1); + + IUniswapV2Pair(pair).sync(); + staking.rebase(); + assertEq(treasury.cummulativePriceSnapshots(pair), IUniswapV2Pair(pair).price0CumulativeLast()); + } + + function test_phantomRefundBenchmark() public { + vm.startPrank(INITIALIZER); + Gatekeeper newGatekeeper = new Gatekeeper( + IGatekeeper(staking.gatekeeper()).storageHistory(), + address(staking) + ); + vm.stopPrank(); + + vm.prank(GOVERNOR); + staking.updateGatekeeperAddress(address(newGatekeeper)); + + // needed only for GAS_EXECUTION_BUFFER benchmarking with --gas-report flag + vm.prank(address(newGatekeeper)); + staking.phantomRefund(ALICE, 420, 432000); + + vm.prank(address(newGatekeeper)); + staking.phantomRefund(ALICE, 1337, 432000); + + vm.prank(address(newGatekeeper)); + staking.phantomRefund(ALICE, 69, 432000); } function test_gasPriceCorrectDuringRecall() public { - vm.startPrank(INITIALIZER); - Gatekeeper gatekeeper = Gatekeeper(payable(staking.gatekeeper())); + address gatekeeperAddress = staking.gatekeeper(); + Gatekeeper gatekeeper = Gatekeeper(payable(gatekeeperAddress)); + + vm.prank(INITIALIZER); gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6); - vm.stopPrank(); vm.startPrank(ALICE); uint256 aliceBalance = ftso.balanceOf(ALICE); @@ -169,10 +233,11 @@ contract GatekeeperRecallTest is Test { } function test_recallChainWorks() public { - vm.startPrank(INITIALIZER); - Gatekeeper gatekeeper = Gatekeeper(payable(staking.gatekeeper())); + address gatekeeperAddress = staking.gatekeeper(); + Gatekeeper gatekeeper = Gatekeeper(payable(gatekeeperAddress)); + + vm.prank(INITIALIZER); gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6); - vm.stopPrank(); vm.startPrank(ALICE); uint256 aliceBalance = ftso.balanceOf(ALICE); diff --git a/test/treasury/TreasuryCumulativePrice.t.soll b/test/treasury/TreasuryCumulativePrice.t.soll new file mode 100644 index 0000000..065c70c --- /dev/null +++ b/test/treasury/TreasuryCumulativePrice.t.soll @@ -0,0 +1,88 @@ +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Fatso} from "../../src/FatsoERC20.sol"; +import {GhostAuthority} from "../../src/GhostAuthority.sol"; +import {GhostTreasury} from "../../src/Treasury.sol"; +import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol"; +import {WETH9} from "../../src/mocks/WETH9.sol"; + +import {ITreasury} from "../../src/interfaces/ITreasury.sol"; +import {UniswapV2Pair} from "@uniswap-v2-contracts/UniswapV2Pair.sol"; +import {UniswapV2Factory} from "@uniswap-v2-contracts/UniswapV2Factory.sol"; +import {UniswapV2Router} from "@uniswap-v2-contracts/UniswapV2Router.sol"; +import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; + +// TODO: remove fucking old uniswap dependencies + +contract GhostTreasuryCumulativePriceTest is Test { + address public constant OWNER = 0x0000000000000000000000000000000000000001; + address public constant GOVERNOR = 0x0000000000000000000000000000000000000002; + address public constant GUARDIAN = 0x0000000000000000000000000000000000000003; + address public constant ALICE = 0x0000000000000000000000000000000000000004; + + uint256 public constant AMOUNT = 2000 ether; + + Fatso ftso; + WETH9 weth; + UniswapV2Factory factory; + UniswapV2Router router; + GhostTreasury treasury; + GhostAuthority authority; + GhostBondingCalculator calculator; + + function setUp() public { + vm.startPrank(OWNER); + authority = new GhostAuthority(GOVERNOR, GUARDIAN, OWNER, OWNER); + weth = new WETH9(); + ftso = new Fatso(address(authority), "Fatso", "FTSO"); + treasury = new GhostTreasury(address(ftso), address(authority)); + calculator = new GhostBondingCalculator(address(ftso), 2000, 1); + factory = new UniswapV2Factory(OWNER); + router = new UniswapV2Router(address(factory), address(weth)); + vm.stopPrank(); + + vm.startPrank(GOVERNOR); + authority.pushVault(address(treasury)); + treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0)); + treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(weth), address(calculator)); + treasury.enable(ITreasury.STATUS.DEXROUTER, address(router), address(0)); + vm.stopPrank(); + + vm.deal(ALICE, 4 ether); + vm.startPrank(ALICE); + weth.deposit{value: 2 ether}(); + weth.approve(address(treasury), type(uint256).max); + treasury.deposit(address(weth), 1 ether, 0); + vm.stopPrank(); + + vm.startPrank(ALICE); + ftso.approve(address(router), type(uint256).max); + (,, uint256 liquidity) = router.addLiquidityETH{value: 1 ether}( + address(ftso), AMOUNT, AMOUNT, 1 ether, ALICE, 69 + ); + vm.stopPrank(); + + address lpToken = factory.getPair(address(ftso), address(weth)); + require(lpToken != address(0), "pair not created"); + + vm.startPrank(GOVERNOR); + treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, lpToken, address(calculator)); + vm.stopPrank(); + + vm.startPrank(ALICE); + IERC20(lpToken).approve(address(treasury), type(uint256).max); + treasury.deposit(lpToken, liquidity, treasury.tokenValue(lpToken, liquidity)); + vm.stopPrank(); + } + + function test_initialTreasuryPricingEmpty() public view { + assertEq(treasury.blockTimestampLast(), 0); + assertEq(treasury.cummulativePriceLast(), 0); + assertEq(treasury.reservesValuationPrice(), 0); + } + + // TODO: after creation price should be correct but Treasury does not know anything + // TODO: change price as hard as possible, and then check the treasury cummulative +}