SKIP TO CONTENT

Demo build — mock protocol data. No chain, no ABIs yet.

  • SYNCING: ……

BondsAndBuyback.t.sol

Bond pricing floored at NAV, vesting, and the buyback bid at RFV - 1.5%.

219 lines8.4 KBSolidity
Source of test/BondsAndBuyback.t.sol, 219 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {Base} from "./Base.t.sol";
5import {K401BondDepository} from "../src/K401BondDepository.sol";
6import {K401Buyback} from "../src/K401Buyback.sol";
7import {K401Treasury} from "../src/K401Treasury.sol";
8 
9contract BondsAndBuybackTest is Base {
10 uint256 internal mid;
11 
12 function setUp() public override {
13 super.setUp();
14 _primeOracle();
15 vm.prank(owner);
16 mid = bonds.createMarket(address(usdg), false, 500, 100); // 5% discount, 1% of supply/epoch
17 }
18 
19 /*//////////////////////////////////////////////////////////////
20 BONDS
21 //////////////////////////////////////////////////////////////*/
22 
23 function test_bondMintsAgainstTheTreasuryAndVestsOverFiveDays() public {
24 assertEq(bonds.VESTING_TERM(), 5 days);
25 
26 usdg.mint(bob, 10_000e6);
27 uint256 reserveBefore = treasury.reserveValueWad();
28 uint256 supplyBefore = k401.totalSupply();
29 
30 vm.startPrank(bob);
31 usdg.approve(address(bonds), type(uint256).max);
32 uint256 payout = bonds.bond(mid, 10_000e6);
33 vm.stopPrank();
34 
35 assertGt(payout, 0);
36 assertEq(treasury.reserveValueWad(), reserveBefore + 10_000e18, "principal lands in Bucket A");
37 assertEq(k401.totalSupply(), supplyBefore + payout, "exactly the payout is minted");
38 assertEq(k401.balanceOf(bob), 0, "nothing is liquid until it vests");
39 assertGe(treasury.rfvPerToken(), 1e18);
40 
41 // Half way: half vested.
42 _skip(2.5 days);
43 assertApproxEqRel(bonds.percentVested(bob, mid), 0.5e18, 0.001e18);
44 vm.prank(bob);
45 uint256 first = bonds.redeemBond(mid);
46 assertApproxEqRel(first, payout / 2, 0.001e18);
47 
48 // Fully vested.
49 _skip(2.5 days);
50 assertEq(bonds.percentVested(bob, mid), 1e18);
51 vm.prank(bob);
52 uint256 second = bonds.redeemBond(mid);
53 assertEq(first + second, payout);
54 assertEq(k401.balanceOf(bob), payout);
55 
56 vm.prank(bob);
57 vm.expectRevert(K401BondDepository.NothingToRedeem.selector);
58 bonds.redeemBond(mid);
59 }
60 
61 function test_bondIsNavAccretive() public {
62 uint256 navBefore = treasury.nav();
63 usdg.mint(bob, 15_000e6);
64 vm.startPrank(bob);
65 usdg.approve(address(bonds), type(uint256).max);
66 bonds.bond(mid, 15_000e6);
67 vm.stopPrank();
68 assertGe(treasury.nav(), navBefore, "bonding never dilutes NAV");
69 }
70 
71 function test_capacityIsFixedPerEpochAndResets() public {
72 uint256 cap = bonds.capacityLeft(mid);
73 assertEq(cap, (k401.totalSupply() * 100) / 10_000);
74 
75 usdg.mint(bob, 1_000_000e6);
76 vm.startPrank(bob);
77 usdg.approve(address(bonds), type(uint256).max);
78 // ~1.9 USDG per token, so 15k USDG is ~7.9k of the 10k epoch capacity.
79 bonds.bond(mid, 15_000e6);
80 uint256 left = bonds.capacityLeft(mid);
81 assertLt(left, cap);
82 
83 vm.expectRevert(K401BondDepository.CapacityExceeded.selector);
84 bonds.bond(mid, 500_000e6);
85 vm.stopPrank();
86 
87 _skip(8 hours + 1);
88 assertEq(bonds.capacityLeft(mid), (k401.totalSupply() * 100) / 10_000, "epoch capacity resets");
89 }
90 
91 function test_lpPrincipalIsValuedAtTheFloorNotTheMarket() public {
92 vm.startPrank(owner);
93 treasury.addReserveToken(address(pair));
94 uint256 lpMarket = bonds.createMarket(address(pair), true, 0, 100);
95 vm.stopPrank();
96 
97 uint256 lpAmount = pair.balanceOf(owner) / 100;
98 // Pool is 100k 401K + 200k USDG. Market value of the 401K half is 2.00 each;
99 // the treasury values the LP at the geometric mean, i.e. the 401K side at <= 1.00.
100 uint256 valued = treasury.valueOf(address(pair), lpAmount);
101 uint256 marketValued = ((100_000e18 * oracle.consult()) / 1e18 + 200_000e18) * lpAmount / pair.totalSupply();
102 assertLt(valued, marketValued, "LP is never marked at market price");
103 
104 uint256 atFloor = (100_000e18 + 200_000e18) * lpAmount / pair.totalSupply();
105 assertLe(valued, atFloor, "and never above the 1 USDG floor valuation");
106 assertGt(lpMarket, 0);
107 }
108 
109 function test_marketParametersAreBounded() public {
110 vm.startPrank(owner);
111 vm.expectRevert(K401BondDepository.BadParams.selector);
112 bonds.createMarket(address(usdg), false, 2_001, 100);
113 vm.expectRevert(K401BondDepository.BadParams.selector);
114 bonds.createMarket(address(usdg), false, 500, 501);
115 vm.expectRevert(K401BondDepository.BadParams.selector);
116 bonds.createMarket(address(usdg), false, 500, 0);
117 vm.stopPrank();
118 }
119 
120 function test_pausedMarketRejectsBonds() public {
121 vm.prank(owner);
122 bonds.setMarketActive(mid, false);
123 usdg.mint(bob, 1_000e6);
124 vm.startPrank(bob);
125 usdg.approve(address(bonds), type(uint256).max);
126 vm.expectRevert(K401BondDepository.MarketInactive.selector);
127 bonds.bond(mid, 1_000e6);
128 vm.stopPrank();
129 }
130 
131 /*//////////////////////////////////////////////////////////////
132 BUYBACK
133 //////////////////////////////////////////////////////////////*/
134 
135 function test_buybackBidsAtRfvMinus150Bps() public view {
136 assertEq(buyback.DISCOUNT_BPS(), 150);
137 assertEq(buyback.bidPrice(), (treasury.rfv() * 9_850) / 10_000);
138 assertLt(buyback.bidPrice(), treasury.rfv());
139 assertLt(buyback.bidPrice(), treasury.nav(), "never bids at NAV");
140 }
141 
142 function test_buybackBurnsSupplyAndRaisesRfvPerToken() public {
143 usdg.mint(address(buyback), 100_000e6);
144 _giveSeats(carol, 100);
145 
146 uint256 rfvBefore = treasury.rfvPerToken();
147 uint256 supplyBefore = k401.totalSupply();
148 uint256 quoted = buyback.quote(50e18);
149 
150 vm.startPrank(carol);
151 k401.approve(address(buyback), type(uint256).max);
152 uint256 out = buyback.sell(50e18, 0);
153 vm.stopPrank();
154 
155 assertEq(out, quoted, "settles at the quoted standing bid");
156 assertEq(usdg.balanceOf(carol), out);
157 assertEq(k401.totalSupply(), supplyBefore - 50e18, "the 401K is burned");
158 assertGt(treasury.rfvPerToken(), rfvBefore, "strictly accretive");
159 assertEq(k401.seatBalanceOf(carol), 50);
160 }
161 
162 function test_buybackRevertsWithoutLiquidity() public {
163 _giveSeats(carol, 5);
164 vm.startPrank(carol);
165 k401.approve(address(buyback), type(uint256).max);
166 vm.expectRevert(K401Buyback.InsufficientBidLiquidity.selector);
167 buyback.sell(1e18, 0);
168 vm.stopPrank();
169 }
170 
171 function test_buybackHonoursMinOut() public {
172 usdg.mint(address(buyback), 100_000e6);
173 _giveSeats(carol, 5);
174 vm.startPrank(carol);
175 k401.approve(address(buyback), type(uint256).max);
176 vm.expectRevert(K401Buyback.SlippageExceeded.selector);
177 buyback.sell(1e18, 100e6);
178 vm.stopPrank();
179 }
180 
181 /*//////////////////////////////////////////////////////////////
182 TREASURY
183 //////////////////////////////////////////////////////////////*/
184 
185 function test_treasuryRejectsUnauthorizedDepositAndManage() public {
186 usdg.mint(alice, 1_000e6);
187 vm.startPrank(alice);
188 usdg.approve(address(treasury), type(uint256).max);
189 vm.expectRevert(K401Treasury.NotAuthorized.selector);
190 treasury.deposit(address(usdg), 1_000e6, 0);
191 vm.expectRevert(K401Treasury.NotAuthorized.selector);
192 treasury.manage(address(usdg), 1, alice);
193 vm.stopPrank();
194 }
195 
196 function test_treasuryManageCannotBreakTheFloor() public {
197 vm.startPrank(owner);
198 treasury.addReserveManager(owner);
199 // Reserve is 1.2M against 1M supply: pulling more than 200k breaks the floor.
200 vm.expectRevert(K401Treasury.FloorBreached.selector);
201 treasury.manage(address(usdg), 300_000e6, owner);
202 treasury.manage(address(usdg), 150_000e6, owner);
203 vm.stopPrank();
204 assertGe(treasury.rfvPerToken(), 1e18);
205 }
206 
207 function test_equityIsMarkedByFeedNotBySwapQuote() public {
208 assertEq(treasury.equityValueWad(), 0);
209 nvda.mint(address(treasury), 100e18);
210 assertEq(treasury.equityValueWad(), 100 * 100e18, "100 NVDA at the $100 feed price");
211 
212 nvdaFeed.setAnswer(150e8);
213 assertEq(treasury.equityValueWad(), 100 * 150e18);
214 
215 // Equity lifts NAV but never RFV.
216 assertGt(treasury.nav(), treasury.rfv());
217 }
218}
219 

Click any line number to deep-link to it — the target line highlights on load.