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

View File

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

View File

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

View File

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

View File

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

View File

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

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
}