diff --git a/src/BondDepository.sol b/src/BondDepository.sol index 1aa9de3..66ff7bc 100644 --- a/src/BondDepository.sol +++ b/src/BondDepository.sol @@ -41,6 +41,11 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { WETH = wrapped; } + function addNote(address user, uint256 payout, uint48 expiry) external returns (uint256) { + if (msg.sender != TREASURY) revert NotTreasury(); + return _addNote(user, payout, expiry, type(uint48).max); + } + function deposit( uint256 id, uint256 amount, @@ -77,7 +82,7 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { emit Bond(id, amount, price); - index = addNote(user, payout, uint48(expiry), uint48(id)); // forge-lint: disable-line(unsafe-typecast) + index = _addNote(user, payout, uint48(expiry), uint48(id)); // forge-lint: disable-line(unsafe-typecast) IWETH9(WETH).deposit{value: msg.value}(); IERC20(WETH).safeTransfer(TREASURY, msg.value); diff --git a/src/Gatekeeper.sol b/src/Gatekeeper.sol index 7a035d2..3b7ed45 100644 --- a/src/Gatekeeper.sol +++ b/src/Gatekeeper.sol @@ -25,6 +25,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { uint256 public constant EXISTENTIAL_DEPOSIT = 500 * 1e12; // 0.005 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 REGISTRY_INDEX = 0; @@ -160,7 +161,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) {} + try IStaking(staking).phantomRefund(msg.sender, gasSpent, REGISTRY_INDEX, PHANTOM_EXPIRY) {} catch { emit VoluntaryVerification(msg.sender, gasSpent); } diff --git a/src/Staking.sol b/src/Staking.sol index 83c9249..ff69959 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -171,14 +171,19 @@ contract GhostStaking is IStaking, GhostAccessControlled { IGatekeeper(gatekeeper).ghost(receiver, amount); } - function phantomRefund(address receiver, uint256 amount, uint256 registryIndex) external override { + 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) + uint48 phantomRelease = uint48(block.timestamp + phantomExpiry); address treasury = ISTNK(stnk).treasury(); - uint256 gasFtsoAmount = ITreasury(treasury).phantomRefund(amount, registryIndex); - uint256 gasGhstAmount = IGHST(ghst).balanceTo(gasFtsoAmount); - - IGHST(ghst).mint(receiver, gasGhstAmount); + ITreasury(treasury).phantomRefund(receiver, phantomRelease, amount, registryIndex); } function recall(address receiver, uint256 amount) external override { diff --git a/src/Treasury.sol b/src/Treasury.sol index 26edc83..f44197a 100644 --- a/src/Treasury.sol +++ b/src/Treasury.sol @@ -18,6 +18,7 @@ import {FullMath} from "./libraries/FullMath.sol"; import {IFTSO} from "./interfaces/IFTSO.sol"; import {IBondingCalculator} from "./interfaces/IBondingCalculator.sol"; import {ITreasury} from "./interfaces/ITreasury.sol"; +import {IBondDepository} from "./interfaces/IBondDepository.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; contract GhostTreasury is GhostAccessControlled, ITreasury { @@ -25,6 +26,7 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { using EnumerableSet for EnumerableSet.AddressSet; address public immutable FTSO; + address public bondDepository; uint256 public totalReserves; mapping(STATUS => EnumerableSet.AddressSet) private _registry; @@ -77,20 +79,22 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { } function phantomRefund( + address receiver, + uint48 phantomRelease, uint256 gasSpent, - uint256 index + uint256 registryIndex ) external override returns (uint256 gasValue) { if (!_permissions[STATUS.STAKING][msg.sender]) revert NotApproved(); - address reserveToken = _registry[STATUS.RESERVETOKEN].at(index); + address reserveToken = _registry[STATUS.RESERVETOKEN].at(registryIndex); if (!_permissions[STATUS.RESERVETOKEN][reserveToken]) revert NotApproved(); uint256 totalSupply = IERC20(FTSO).totalSupply(); gasValue = tokenValue(reserveToken, gasSpent); gasValue = FullMath.mulDiv(gasValue, totalSupply, totalReserves); - if (gasValue <= excessReserves()) { - IFTSO(FTSO).mint(msg.sender, gasValue); + if (gasValue > 0 && gasValue <= excessReserves()) { + IBondDepository(bondDepository).addNote(receiver, gasValue, phantomRelease); } } @@ -178,6 +182,15 @@ contract GhostTreasury is GhostAccessControlled, ITreasury { emit Permissioned(toDisable, status, false); } + function setBondDepository(address newBondDepository) external { + if ( + msg.sender != authority.governor() && + msg.sender != authority.guardian() + ) revert NotApproved(); + bondDepository = newBondDepository; + emit BondDepositoryUpdated(newBondDepository); + } + function forfeitReserves( address router, uint256 liquidity, diff --git a/src/interfaces/IBondDepository.sol b/src/interfaces/IBondDepository.sol index 585eb29..9613810 100644 --- a/src/interfaces/IBondDepository.sol +++ b/src/interfaces/IBondDepository.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; interface IBondDepository { + error NotTreasury(); error DepositoryConcluded(uint48 concludedTime); error DepositoryMaxPrice(uint256 price, uint256 maxPrice); error DepositoryMaxSize(uint256 payout, uint256 maxPayout); @@ -75,6 +76,7 @@ interface IBondDepository { ) external returns (uint256 id_); function close(uint256 _id) external; + function addNote(address user, uint256 payout, uint48 expiry) external returns (uint256); function isLive(uint256 _bid) external view returns (bool); function liveMarkets() external view returns (uint256[] memory); function liveMarketsFor(address _quoteToken) external view returns (uint256[] memory); diff --git a/src/interfaces/IStaking.sol b/src/interfaces/IStaking.sol index 61a33e8..27e5b42 100644 --- a/src/interfaces/IStaking.sol +++ b/src/interfaces/IStaking.sol @@ -70,7 +70,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) external; + function phantomRefund(address receiver, uint256 amount, uint256 registryIndex, 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 511be89..dcc87d1 100644 --- a/src/interfaces/ITreasury.sol +++ b/src/interfaces/ITreasury.sol @@ -21,6 +21,7 @@ interface ITreasury { event Minted(address indexed caller, address indexed recipient, uint256 amount); event Permissioned(address addr, STATUS indexed status, bool result); event Withdrawal(address indexed token, uint256 indexed amount, uint256 value); + event BondDepositoryUpdated(address bondDepository); function deposit( address _token, @@ -29,7 +30,7 @@ interface ITreasury { ) external returns (uint256); function registered(STATUS status, address token) external view returns(bool); - function phantomRefund(uint256 gasSpent, uint256 amount) external returns (uint256); + function phantomRefund(address receiver, uint48 phantomExpiry, uint256 gasSpent, uint256 amount) external returns (uint256); 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/src/types/NoteKeeper.sol b/src/types/NoteKeeper.sol index 4697fe3..e4803b1 100644 --- a/src/types/NoteKeeper.sol +++ b/src/types/NoteKeeper.sol @@ -27,7 +27,7 @@ abstract contract NoteKeeper is INoteKeeper { _TREASURY = ITreasury(_treasuryAddress); } - function addNote( + function _addNote( address user, uint256 payout, uint48 expiry, diff --git a/test/gatekeeper/GatekeeperRecall.t.sol b/test/gatekeeper/GatekeeperRecall.t.sol index e434b98..0095841 100644 --- a/test/gatekeeper/GatekeeperRecall.t.sol +++ b/test/gatekeeper/GatekeeperRecall.t.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {Fatso} from "../../src/FatsoERC20.sol"; import {Stinky} from "../../src/StinkyERC20.sol"; import {Ghost} from "../../src/GhstERC20.sol"; +import {GhostBondDepository} from "../../src/BondDepository.sol"; import {GhostAuthority} from "../../src/GhostAuthority.sol"; import {GhostTreasury} from "../../src/Treasury.sol"; import {GhostStaking} from "../../src/Staking.sol"; @@ -46,6 +47,7 @@ contract GatekeeperRecallTest is Test { GhostTreasury treasury; GhostAuthority authority; GhostBondingCalculator calculator; + GhostBondDepository bondDepository; WETH9 reserve; function setUp() public { @@ -71,15 +73,25 @@ contract GatekeeperRecallTest is Test { ); treasury = new GhostTreasury(address(ftso), address(authority)); calculator = new GhostBondingCalculator(address(ftso), 6000, 3); + bondDepository = new GhostBondDepository( + address(authority), + address(ftso), + address(ghst), + address(staking), + address(treasury), + address(reserve) + ); stnk.initialize(address(staking), address(treasury), address(ghst)); ghst.initialize(address(staking)); vm.stopPrank(); vm.startPrank(GOVERNOR); authority.pushVault(address(treasury)); + treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(bondDepository), 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)); + treasury.setBondDepository(address(bondDepository)); vm.stopPrank(); vm.deal(ALICE, INITIAL_MINT); @@ -109,11 +121,11 @@ contract GatekeeperRecallTest is Test { vm.warp(block.timestamp + gatekeeper.FULL_GAS_RESTORATION_TIME() * 69); - uint256 bobBalanceBeforeFirst = ghst.balanceOf(BOB); uint256 priceBeforeFirst = gatekeeper.getCurrentGasPrice(); assertEq(priceBeforeFirst, gatekeeper.MAX_ALLOWED_GAS_PRICE()); assertEq(ghst.balanceOf(BOB), 0); + vm.txGasPrice(2 gwei); vm.prank(BOB, BOB); gatekeeper.verify( hex"bf06188a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000017508f1956a800000000000000000000000000000000000000000020000000000007a6980000000", @@ -121,10 +133,12 @@ contract GatekeeperRecallTest is Test { 0x8c3fc03b5cb738777ceec396ee8647f782d6670992f8f973de2dfc83379978d5 ); - uint256 bobBalanceAfterFirst = ghst.balanceOf(BOB); - assertGt(bobBalanceAfterFirst, bobBalanceBeforeFirst); + (uint256 pendingPayout, bool matured) = bondDepository.pendingFor(BOB, 0); + assertGt(pendingPayout, 0); // 19273751457699280 + assertFalse(matured); assertEq(gatekeeper.getCurrentGasPrice(), 0); + vm.txGasPrice(2 gwei); vm.prank(BOB, BOB); gatekeeper.verify( hex"3137142600000000000000000000000000000000000000000000000000000000000000120000000000007a69010101010101010101010101010101010101010100000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000042b7ce50000000000000000000000000000000000000000000000000000000000", @@ -132,13 +146,12 @@ contract GatekeeperRecallTest is Test { 0xdbeed3eb94a829710147595c1b10f2ac6ec761c9721d8cc4711842c212ac517e ); - assertEq(ghst.balanceOf(BOB), bobBalanceAfterFirst); assertEq(gatekeeper.getCurrentGasPrice(), 0); - vm.warp(block.timestamp + gatekeeper.FULL_GAS_RESTORATION_TIME() / 2); uint256 gasPriceRestored = gatekeeper.getCurrentGasPrice(); assertEq(gasPriceRestored, gatekeeper.MAX_ALLOWED_GAS_PRICE() / 2); + vm.txGasPrice(2 gwei); vm.prank(BOB, BOB); gatekeeper.verify( hex"31371426000000000000000000000000000000000000000000000000000000000000000d0000000000007a6901010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064e4e33ef800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000000000000000000000000000000000000", @@ -146,8 +159,13 @@ contract GatekeeperRecallTest is Test { 0xc2da93948f869c5102992bd1cbd70ef5f821ff522305c69d113faf2e89570ce7 ); - assertGt(ghst.balanceOf(BOB), bobBalanceAfterFirst); + (pendingPayout, matured) = bondDepository.pendingFor(BOB, 1); + assertGt(pendingPayout, 69); + assertFalse(matured); assertEq(gatekeeper.getCurrentGasPrice(), 0); + + uint256[] memory indexes = bondDepository.indexesFor(BOB); + assertEq(indexes.length, 2); } function test_recallChainWorks() public { @@ -168,7 +186,6 @@ contract GatekeeperRecallTest is Test { uint256 actualAmount = 105000000000000000; address actualReceiver = address(0x0000000000000000000000000000000000000002); - uint256 bobGhstBefore = ghst.balanceOf(BOB); uint256 receiverEthBefore = actualReceiver.balance; uint256 receiverGhstBefore = ghst.balanceOf(actualReceiver); uint256 totalReservesBefore = treasury.totalReserves(); @@ -185,14 +202,15 @@ contract GatekeeperRecallTest is Test { vm.stopPrank(); { - uint256 bobGhstAfter = ghst.balanceOf(BOB); + (uint256 bobPhantomClaimAfter, bool maturedAfter) = bondDepository.pendingFor(BOB, 0); uint256 receiverGhstAfter = ghst.balanceOf(actualReceiver); + assertFalse(maturedAfter); assertTrue( - ghst.balanceFrom(bobGhstAfter - bobGhstBefore) * totalReservesBefore / totalSupplyBefore + ghst.balanceFrom(bobPhantomClaimAfter) * totalReservesBefore / totalSupplyBefore > gatekeeper.GAS_EXECUTION_BUFFER() ); - assertTrue(bobGhstAfter > bobGhstBefore); + assertApproxEqAbs(receiverGhstAfter - receiverGhstBefore, actualAmount / 2, 1e18); } @@ -211,7 +229,8 @@ contract GatekeeperRecallTest is Test { uint256 backingRatioBefore = totalReservesBefore * 1e18 / totalSupplyBefore; uint256 backingRatioAfter = treasury.totalReserves() * 1e18 / ftso.totalSupply(); - uint256 gasFtsoMinted = ghst.balanceFrom(ghst.balanceOf(BOB) - bobGhstBefore); + (uint256 bobPhantomClaim,) = bondDepository.pendingFor(BOB, 0); + uint256 gasFtsoMinted = ghst.balanceFrom(bobPhantomClaim); uint256 expectedDelta = (backingRatioBefore * gasFtsoMinted) / totalSupplyBefore; assertApproxEqAbs(backingRatioAfter, backingRatioBefore - expectedDelta, 1000); @@ -220,5 +239,22 @@ contract GatekeeperRecallTest is Test { treasury.auditReserves(); assertApproxEqAbs(treasury.totalReserves() * 1e18 / ftso.totalSupply(), backingRatioBefore - expectedDelta, 2000); } + + { + (uint256 phantomClaim, bool claimMatured) = bondDepository.pendingFor(BOB, 0); + assertFalse(claimMatured); + + vm.expectRevert(); + bondDepository.redeemAll(BOB, true); + + vm.warp(block.timestamp + gatekeeper.PHANTOM_EXPIRY() + 69); + (phantomClaim, claimMatured) = bondDepository.pendingFor(BOB, 0); + assertTrue(claimMatured); + + vm.prank(BOB); + uint256 actualPayout = bondDepository.redeemAll(BOB, true); + assertEq(phantomClaim, actualPayout); + assertEq(actualPayout, ghst.balanceOf(BOB)); + } } }