use TWAP price from DEX for phantom refunds

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-10-27 17:06:47 +03:00
parent ec29d7e18d
commit f79b32554d
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
8 changed files with 182 additions and 22 deletions

@ -1 +1 @@
Subproject commit 5f4365a3a872a0df3f6026402bba7ac71baa9b33 Subproject commit b131a064dbcfe6b2750d0f4c3d576b23c561fdb9

View File

@ -26,7 +26,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard {
uint256 public constant FULL_GAS_RESTORATION_TIME = 3600; // 1 hour uint256 public constant FULL_GAS_RESTORATION_TIME = 3600; // 1 hour
uint256 public constant MAX_ALLOWED_GAS_PRICE = 3e9; // 3 gwei uint256 public constant MAX_ALLOWED_GAS_PRICE = 3e9; // 3 gwei
uint256 public constant PHANTOM_EXPIRY = 432000; // seconds, 5 days 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; uint256 public constant REGISTRY_INDEX = 0;
address public override staking; address public override staking;
@ -117,6 +117,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard {
} }
function ghost(bytes32 receiver, uint256 amount) external override { function ghost(bytes32 receiver, uint256 amount) external override {
// TODO: if (!_initialized) revert SomeCoolError();
if (msg.sender != staking) revert NotStaking(); if (msg.sender != staking) revert NotStaking();
if (amount < EXISTENTIAL_DEPOSIT) revert NonExistentAmount(); if (amount < EXISTENTIAL_DEPOSIT) revert NonExistentAmount();
@ -129,6 +130,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard {
uint256 rx, uint256 rx,
uint256 s uint256 s
) external nonReentrant { ) external nonReentrant {
// TODO: if (!_initialized) revert SomeCoolError();
uint256 gasStart = gasleft(); uint256 gasStart = gasleft();
uint256 px = _extractPublicKey(call); uint256 px = _extractPublicKey(call);
@ -151,7 +153,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard {
_lastVerificationTime = block.timestamp; _lastVerificationTime = block.timestamp;
uint256 gasSpent = (gasStart - gasleft() + GAS_EXECUTION_BUFFER) * currentGasPrice; 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 { catch {
emit VoluntaryVerification(msg.sender, gasSpent); emit VoluntaryVerification(msg.sender, gasSpent);
} }

View File

@ -178,16 +178,17 @@ contract GhostStaking is IStaking, GhostAccessControlled {
function phantomRefund( function phantomRefund(
address receiver, address receiver,
uint256 amount, uint256 amount,
uint256 registryIndex,
uint256 phantomExpiry uint256 phantomExpiry
) external override { ) external override {
if (gatekeeper != msg.sender) revert NotGatekeeper(); if (gatekeeper != msg.sender) revert NotGatekeeper();
// forge-lint: disable-next-line(unsafe-typecast)
uint96 routerIndex = uint96(dexRouterIndex);
// forge-lint: disable-next-line(unsafe-typecast) // forge-lint: disable-next-line(unsafe-typecast)
uint48 phantomRelease = uint48(block.timestamp + phantomExpiry); uint48 phantomRelease = uint48(block.timestamp + phantomExpiry);
address treasury = ISTNK(stnk).treasury(); 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 { function recall(address receiver, uint256 amount) external override {

View File

@ -88,19 +88,23 @@ contract GhostTreasury is GhostAccessControlled, ITreasury {
function phantomRefund( function phantomRefund(
address receiver, address receiver,
uint48 phantomRelease, uint48 phantomRelease,
uint256 gasSpent, uint96 dexRouterIndex,
uint256 registryIndex uint256 gasSpent
) external override { ) external override {
if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved();
address reserveToken = _registry[STATUS.RESERVETOKEN].at(registryIndex); address dexRouter = _registry[STATUS.DEXROUTER].at(dexRouterIndex);
if (!_permissions[STATUS.RESERVETOKEN][reserveToken]) revert NotApproved(); if (!_permissions[STATUS.DEXROUTER][dexRouter] || dexRouter == address(0)) {
revert NotApproved();
}
uint256 totalSupply = IERC20(FTSO).totalSupply(); address wethAddress = IUniswapV2Router01(dexRouter).WETH();
uint256 gasValue = tokenValue(reserveToken, gasSpent); address factory = IUniswapV2Router01(dexRouter).factory();
gasValue = FullMath.mulDiv(gasValue, totalSupply, totalReserves); address pairAddress = IUniswapV2Factory(factory).getPair(FTSO, wethAddress);
uint256 gasValue = (capturedPriceSnapshots[pairAddress] * gasSpent) >> 112;
uint256 currentExcessReserves = excessReserves(); uint256 currentExcessReserves = excessReserves();
if (gasValue > 0 && gasValue <= currentExcessReserves) { if (gasValue > 0 && gasValue <= currentExcessReserves) {
IBondDepository(bondDepository).addNote(receiver, gasValue, phantomRelease); IBondDepository(bondDepository).addNote(receiver, gasValue, phantomRelease);
} else { } else {

View File

@ -71,7 +71,7 @@ interface IStaking {
function ghost(bytes32 receiver, uint256 amount) external; function ghost(bytes32 receiver, uint256 amount) external;
function recall(address receiver, uint256 amount) external; function recall(address receiver, uint256 amount) external;
function recall(uint256 amount, uint256 registryIndex) external returns (address, uint256); 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 rebase() external returns (uint256);
function index() external view returns (uint256); function index() external view returns (uint256);

View File

@ -40,7 +40,7 @@ interface ITreasury {
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 permissioned(STATUS status, address token) external view returns (bool);
function registeredAt(STATUS status, uint256 index) external view returns (address); 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 buyBack(address receiver, uint256 amount, uint256 index) external returns (address, uint256);
function withdraw(address token, uint256 amount) external; function withdraw(address token, uint256 amount) external;
function mint(address _recipient, uint256 _amount) external; function mint(address _recipient, uint256 _amount) external;

View File

@ -13,6 +13,12 @@ import {WETH9} from "../../src/mocks/WETH9.sol";
import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol"; import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol";
import {Gatekeeper} from "../../src/Gatekeeper.sol"; import {Gatekeeper} from "../../src/Gatekeeper.sol";
import {ITreasury} from "../../src/interfaces/ITreasury.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 { contract GatekeeperRecallTest is Test {
uint256 public constant TOTAL_INITIAL_SUPPLY = 5000000000000000; uint256 public constant TOTAL_INITIAL_SUPPLY = 5000000000000000;
@ -50,9 +56,14 @@ contract GatekeeperRecallTest is Test {
GhostBondDepository bondDepository; GhostBondDepository bondDepository;
WETH9 reserve; WETH9 reserve;
UniswapV2Factory factory;
UniswapV2Router router;
function setUp() public { function setUp() public {
vm.startPrank(INITIALIZER, INITIALIZER); vm.startPrank(INITIALIZER, INITIALIZER);
reserve = new WETH9(); reserve = new WETH9();
factory = new UniswapV2Factory(address(0));
router = new UniswapV2Router(address(factory), address(reserve));
authority = new GhostAuthority( authority = new GhostAuthority(
GOVERNOR, GOVERNOR,
GUARDIAN, GUARDIAN,
@ -91,24 +102,77 @@ contract GatekeeperRecallTest is Test {
treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0)); treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0));
treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator)); treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
treasury.enable(ITreasury.STATUS.STAKING, address(staking), address(0)); treasury.enable(ITreasury.STATUS.STAKING, address(staking), address(0));
treasury.enable(ITreasury.STATUS.DEXROUTER, address(router), address(0));
treasury.setBondDepository(address(bondDepository)); treasury.setBondDepository(address(bondDepository));
vm.stopPrank(); 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); vm.startPrank(ALICE);
reserve.deposit{value: INITIAL_MINT}(); reserve.deposit{value: INITIAL_MINT}();
reserve.approve(address(treasury), type(uint256).max); 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()); assertEq(ftso.totalSupply(), treasury.baseSupply());
vm.stopPrank(); 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 { function test_gasPriceCorrectDuringRecall() public {
vm.startPrank(INITIALIZER); address gatekeeperAddress = staking.gatekeeper();
Gatekeeper gatekeeper = Gatekeeper(payable(staking.gatekeeper())); Gatekeeper gatekeeper = Gatekeeper(payable(gatekeeperAddress));
vm.prank(INITIALIZER);
gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6); gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6);
vm.stopPrank();
vm.startPrank(ALICE); vm.startPrank(ALICE);
uint256 aliceBalance = ftso.balanceOf(ALICE); uint256 aliceBalance = ftso.balanceOf(ALICE);
@ -169,10 +233,11 @@ contract GatekeeperRecallTest is Test {
} }
function test_recallChainWorks() public { function test_recallChainWorks() public {
vm.startPrank(INITIALIZER); address gatekeeperAddress = staking.gatekeeper();
Gatekeeper gatekeeper = Gatekeeper(payable(staking.gatekeeper())); Gatekeeper gatekeeper = Gatekeeper(payable(gatekeeperAddress));
vm.prank(INITIALIZER);
gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6); gatekeeper.updatePublicKeyMetadata(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6);
vm.stopPrank();
vm.startPrank(ALICE); vm.startPrank(ALICE);
uint256 aliceBalance = ftso.balanceOf(ALICE); uint256 aliceBalance = ftso.balanceOf(ALICE);

View File

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