From 40c203a6b03dd329560b1f0964956babc2604dfe Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Thu, 24 Sep 2026 13:37:14 +0300 Subject: [PATCH] enforce lower bound on bond price Signed-off-by: Uncle Fatso --- src/BondDepository.sol | 17 ++++++++----- src/interfaces/IBondDepository.sol | 5 ++-- test/bonding/BondDepositorty.t.sol | 41 ++++++++++++++++++++++-------- test/gatekeeper/Gatekeeper.t.sol | 11 ++++---- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/BondDepository.sol b/src/BondDepository.sol index cea68db..fac0fa9 100644 --- a/src/BondDepository.sol +++ b/src/BondDepository.sol @@ -150,7 +150,7 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { } function create( - uint256[3] calldata _market, + uint256[4] calldata _market, uint256[2] calldata _terms, address _quoteToken, uint32[2] calldata _intervals, @@ -169,19 +169,20 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { // forge-lint: disable-next-line(unsafe-typecast) uint64 maxPayout = uint64(FullMath.mulDiv(targetDebt, _intervals[0], secondsToConclusion)); - uint256 maxDebt = targetDebt + FullMath.mulDiv(targetDebt, _market[2], 1e5); + uint256 maxDebt = targetDebt + FullMath.mulDiv(targetDebt, _market[3], 1e5); uint256 controlVariable = FullMath.mulDiv(_market[1], ITreasury(TREASURY).baseSupply(), targetDebt); id = markets.length; markets.push( Market({ + capacity: _market[0], + purchased: 0, + minimumPrice: _market[2], quoteToken: _quoteToken, capacityInQuote: _booleans[0], - capacity: _market[0], totalDebt: targetDebt, maxPayout: maxPayout, - purchased: 0, sold: 0 }) ); @@ -225,7 +226,9 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { } function marketPrice(uint256 id) public view override returns (uint256) { - return FullMath.mulDiv(currentControlVariable(id), debtRatio(id), 10**metadatas[id].quoteDecimals); + uint256 minimumPrice = markets[id].minimumPrice; + uint256 currentPrice = FullMath.mulDiv(currentControlVariable(id), debtRatio(id), 10**metadatas[id].quoteDecimals); + return currentPrice > minimumPrice ? currentPrice : minimumPrice; } function payoutFor( @@ -311,7 +314,9 @@ contract GhostBondDepository is IBondDepository, NoteKeeper { } function _marketPrice(uint256 _id) internal view returns (uint256) { - return FullMath.mulDiv(terms[_id].controlVariable, _debtRatio(_id), 10**metadatas[_id].quoteDecimals); + uint256 minimumPrice = markets[_id].minimumPrice; + uint256 currentPrice = FullMath.mulDiv(terms[_id].controlVariable, _debtRatio(_id), 10**metadatas[_id].quoteDecimals); + return currentPrice > minimumPrice ? currentPrice : minimumPrice; } function _debtRatio(uint256 id) internal view returns (uint256) { diff --git a/src/interfaces/IBondDepository.sol b/src/interfaces/IBondDepository.sol index fa28a73..9c32229 100644 --- a/src/interfaces/IBondDepository.sol +++ b/src/interfaces/IBondDepository.sol @@ -18,12 +18,13 @@ interface IBondDepository { struct Market { uint256 capacity; + uint256 purchased; + uint256 minimumPrice; address quoteToken; bool capacityInQuote; uint64 totalDebt; uint64 maxPayout; uint64 sold; - uint256 purchased; } struct Term { @@ -65,7 +66,7 @@ interface IBondDepository { ); function create( - uint256[3] memory _market, + uint256[4] memory _market, uint256[2] memory _terms, address _quoteToken, uint32[2] memory _intervals, diff --git a/test/bonding/BondDepositorty.t.sol b/test/bonding/BondDepositorty.t.sol index 79b0bf3..6953101 100644 --- a/test/bonding/BondDepositorty.t.sol +++ b/test/bonding/BondDepositorty.t.sol @@ -121,7 +121,7 @@ contract GhostBondDepositoryTest is Test { vm.prank(POLICY); depository.create( - [CAPACITY, INITIAL_PRICE, BUFFER], + [CAPACITY, INITIAL_PRICE, 0, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) @@ -144,7 +144,7 @@ contract GhostBondDepositoryTest is Test { vm.prank(POLICY); depository.create( - [CAPACITY, INITIAL_PRICE, BUFFER], + [CAPACITY, INITIAL_PRICE, 0, BUFFER], [VESTING, conclusion], address(weth), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) @@ -166,14 +166,14 @@ contract GhostBondDepositoryTest is Test { } function test_shouldSetMaxPayoutToCorrectPercentageOfCapacity() public view { - (, , , , uint256 maxPayout, ,) = depository.markets(0); + (, , , , , , uint256 maxPayout,) = depository.markets(0); assertEq(maxPayout, CAPACITY / 6); } function test_shouldReturnIdsOfAllMarkets() public { vm.prank(POLICY); depository.create( - [CAPACITY, INITIAL_PRICE, BUFFER], + [CAPACITY, INITIAL_PRICE, 0, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) @@ -186,10 +186,31 @@ contract GhostBondDepositoryTest is Test { assertEq(liveMarkets[1], 1); } + function test_marketPriceShouldNeverFallBelowMinimum() public { + uint256 initialPrice = 420 * 1e9; + uint256 minPrice = 69 * 1e9; + + vm.prank(POLICY); + uint256 marketId = depository.create( + [CAPACITY, initialPrice, minPrice, BUFFER], + [VESTING, TIME_TO_CONCLUSION], + address(reserve), + [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], + [false, true] + ); + + uint256 currentPrice = depository.marketPrice(marketId); + assertEq(currentPrice, initialPrice); + + vm.warp(block.timestamp + TIME_TO_CONCLUSION - 420); + uint256 priceAfterDecay = depository.marketPrice(marketId); + assertEq(priceAfterDecay, minPrice); + } + function test_shouldUpdateIdsOfMarkets() public { vm.startPrank(POLICY); depository.create( - [CAPACITY, INITIAL_PRICE, BUFFER], + [CAPACITY, INITIAL_PRICE, 0, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) @@ -221,13 +242,13 @@ contract GhostBondDepositoryTest is Test { } function test_shouldDecayDebt() public { - (, , , uint256 totalDebt, , ,) = depository.markets(0); + (, , , , , uint256 totalDebt, ,) = depository.markets(0); skip(DEPOSIT_INTERVAL); vm.prank(ALICE); depository.deposit(0, 0, INITIAL_PRICE, ALICE); - (, , , uint256 newTotalDebt, , ,) = depository.markets(0); + (, , , , , uint256 newTotalDebt, ,) = depository.markets(0); assertEq(totalDebt > newTotalDebt, true); } @@ -432,7 +453,7 @@ contract GhostBondDepositoryTest is Test { } function test_shouldDecayMaxPayoutInTargetDepositInterval() public { - (, , , , uint64 maxPayout, ,) = depository.markets(0); + (, , , , , , uint64 maxPayout,) = depository.markets(0); uint256 price = depository.marketPrice(0); uint256 amount = maxPayout * price; vm.prank(ALICE); @@ -443,11 +464,11 @@ contract GhostBondDepositoryTest is Test { } function test_shouldCloseMarket() public { - (uint256 cap, , , , , ,) = depository.markets(0); + (uint256 cap, , , , , , ,) = depository.markets(0); assertEq(cap > 0, true); vm.prank(POLICY); depository.close(0); - (uint256 newCap, , , , , ,) = depository.markets(0); + (uint256 newCap, , , , , , ,) = depository.markets(0); assertEq(newCap, 0); } diff --git a/test/gatekeeper/Gatekeeper.t.sol b/test/gatekeeper/Gatekeeper.t.sol index b779e97..bf79c6b 100644 --- a/test/gatekeeper/Gatekeeper.t.sol +++ b/test/gatekeeper/Gatekeeper.t.sol @@ -72,7 +72,7 @@ contract MockGovernance is Test { } function create( - uint256[3] calldata market, + uint256[4] calldata market, uint256[2] calldata terms, address quoteToken, uint32[2] calldata intervals, @@ -83,6 +83,7 @@ contract MockGovernance is Test { assertEq(market[0], 69); assertEq(market[1], 420); assertEq(market[2], 1337); + assertEq(market[3], 0); assertEq(terms[0], 420); assertEq(terms[1], 1337); @@ -468,12 +469,12 @@ contract GatekeeperTest is Test { 0x10121ffe0cc95cf3cbb4e92d72d61feec3e94045185cd7f3c5ce0d01e3a644ea ); - // execute close, exodusSession: 10 + // execute create, exodusSession: 10 vm.prank(ALICE); gatekeeper.verify( - hex"31371426000000000000000000000000000000000000000000000000000000000000000a0000000000007a6901010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000144a0139f41000000000000000000000000000000000000000000000000000000000000004500000000000000000000000000000000000000000000000000000000000001a4000000000000000000000000000000000000000000000000000000000000053900000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000000000000000000000000005390000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000230000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000", - 0xb354049d0012ef7ec3edac6aba453daf4bdc2d28fe1d88138f94459799e2343f, - 0x13a6f8b10007b41fa08c8efee85501fac2554571af99fe587b534fe28fdb82a3 + hex"31371426000000000000000000000000000000000000000000000000000000000000000a0000000000007a6901010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000164206eb0f8000000000000000000000000000000000000000000000000000000000000004500000000000000000000000000000000000000000000000000000000000001a40000000000000000000000000000000000000000000000000000000000000539000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000000000000000000000000005390000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000230000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000", + 0x18c5c0b98ac8221b75b1f7bf3e36fc0d820d5866b23010f4861d8602066f93b0, + 0x45db6a091ddc9d19156c6d2f1d3aca7541733faeebdeed7b8572192d050e4e06 ); // execute enable, exodusSession: 13