From 95b938a4590834779906fcf4c0c875c01e3f5252 Mon Sep 17 00:00:00 2001 From: "J.0 Satoshi" Date: Thu, 11 Jun 2026 17:56:25 +0300 Subject: [PATCH 1/4] remove unused minTokens parameter from buy() --- src/OriginKeyToken.sol | 3 +- test/OKT.audit.t.sol | 74 ++++++++++++++++++++-------------------- test/OKT.invariant.t.sol | 4 +-- test/OKT.security.t.sol | 60 ++++++++++++++++---------------- test/OKT.t.sol | 74 ++++++++++++++++++++-------------------- 5 files changed, 107 insertions(+), 108 deletions(-) diff --git a/src/OriginKeyToken.sol b/src/OriginKeyToken.sol index dc32a3f..0e896c6 100644 --- a/src/OriginKeyToken.sol +++ b/src/OriginKeyToken.sol @@ -230,14 +230,13 @@ contract OriginKeyToken is ReentrancyGuard { // ─── Buy ────────────────────────────────────────────────────────────────── // PITcoin exact — fee returned to first buyer, distributed to holders after - function buy(uint256 cbbtcAmount, uint256 minTokens) external nonReentrant { + function buy(uint256 cbbtcAmount) external nonReentrant { require(cbbtcAmount >= MIN_SATS, "Minimum 100 sats"); require(cbbtcAmount <= MAX_BUY, "Maximum 1,000,000 sats per buy"); CBBTC.safeTransferFrom(msg.sender, address(this), cbbtcAmount); uint256 fee = (cbbtcAmount * BUY_FEE) / 100; uint256 tokens = cbbtcAmount - fee; - require(tokens >= minTokens, "Slippage: too few tokens"); if (totalSupply > 0) { _distributeFee(fee); diff --git a/test/OKT.audit.t.sol b/test/OKT.audit.t.sol index e528084..649c6d3 100644 --- a/test/OKT.audit.t.sol +++ b/test/OKT.audit.t.sol @@ -29,7 +29,7 @@ contract ReentrancyAttacker { function attack(uint256 amount) external { cbbtc.approve(address(okt), type(uint256).max); - okt.buy(amount, 0); + okt.buy(amount); } // Try to reenter on cbBTC transfer callback @@ -71,7 +71,7 @@ contract OKTAuditTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_reentrancyOnWithdraw() public { - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); ReentrancyAttacker attacker = new ReentrancyAttacker(okt, cbbtc); cbbtc.mint(address(attacker), 100_000); @@ -79,7 +79,7 @@ contract OKTAuditTest is Test { attacker.attack(10_000); // Generate dividends for attacker - vm.prank(bob); okt.buy(100_000, 0); + vm.prank(bob); okt.buy(100_000); // Attacker tries to reenter — ReentrancyGuard should block uint256 contractBefore = cbbtc.balanceOf(address(okt)); @@ -95,15 +95,15 @@ contract OKTAuditTest is Test { // Sandwich attack simulation — buy before, sell after a large trade function test_sandwichAttackUnprofitable() public { - vm.prank(alice); okt.buy(1_000_000, 0); // seed + vm.prank(alice); okt.buy(1_000_000); // seed uint256 attackerBefore = cbbtc.balanceOf(bob); // Attacker front-runs with buy - vm.prank(bob); okt.buy(100_000, 0); + vm.prank(bob); okt.buy(100_000); // Victim makes large buy - vm.prank(carol); okt.buy(1_000_000, 0); + vm.prank(carol); okt.buy(1_000_000); // Attacker back-runs with sell uint256 bobBal = okt.balanceOf(bob); @@ -123,12 +123,12 @@ contract OKTAuditTest is Test { // Flash loan simulation — massive buy/sell in same context function test_flashLoanAttackUnprofitable() public { - vm.prank(alice); okt.buy(1_000_000, 0); // seed + vm.prank(alice); okt.buy(1_000_000); // seed uint256 attackerBefore = cbbtc.balanceOf(bob); // Simulate flash loan — buy massive amount - vm.prank(bob); okt.buy(1_000_000, 0); + vm.prank(bob); okt.buy(1_000_000); // Immediately sell uint256 bobBal = okt.balanceOf(bob); @@ -153,15 +153,15 @@ contract OKTAuditTest is Test { // Attacker buys massive amount to dominate supply // Whale buys max 10 times for (uint i = 0; i < 10; i++) { - vm.prank(bob); okt.buy(1_000_000, 0); + vm.prank(bob); okt.buy(1_000_000); } // Other users should still be able to buy - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); assertGt(okt.balanceOf(alice), 0, "Alice should still be able to buy"); // Larger buy to generate meaningful dividends - vm.prank(carol); okt.buy(1_000_000, 0); + vm.prank(carol); okt.buy(1_000_000); uint256 aliceDivs = okt.dividendsOf(alice); // Alice holds ~0.2% of supply so gets ~0.2% of 70,000 fee = ~140 sats assertGt(aliceDivs, 0, "Alice should earn dividends even with whale"); @@ -173,17 +173,17 @@ contract OKTAuditTest is Test { function test_maxBuyDoesNotOverflow() public { // Buy with max amount — should work cleanly - vm.prank(alice); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(1_000_000); assertGt(okt.balanceOf(alice), 0, "Max buy should work"); assertGt(okt.totalSupply(), 0, "Supply should increase"); } function test_profitPerTokenDoesNotOverflow() public { // Small supply, large fee — worst case for overflow - vm.prank(alice); okt.buy(100, 0); // minimum buy, first buyer gets 100 + vm.prank(alice); okt.buy(100); // minimum buy, first buyer gets 100 // Large buy generates large fee distributed to small supply - vm.prank(bob); okt.buy(1_000_000, 0); + vm.prank(bob); okt.buy(1_000_000); // profitPerToken should be very large but not overflow uint256 ppt = okt.profitPerToken(); @@ -204,13 +204,13 @@ contract OKTAuditTest is Test { uint256 totalOut = 0; // Track all cbBTC entering contract - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); totalIn += 100_000; - vm.prank(bob); okt.buy(200_000, 0); + vm.prank(bob); okt.buy(200_000); totalIn += 200_000; - vm.prank(carol); okt.buy(50_000, 0); + vm.prank(carol); okt.buy(50_000); totalIn += 50_000; // Track all cbBTC leaving contract @@ -246,16 +246,16 @@ contract OKTAuditTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_gasConsistentAfterManyTransactions() public { - vm.prank(alice); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(1_000_000); // Do 100 transactions to grow state for (uint i = 0; i < 100; i++) { - vm.prank(bob); okt.buy(1_000, 0); + vm.prank(bob); okt.buy(1_000); } // Measure gas for a buy after 100 transactions uint256 gasBefore = gasleft(); - vm.prank(carol); okt.buy(1_000, 0); + vm.prank(carol); okt.buy(1_000); uint256 gasUsed = gasBefore - gasleft(); // Gas should be reasonable — under 200k @@ -263,10 +263,10 @@ contract OKTAuditTest is Test { } function test_gasConsistentForSellAfterManyTransactions() public { - vm.prank(alice); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(1_000_000); for (uint i = 0; i < 100; i++) { - vm.prank(bob); okt.buy(1_000, 0); + vm.prank(bob); okt.buy(1_000); } uint256 bobBal = okt.balanceOf(bob); @@ -306,26 +306,26 @@ contract OKTAuditTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_buyExactMinimum() public { - vm.prank(alice); okt.buy(100, 0); + vm.prank(alice); okt.buy(100); assertEq(okt.balanceOf(alice), 100); // first buyer gets fee back } function test_buyAboveMaxReverts() public { vm.prank(alice); vm.expectRevert("Maximum 1,000,000 sats per buy"); - okt.buy(1_000_001, 0); + okt.buy(1_000_001); } function test_buyExactMaxWorks() public { - vm.prank(alice); okt.buy(100, 0); // seed first buyer - vm.prank(bob); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(100); // seed first buyer + vm.prank(bob); okt.buy(1_000_000); assertGt(okt.balanceOf(bob), 0, "Max buy should work"); } function test_buyBelowMinimumReverts() public { vm.prank(alice); vm.expectRevert("Minimum 100 sats"); - okt.buy(99, 0); + okt.buy(99); } function test_inscribeBelowMinimumReverts() public { @@ -336,8 +336,8 @@ contract OKTAuditTest is Test { // sellOneToken removed — replaced by test_sellBelowMinimumReverts and test_sellMinimumChargesFee function test_sellBelowMinimumReverts() public { - vm.prank(alice); okt.buy(10_000, 0); - vm.prank(bob); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); + vm.prank(bob); okt.buy(10_000); vm.prank(bob); vm.expectRevert("Minimum 100 sats to sell"); @@ -345,8 +345,8 @@ contract OKTAuditTest is Test { } function test_sellMinimumChargesFee() public { - vm.prank(alice); okt.buy(10_000, 0); - vm.prank(bob); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); + vm.prank(bob); okt.buy(10_000); uint256 bobBefore = cbbtc.balanceOf(bob); vm.prank(bob); @@ -371,7 +371,7 @@ contract OKTAuditTest is Test { } function test_zeroTransferReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Zero tokens"); okt.transfer(bob, 0); @@ -402,7 +402,7 @@ contract OKTAuditTest is Test { okt.inscribe(vault2, bytes32("SMALL"), 10_000, 0, ""); // Generate dividends - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); uint256 vault1Divs = okt.dividendsOf(vault1); uint256 vault2Divs = okt.dividendsOf(vault2); @@ -420,8 +420,8 @@ contract OKTAuditTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_transferDoesNotCreateDividends() public { - vm.prank(alice); okt.buy(100_000, 0); - vm.prank(bob); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); + vm.prank(bob); okt.buy(100_000); uint256 contractBefore = cbbtc.balanceOf(address(okt)); @@ -441,8 +441,8 @@ contract OKTAuditTest is Test { } function test_transferPreservesDividends() public { - vm.prank(alice); okt.buy(100_000, 0); - vm.prank(bob); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); + vm.prank(bob); okt.buy(100_000); uint256 aliceDivsBefore = okt.dividendsOf(alice); diff --git a/test/OKT.invariant.t.sol b/test/OKT.invariant.t.sol index 5b8dc31..8539add 100644 --- a/test/OKT.invariant.t.sol +++ b/test/OKT.invariant.t.sol @@ -48,7 +48,7 @@ contract OKTHandler is Test { if (cbbtc.balanceOf(actor) < amount) return; vm.prank(actor); - try okt.buy(amount, 0) {} catch {} + try okt.buy(amount) {} catch {} } // ─── Sell ───────────────────────────────────────────────────────────────── @@ -130,7 +130,7 @@ contract OKTInvariantTest is Test { // Seed the contract with a first buy so totalSupply > 0 address first = handler.actors(0); vm.prank(first); - okt.buy(10_000, 0); + okt.buy(10_000); // Tell Foundry to only call handler functions targetContract(address(handler)); diff --git a/test/OKT.security.t.sol b/test/OKT.security.t.sol index 08c7243..13fb85e 100644 --- a/test/OKT.security.t.sol +++ b/test/OKT.security.t.sol @@ -48,13 +48,13 @@ contract OKTSecurityTest is Test { function test_dividendDrift_100Cycles() public { // Seed the contract - vm.prank(alice); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(1_000_000); uint256 contractBalBefore = cbbtc.balanceOf(address(okt)); // Bob does 100 buy/sell/withdraw cycles for (uint i = 0; i < 100; i++) { - vm.prank(bob); okt.buy(10_000, 0); + vm.prank(bob); okt.buy(10_000); uint256 bobBal = okt.balanceOf(bob); if (bobBal > 1) { vm.prank(bob); okt.sell(bobBal - 1, 0); @@ -73,14 +73,14 @@ contract OKTSecurityTest is Test { function test_dividendDrift_multipleActors() public { // Three actors trading simultaneously - vm.prank(alice); okt.buy(500_000, 0); - vm.prank(bob); okt.buy(500_000, 0); - vm.prank(carol); okt.buy(500_000, 0); + vm.prank(alice); okt.buy(500_000); + vm.prank(bob); okt.buy(500_000); + vm.prank(carol); okt.buy(500_000); for (uint i = 0; i < 50; i++) { // Each actor buys, sells, withdraws in rotation - vm.prank(alice); okt.buy(10_000, 0); - vm.prank(bob); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); + vm.prank(bob); okt.buy(10_000); uint256 aliceBal = okt.balanceOf(alice); if (aliceBal > 1000) { @@ -92,7 +92,7 @@ contract OKTSecurityTest is Test { vm.prank(bob); okt.withdraw(); } - vm.prank(carol); okt.buy(5_000, 0); + vm.prank(carol); okt.buy(5_000); } // Final solvency check @@ -109,10 +109,10 @@ contract OKTSecurityTest is Test { function test_precisionLoss_minimumBuy() public { // First buyer - vm.prank(alice); okt.buy(100, 0); // minimum buy + vm.prank(alice); okt.buy(100); // minimum buy // Second buyer minimum - vm.prank(bob); okt.buy(100, 0); + vm.prank(bob); okt.buy(100); // Contract should hold exactly 200 sats assertEq(cbbtc.balanceOf(address(okt)), 200); @@ -124,13 +124,13 @@ contract OKTSecurityTest is Test { function test_precisionLoss_manySmallBuys() public { // First buyer - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); uint256 contractBefore = cbbtc.balanceOf(address(okt)); // 200 minimum buys from different senders for (uint i = 0; i < 200; i++) { - vm.prank(bob); okt.buy(100, 0); + vm.prank(bob); okt.buy(100); } uint256 contractAfter = cbbtc.balanceOf(address(okt)); @@ -141,8 +141,8 @@ contract OKTSecurityTest is Test { } function test_precisionLoss_manySmallSells() public { - vm.prank(alice); okt.buy(100_000, 0); - vm.prank(bob); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); + vm.prank(bob); okt.buy(100_000); // Bob sells 1 token at a time, 100 times for (uint i = 0; i < 100; i++) { @@ -159,8 +159,8 @@ contract OKTSecurityTest is Test { function test_precisionLoss_largeSpread() public { // One whale, one small buyer — tests precision with very different balances - vm.prank(alice); okt.buy(1_000_000, 0); // max buy whale - vm.prank(bob); okt.buy(100, 0); // minimum buy + vm.prank(alice); okt.buy(1_000_000); // max buy whale + vm.prank(bob); okt.buy(100); // minimum buy uint256 aliceDivs = okt.dividendsOf(alice); uint256 bobDivs = okt.dividendsOf(bob); @@ -179,8 +179,8 @@ contract OKTSecurityTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_reinvestDustLoop_blocked() public { - vm.prank(alice); okt.buy(10_000, 0); - vm.prank(bob); okt.buy(200, 0); // tiny buy generates small dividend + vm.prank(alice); okt.buy(10_000); + vm.prank(bob); okt.buy(200); // tiny buy generates small dividend uint256 supplyBefore = okt.totalSupply(); @@ -250,7 +250,7 @@ contract OKTSecurityTest is Test { okt.inscribe(vault1, bytes32("TEST-001"), 50_000, 0, ""); // Need another holder to transfer to - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); // Vault transfers — should trigger sweep vm.prank(vault1); @@ -265,7 +265,7 @@ contract OKTSecurityTest is Test { okt.inscribe(vault1, bytes32("TEST-001"), 50_000, 0, ""); // Generate some dividends for the vault - vm.prank(alice); okt.buy(100_000, 0); + vm.prank(alice); okt.buy(100_000); uint256 vaultDivs = okt.dividendsOf(vault1); if (vaultDivs > 0) { @@ -283,8 +283,8 @@ contract OKTSecurityTest is Test { // ═══════════════════════════════════════════════════════════════════════════ function test_doubleSpend_sellThenWithdraw() public { - vm.prank(alice); okt.buy(1_000_000, 0); - vm.prank(bob); okt.buy(1_000_000, 0); + vm.prank(alice); okt.buy(1_000_000); + vm.prank(bob); okt.buy(1_000_000); uint256 bobCbbtcBefore = cbbtc.balanceOf(bob); @@ -307,13 +307,13 @@ contract OKTSecurityTest is Test { } function test_doubleSpend_rapidBuySellCycle() public { - vm.prank(alice); okt.buy(1_000_000, 0); // seed + vm.prank(alice); okt.buy(1_000_000); // seed uint256 attackerBefore = cbbtc.balanceOf(attacker); // Attacker tries rapid buy/sell/withdraw 20 times for (uint i = 0; i < 20; i++) { - vm.prank(attacker); okt.buy(10_000, 0); + vm.prank(attacker); okt.buy(10_000); uint256 bal = okt.balanceOf(attacker); if (bal > 1 && okt.totalSupply() > bal) { vm.prank(attacker); okt.sell(bal - 1, 0); @@ -337,39 +337,39 @@ contract OKTSecurityTest is Test { function test_zeroBuyReverts() public { vm.prank(alice); vm.expectRevert("Minimum 100 sats"); - okt.buy(0, 0); + okt.buy(0); } function test_zeroSellReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Minimum 100 sats to sell"); okt.sell(0, 0); } function test_sellMoreThanBalanceReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Insufficient balance"); okt.sell(999_999, 0); } function test_transferToZeroReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Zero address"); okt.transfer(address(0), 100); } function test_withdrawWithNoDivsReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("No dividends to withdraw"); okt.withdraw(); } function test_sellEntireSupplyReverts() public { - vm.prank(alice); okt.buy(10_000, 0); + vm.prank(alice); okt.buy(10_000); uint256 bal = okt.balanceOf(alice); vm.prank(alice); vm.expectRevert("Cannot sell entire supply"); diff --git a/test/OKT.t.sol b/test/OKT.t.sol index dba785d..bba3b88 100644 --- a/test/OKT.t.sol +++ b/test/OKT.t.sol @@ -46,27 +46,27 @@ contract OKTTest is Test { function test_firstBuyGetsFullAmount() public { vm.prank(alice); - okt.buy(SATS, 0); + okt.buy(SATS); assertEq(okt.balanceOf(alice), SATS); // first buyer gets fee back } function test_secondBuyPays7PercentFee() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); assertEq(okt.balanceOf(bob), SATS * 93 / 100); } // ─── Dividend tests ─────────────────────────────────────────────────────── function test_dividendsAccumulateAfterBuy() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); assertGt(okt.dividendsOf(alice), 0); } function test_withdrawGivesCorrectAmount() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); uint256 divs = okt.dividendsOf(alice); uint256 balBefore = cbbtc.balanceOf(alice); vm.prank(alice); okt.withdraw(); @@ -76,8 +76,8 @@ contract OKTTest is Test { // ─── Sell tests ─────────────────────────────────────────────────────────── function test_sellSendsCbbtcDirectly() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); uint256 balBefore = cbbtc.balanceOf(bob); uint256 bobTokens = okt.balanceOf(bob); vm.prank(bob); okt.sell(bobTokens - 1, 0); @@ -87,17 +87,17 @@ contract OKTTest is Test { } function test_sellAndRebuyStillEarnsDividends() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); vm.prank(bob); okt.sell(1000, 0); // sell fixed amount - vm.prank(bob); okt.buy(SATS, 0); - vm.prank(carol); okt.buy(SATS, 0); + vm.prank(bob); okt.buy(SATS); + vm.prank(carol); okt.buy(SATS); assertGt(okt.dividendsOf(bob), 0); } function test_sellEverythingDividendsRemain() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); uint256 divsBefore = okt.dividendsOf(alice); vm.prank(alice); okt.sell(1000, 0); uint256 divsAfter = okt.dividendsOf(alice); @@ -107,8 +107,8 @@ contract OKTTest is Test { // ─── Reinvest test ──────────────────────────────────────────────────────── function test_reinvestMintsTokens() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); uint256 balBefore = okt.balanceOf(alice); vm.prank(alice); okt.reinvest(); assertGt(okt.balanceOf(alice), balBefore); @@ -116,8 +116,8 @@ contract OKTTest is Test { // ─── Double-spend protection test ─────────────────────────────────────── function test_noDoubleSpendOnSell() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); uint256 bobCbbtcBefore = cbbtc.balanceOf(bob); uint256 bobTokens = okt.balanceOf(bob); @@ -139,8 +139,8 @@ contract OKTTest is Test { // ─── Reinvest dust loop prevention ────────────────────────────────────── function test_reinvestMinimum100Sats() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); // Alice has dividends from Bob's buy but likely < 100 sats uint256 divs = okt.dividendsOf(alice); if (divs < 100) { @@ -151,8 +151,8 @@ contract OKTTest is Test { } function test_reinvestDustLoopBlocked() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(200, 0); // small buy generates tiny dividend + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(200); // small buy generates tiny dividend uint256 divs = okt.dividendsOf(alice); if (divs < 100) { // Cannot reinvest dust — loop is blocked @@ -166,9 +166,9 @@ contract OKTTest is Test { function test_minimalSell() public { // Alice buys first - gets 10000 (first buyer) - vm.prank(alice); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); // Bob buys - gets 9300 - vm.prank(bob); okt.buy(SATS, 0); + vm.prank(bob); okt.buy(SATS); // Check balances before sell uint256 bobBalance = okt.balanceOf(bob); @@ -191,9 +191,9 @@ contract OKTTest is Test { // cbBTC in contract must always cover all claimable dividends function test_solvency_after_buys() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); - vm.prank(carol); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); + vm.prank(carol); okt.buy(SATS); uint256 contractBalance = cbbtc.balanceOf(address(okt)); uint256 totalOwed = okt.dividendsOf(alice) @@ -204,9 +204,9 @@ contract OKTTest is Test { } function test_solvency_after_sell() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); - vm.prank(carol); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); + vm.prank(carol); okt.buy(SATS); vm.prank(bob); okt.sell(1000, 0); // sell fixed amount uint256 contractBalance = cbbtc.balanceOf(address(okt)); @@ -226,9 +226,9 @@ contract OKTTest is Test { } function test_solvency_after_withdraw() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); - vm.prank(carol); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); + vm.prank(carol); okt.buy(SATS); vm.prank(bob); okt.sell(1000, 0); // sell fixed amount vm.prank(alice); okt.withdraw(); @@ -241,9 +241,9 @@ contract OKTTest is Test { } function test_solvency_after_reinvest() public { - vm.prank(alice); okt.buy(SATS, 0); - vm.prank(bob); okt.buy(SATS, 0); - vm.prank(carol); okt.buy(SATS, 0); + vm.prank(alice); okt.buy(SATS); + vm.prank(bob); okt.buy(SATS); + vm.prank(carol); okt.buy(SATS); vm.prank(bob); okt.sell(1000, 0); // sell fixed amount vm.prank(alice); okt.reinvest(); From adb6a96c814a8502f8804828c46bb66dc11fc2b4 Mon Sep 17 00:00:00 2001 From: J0 Satoshi Date: Fri, 12 Jun 2026 22:48:58 +0300 Subject: [PATCH 2/4] remove minCbbtc and isVault --- src/OriginKeyToken.sol | 13 +++++-------- test/OKT.audit.t.sol | 12 ++++++------ test/OKT.invariant.t.sol | 2 +- test/OKT.security.t.sol | 18 +++++++++--------- test/OKT.t.sol | 16 ++++++++-------- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/OriginKeyToken.sol b/src/OriginKeyToken.sol index 0e896c6..a418401 100644 --- a/src/OriginKeyToken.sol +++ b/src/OriginKeyToken.sol @@ -144,8 +144,7 @@ contract OriginKeyToken is ReentrancyGuard { // ─── Vault registrar ────────────────────────────────────────────────────── address public immutable vaultRegistrar; - mapping(address => bytes32) public vaultRegistry; - mapping(address => bool) public isVault; + mapping(address => bytes32) public vaultRegistry; // bytes32(0) means no vault mapping(address => bool) public vaultHasBeenSwept; mapping(address => uint256) public vaultOrdinal; mapping(address => bool) public vaultHasOrdinal; @@ -222,7 +221,7 @@ contract OriginKeyToken is ReentrancyGuard { // ─── Vault sweep check ─────────────────────────────────────────────────── function _checkVaultSweep(address vault, uint256 amount) internal { - if (isVault[vault] && !vaultHasBeenSwept[vault]) { + if (vaultRegistry[vault] != bytes32(0) && !vaultHasBeenSwept[vault]) { vaultHasBeenSwept[vault] = true; emit VaultSwept(vault, vaultRegistry[vault], amount, block.timestamp); } @@ -274,7 +273,7 @@ contract OriginKeyToken is ReentrancyGuard { // DO NOT change signedSub to signedAdd — that breaks dividend distribution // for any wallet that sells and rebuys. PITcoin uses signedSub. Always. // - function sell(uint256 tokens, uint256 minCbbtc) external nonReentrant { + function sell(uint256 tokens) external nonReentrant { require(tokens >= MIN_SELL, "Minimum 100 sats to sell"); require(balanceOf[msg.sender] >= tokens, "Insufficient balance"); require(totalSupply > tokens, "Cannot sell entire supply"); @@ -283,7 +282,6 @@ contract OriginKeyToken is ReentrancyGuard { uint256 fee = (tokens * SELL_FEE) / 100; uint256 taxed = tokens - fee; - require(taxed >= minCbbtc, "Slippage: too little cbBTC"); // 1. Burn tokens balanceOf[msg.sender] -= tokens; @@ -371,7 +369,7 @@ contract OriginKeyToken is ReentrancyGuard { require(vault != address(0), "Vault: zero address"); require(assetId != bytes32(0), "Vault: empty asset ID"); require(cbbtcAmount >= MIN_SATS, "Vault: minimum 100 sats"); - require(!isVault[vault], "Vault: already registered"); + require(vaultRegistry[vault] == bytes32(0), "Vault: already registered"); CBBTC.safeTransferFrom(msg.sender, address(this), cbbtcAmount); @@ -397,7 +395,6 @@ contract OriginKeyToken is ReentrancyGuard { // Register vault vaultRegistry[vault] = assetId; - isVault[vault] = true; vaultHasOrdinal[vault] = (ordinalNumber > 0); if (ordinalNumber > 0) { @@ -459,7 +456,7 @@ contract OriginKeyToken is ReentrancyGuard { bytes32 assetId ) { return ( - isVault[vault], + vaultRegistry[vault] != bytes32(0), vaultHasBeenSwept[vault], balanceOf[vault], vaultRegistry[vault] diff --git a/test/OKT.audit.t.sol b/test/OKT.audit.t.sol index 649c6d3..aa03bec 100644 --- a/test/OKT.audit.t.sol +++ b/test/OKT.audit.t.sol @@ -108,7 +108,7 @@ contract OKTAuditTest is Test { // Attacker back-runs with sell uint256 bobBal = okt.balanceOf(bob); if (bobBal > 1 && okt.totalSupply() > bobBal) { - vm.prank(bob); okt.sell(bobBal - 1, 0); + vm.prank(bob); okt.sell(bobBal - 1); } uint256 bobDivs = okt.dividendsOf(bob); if (bobDivs > 0) { @@ -133,7 +133,7 @@ contract OKTAuditTest is Test { // Immediately sell uint256 bobBal = okt.balanceOf(bob); if (bobBal > 1 && okt.totalSupply() > bobBal) { - vm.prank(bob); okt.sell(bobBal - 1, 0); + vm.prank(bob); okt.sell(bobBal - 1); } // Collect any dividends @@ -216,7 +216,7 @@ contract OKTAuditTest is Test { // Track all cbBTC leaving contract uint256 bobBefore = cbbtc.balanceOf(bob); uint256 bobBal = okt.balanceOf(bob); - vm.prank(bob); okt.sell(bobBal - 1, 0); + vm.prank(bob); okt.sell(bobBal - 1); uint256 bobDivs = okt.dividendsOf(bob); if (bobDivs > 0) { vm.prank(bob); okt.withdraw(); @@ -271,7 +271,7 @@ contract OKTAuditTest is Test { uint256 bobBal = okt.balanceOf(bob); uint256 gasBefore = gasleft(); - vm.prank(bob); okt.sell(bobBal - 1, 0); + vm.prank(bob); okt.sell(bobBal - 1); uint256 gasUsed = gasBefore - gasleft(); assertLt(gasUsed, 200_000, "Sell gas cost grew unreasonably"); @@ -341,7 +341,7 @@ contract OKTAuditTest is Test { vm.prank(bob); vm.expectRevert("Minimum 100 sats to sell"); - okt.sell(99, 0); + okt.sell(99); } function test_sellMinimumChargesFee() public { @@ -350,7 +350,7 @@ contract OKTAuditTest is Test { uint256 bobBefore = cbbtc.balanceOf(bob); vm.prank(bob); - okt.sell(100, 0); + okt.sell(100); uint256 bobAfter = cbbtc.balanceOf(bob); assertEq(bobAfter - bobBefore, 93, "Minimum sell should charge 7 sats"); } diff --git a/test/OKT.invariant.t.sol b/test/OKT.invariant.t.sol index 8539add..a17ef50 100644 --- a/test/OKT.invariant.t.sol +++ b/test/OKT.invariant.t.sol @@ -63,7 +63,7 @@ contract OKTHandler is Test { if (okt.totalSupply() <= amount) return; vm.prank(actor); - try okt.sell(amount, 0) {} catch {} + try okt.sell(amount) {} catch {} } // ─── Withdraw ───────────────────────────────────────────────────────────── diff --git a/test/OKT.security.t.sol b/test/OKT.security.t.sol index 13fb85e..b75ee22 100644 --- a/test/OKT.security.t.sol +++ b/test/OKT.security.t.sol @@ -57,7 +57,7 @@ contract OKTSecurityTest is Test { vm.prank(bob); okt.buy(10_000); uint256 bobBal = okt.balanceOf(bob); if (bobBal > 1) { - vm.prank(bob); okt.sell(bobBal - 1, 0); + vm.prank(bob); okt.sell(bobBal - 1); } uint256 bobDivs = okt.dividendsOf(bob); if (bobDivs > 0) { @@ -84,7 +84,7 @@ contract OKTSecurityTest is Test { uint256 aliceBal = okt.balanceOf(alice); if (aliceBal > 1000) { - vm.prank(alice); okt.sell(1000, 0); + vm.prank(alice); okt.sell(1000); } uint256 bobDivs = okt.dividendsOf(bob); @@ -148,7 +148,7 @@ contract OKTSecurityTest is Test { for (uint i = 0; i < 100; i++) { uint256 bobBal = okt.balanceOf(bob); if (bobBal > 1 && okt.totalSupply() > 1) { - vm.prank(bob); okt.sell(100, 0); + vm.prank(bob); okt.sell(100); } } @@ -238,7 +238,7 @@ contract OKTSecurityTest is Test { // Vault sells — should trigger VaultSwept vm.prank(vault1); - okt.sell(1000, 0); + okt.sell(1000); // Check vault is marked as swept (bool registered, bool swept,,) = okt.vaultStatus(vault1); @@ -290,7 +290,7 @@ contract OKTSecurityTest is Test { // Bob sells everything except 1 uint256 bobTokens = okt.balanceOf(bob); - vm.prank(bob); okt.sell(bobTokens - 1, 0); + vm.prank(bob); okt.sell(bobTokens - 1); // Bob tries to withdraw any remaining dividends uint256 bobDivs = okt.dividendsOf(bob); @@ -316,7 +316,7 @@ contract OKTSecurityTest is Test { vm.prank(attacker); okt.buy(10_000); uint256 bal = okt.balanceOf(attacker); if (bal > 1 && okt.totalSupply() > bal) { - vm.prank(attacker); okt.sell(bal - 1, 0); + vm.prank(attacker); okt.sell(bal - 1); } uint256 divs = okt.dividendsOf(attacker); if (divs > 0) { @@ -344,14 +344,14 @@ contract OKTSecurityTest is Test { vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Minimum 100 sats to sell"); - okt.sell(0, 0); + okt.sell(0); } function test_sellMoreThanBalanceReverts() public { vm.prank(alice); okt.buy(10_000); vm.prank(alice); vm.expectRevert("Insufficient balance"); - okt.sell(999_999, 0); + okt.sell(999_999); } function test_transferToZeroReverts() public { @@ -373,7 +373,7 @@ contract OKTSecurityTest is Test { uint256 bal = okt.balanceOf(alice); vm.prank(alice); vm.expectRevert("Cannot sell entire supply"); - okt.sell(bal, 0); + okt.sell(bal); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/test/OKT.t.sol b/test/OKT.t.sol index bba3b88..5d942fb 100644 --- a/test/OKT.t.sol +++ b/test/OKT.t.sol @@ -80,7 +80,7 @@ contract OKTTest is Test { vm.prank(bob); okt.buy(SATS); uint256 balBefore = cbbtc.balanceOf(bob); uint256 bobTokens = okt.balanceOf(bob); - vm.prank(bob); okt.sell(bobTokens - 1, 0); + vm.prank(bob); okt.sell(bobTokens - 1); uint256 balAfter = cbbtc.balanceOf(bob); // Bob should receive 93% of tokens sold directly in cbBTC assertGt(balAfter, balBefore); @@ -89,7 +89,7 @@ contract OKTTest is Test { function test_sellAndRebuyStillEarnsDividends() public { vm.prank(alice); okt.buy(SATS); vm.prank(bob); okt.buy(SATS); - vm.prank(bob); okt.sell(1000, 0); // sell fixed amount + vm.prank(bob); okt.sell(1000); // sell fixed amount vm.prank(bob); okt.buy(SATS); vm.prank(carol); okt.buy(SATS); assertGt(okt.dividendsOf(bob), 0); @@ -99,7 +99,7 @@ contract OKTTest is Test { vm.prank(alice); okt.buy(SATS); vm.prank(bob); okt.buy(SATS); uint256 divsBefore = okt.dividendsOf(alice); - vm.prank(alice); okt.sell(1000, 0); + vm.prank(alice); okt.sell(1000); uint256 divsAfter = okt.dividendsOf(alice); assertGe(divsAfter, divsBefore); } @@ -123,7 +123,7 @@ contract OKTTest is Test { uint256 bobTokens = okt.balanceOf(bob); // Bob sells - vm.prank(bob); okt.sell(1000, 0); + vm.prank(bob); okt.sell(1000); uint256 bobCbbtcAfterSell = cbbtc.balanceOf(bob); uint256 bobDivsAfterSell = okt.dividendsOf(bob); @@ -181,7 +181,7 @@ contract OKTTest is Test { // Sell minimum 100 tokens vm.prank(bob); - okt.sell(100, 0); + okt.sell(100); emit log_named_uint("Contract cbBTC after sell", cbbtc.balanceOf(address(okt))); emit log_named_uint("Bob dividends after sell", okt.dividendsOf(bob)); @@ -207,7 +207,7 @@ contract OKTTest is Test { vm.prank(alice); okt.buy(SATS); vm.prank(bob); okt.buy(SATS); vm.prank(carol); okt.buy(SATS); - vm.prank(bob); okt.sell(1000, 0); // sell fixed amount + vm.prank(bob); okt.sell(1000); // sell fixed amount uint256 contractBalance = cbbtc.balanceOf(address(okt)); uint256 aliceDivs = okt.dividendsOf(alice); @@ -229,7 +229,7 @@ contract OKTTest is Test { vm.prank(alice); okt.buy(SATS); vm.prank(bob); okt.buy(SATS); vm.prank(carol); okt.buy(SATS); - vm.prank(bob); okt.sell(1000, 0); // sell fixed amount + vm.prank(bob); okt.sell(1000); // sell fixed amount vm.prank(alice); okt.withdraw(); uint256 contractBalance = cbbtc.balanceOf(address(okt)); @@ -244,7 +244,7 @@ contract OKTTest is Test { vm.prank(alice); okt.buy(SATS); vm.prank(bob); okt.buy(SATS); vm.prank(carol); okt.buy(SATS); - vm.prank(bob); okt.sell(1000, 0); // sell fixed amount + vm.prank(bob); okt.sell(1000); // sell fixed amount vm.prank(alice); okt.reinvest(); uint256 contractBalance = cbbtc.balanceOf(address(okt)); From 23c6f04821f7a38d11f36231a4670da923f140d3 Mon Sep 17 00:00:00 2001 From: naftalimurgor Date: Sun, 14 Jun 2026 16:02:06 +0300 Subject: [PATCH 3/4] Add OriginKeyToken audit report --- AUDIT.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 AUDIT.md diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..b8b2c69 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,98 @@ +# OriginKeyToken Audit + +Date: 2026-06-14 + +## Scope + +- `src/OriginKeyToken.sol` +- Existing tests were reviewed for intent and coverage, but no contract or test files were changed. + +## Tooling + +- `forge test`: 75 tests passed, 0 failed, 0 skipped. +- `slither .`: completed with Slither 0.11.5 after installing `slither-analyzer`. Slither reported dependency-level assembly usage in OpenZeppelin, mixed pragma ranges in dependencies, Solidity 0.8.20 version warnings, and one naming-convention warning for `CBBTC`. +- `slither src/OriginKeyToken.sol --foundry-ignore-compile`: completed with the same detector classes and no additional OKT-specific security findings. + +The audit findings below are based on manual source review, review of the existing Foundry tests, Foundry test execution, and Slither static analysis. + +## Summary + +No direct reserve-draining issue was identified in the reviewed source. The buy, sell, withdraw, transfer, and reinvest accounting follows a coherent per-share accumulator model, and the external reserve transfers in `buy`, `sell`, `withdraw`, `reinvest`, and `inscribe` are either protected by `nonReentrant` or contain no external call. + +The main risks found are operational and integration risks around immutable privileged roles, the breadth of ordinal invalidation, vault-sweep semantics, and intentional non-standard token behavior. + +## Findings + +### M-01: Oracle can permanently invalidate unregistered ordinals + +`reportOrdinalMoved()` accepts any positive `ordinalNumber`, even if no vault is registered for that ordinal. + +- Location: `src/OriginKeyToken.sol:421` +- Registration block: `src/OriginKeyToken.sol:400` + +When the oracle reports an unregistered ordinal as moved, `ordinalHasBeenMoved[ordinalNumber]` is permanently set. A later `inscribe()` for that ordinal will revert with `Ordinal: already moved`. Since `ordinalOracle` is immutable, an oracle compromise or operator mistake can permanently block future registration of targeted ordinals without any recovery path. + +Recommendation: Require reported ordinals to already be registered if the oracle is only meant to monitor registered vaults. If pre-invalidation of unregistered ordinals is intentional, document that power explicitly and consider an operational process around oracle custody, review, and deployment. + +### L-01: The final OKT supply cannot be fully redeemed + +`sell()` requires both `tokens >= MIN_SELL` and `totalSupply > tokens`. + +- Location: `src/OriginKeyToken.sol:276` + +This prevents division-by-zero behavior when distributing the sell fee, but it also means the last token or any residual balance below `MIN_SELL` is permanently non-redeemable through `sell()`. A sole holder can sell down to dust and withdraw dividends, but at least one OKT unit and its backing sat can remain locked. + +Recommendation: Treat this as an intentional economic constraint and disclose it clearly. If full redemption is required, the sell path needs a separate final-supply branch. + +### L-02: `VaultSwept` does not cover every possible value movement from a vault address + +The contract marks a vault as swept when the vault sells OKT, transfers OKT, withdraws dividends, or when the oracle reports its ordinal moved. + +- Sweep helper: `src/OriginKeyToken.sol:223` +- Sell hook: `src/OriginKeyToken.sol:281` +- Transfer hook: `src/OriginKeyToken.sol:312` +- Withdraw hook: `src/OriginKeyToken.sol:330` +- Ordinal hook: `src/OriginKeyToken.sol:421` + +The contract cannot detect ETH, cbBTC, NFT, or other token movements made directly by the vault address outside this contract. If product copy or downstream systems interpret `VaultSwept` as proof that any value left a vault, that claim is broader than the on-chain enforcement. + +Recommendation: Define `VaultSwept` as an OKT-contract-level signal, or pair it with off-chain/indexer monitoring for other assets held by the same vault address. + +### I-01: OKT exposes ERC20-like metadata and `transfer`, but not full ERC20 behavior + +The contract provides `name`, `symbol`, `decimals`, `totalSupply`, `balanceOf`, `transfer`, and `Transfer`, but intentionally omits `approve`, `allowance`, and `transferFrom`. + +- Token surface: `src/OriginKeyToken.sol:118` +- Transfer function: `src/OriginKeyToken.sol:307` + +This is tested as intentional behavior, but wallets, explorers, indexers, bridges, and exchanges may partially classify OKT as ERC20 and then fail on allowance-based flows. + +Recommendation: Document OKT as a custom non-ERC20 token interface wherever integrations are expected. + +### I-02: Reserve accounting assumes canonical, non-rebasing cbBTC behavior + +The constructor checks only that the reserve token address is nonzero and reports 8 decimals. + +- Location: `src/OriginKeyToken.sol:187` + +The accounting assumes exact `safeTransferFrom` and `safeTransfer` amounts, no transfer fees, no rebasing, and no blacklist/freeze behavior that would unexpectedly block reserve exits. This is reasonable for the documented canonical cbBTC deployment, but unsafe for arbitrary 8-decimal ERC20s. + +Recommendation: Deploy only with the canonical cbBTC address for the target network and document that substituting another 8-decimal token is unsupported. + +## Notes + +- The sell path updates balances and payout accounting before distributing the sell fee and before transferring cbBTC out, which matches the stated accounting model. +- `buy`, `sell`, `withdraw`, `reinvest`, and `inscribe` use `nonReentrant`; `transfer` has no external call. +- Direct cbBTC transfers into the contract create surplus backing and do not appear to create extra claimable dividends. +- Fee distribution floors division dust in favor of the contract, not users. + +## Verification Addendum + +Automated verification was completed after the initial manual audit: + +- `forge test`: 75 passed, 0 failed, 0 skipped. +- Invariant coverage included 4 invariant tests with 256 runs and 128,000 handler calls per invariant. +- `slither .`: analyzed 8 contracts with 101 detectors and produced 18 results. The results were informational/noise from OpenZeppelin assembly, dependency pragma ranges, Solidity version warnings, and the `CBBTC` naming convention. +- `slither src/OriginKeyToken.sol --foundry-ignore-compile`: completed with the same detector classes and no additional OKT-specific security findings. + +No new exploitable issue was identified by the completed automated verification. From 3e7e3c3db3c20dd4ce1997c6941b0bc4f4811a28 Mon Sep 17 00:00:00 2001 From: naftalimurgor Date: Sun, 14 Jun 2026 16:09:00 +0300 Subject: [PATCH 4/4] Revert "Add OriginKeyToken audit report" This reverts commit 23c6f04821f7a38d11f36231a4670da923f140d3. --- AUDIT.md | 98 -------------------------------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 AUDIT.md diff --git a/AUDIT.md b/AUDIT.md deleted file mode 100644 index b8b2c69..0000000 --- a/AUDIT.md +++ /dev/null @@ -1,98 +0,0 @@ -# OriginKeyToken Audit - -Date: 2026-06-14 - -## Scope - -- `src/OriginKeyToken.sol` -- Existing tests were reviewed for intent and coverage, but no contract or test files were changed. - -## Tooling - -- `forge test`: 75 tests passed, 0 failed, 0 skipped. -- `slither .`: completed with Slither 0.11.5 after installing `slither-analyzer`. Slither reported dependency-level assembly usage in OpenZeppelin, mixed pragma ranges in dependencies, Solidity 0.8.20 version warnings, and one naming-convention warning for `CBBTC`. -- `slither src/OriginKeyToken.sol --foundry-ignore-compile`: completed with the same detector classes and no additional OKT-specific security findings. - -The audit findings below are based on manual source review, review of the existing Foundry tests, Foundry test execution, and Slither static analysis. - -## Summary - -No direct reserve-draining issue was identified in the reviewed source. The buy, sell, withdraw, transfer, and reinvest accounting follows a coherent per-share accumulator model, and the external reserve transfers in `buy`, `sell`, `withdraw`, `reinvest`, and `inscribe` are either protected by `nonReentrant` or contain no external call. - -The main risks found are operational and integration risks around immutable privileged roles, the breadth of ordinal invalidation, vault-sweep semantics, and intentional non-standard token behavior. - -## Findings - -### M-01: Oracle can permanently invalidate unregistered ordinals - -`reportOrdinalMoved()` accepts any positive `ordinalNumber`, even if no vault is registered for that ordinal. - -- Location: `src/OriginKeyToken.sol:421` -- Registration block: `src/OriginKeyToken.sol:400` - -When the oracle reports an unregistered ordinal as moved, `ordinalHasBeenMoved[ordinalNumber]` is permanently set. A later `inscribe()` for that ordinal will revert with `Ordinal: already moved`. Since `ordinalOracle` is immutable, an oracle compromise or operator mistake can permanently block future registration of targeted ordinals without any recovery path. - -Recommendation: Require reported ordinals to already be registered if the oracle is only meant to monitor registered vaults. If pre-invalidation of unregistered ordinals is intentional, document that power explicitly and consider an operational process around oracle custody, review, and deployment. - -### L-01: The final OKT supply cannot be fully redeemed - -`sell()` requires both `tokens >= MIN_SELL` and `totalSupply > tokens`. - -- Location: `src/OriginKeyToken.sol:276` - -This prevents division-by-zero behavior when distributing the sell fee, but it also means the last token or any residual balance below `MIN_SELL` is permanently non-redeemable through `sell()`. A sole holder can sell down to dust and withdraw dividends, but at least one OKT unit and its backing sat can remain locked. - -Recommendation: Treat this as an intentional economic constraint and disclose it clearly. If full redemption is required, the sell path needs a separate final-supply branch. - -### L-02: `VaultSwept` does not cover every possible value movement from a vault address - -The contract marks a vault as swept when the vault sells OKT, transfers OKT, withdraws dividends, or when the oracle reports its ordinal moved. - -- Sweep helper: `src/OriginKeyToken.sol:223` -- Sell hook: `src/OriginKeyToken.sol:281` -- Transfer hook: `src/OriginKeyToken.sol:312` -- Withdraw hook: `src/OriginKeyToken.sol:330` -- Ordinal hook: `src/OriginKeyToken.sol:421` - -The contract cannot detect ETH, cbBTC, NFT, or other token movements made directly by the vault address outside this contract. If product copy or downstream systems interpret `VaultSwept` as proof that any value left a vault, that claim is broader than the on-chain enforcement. - -Recommendation: Define `VaultSwept` as an OKT-contract-level signal, or pair it with off-chain/indexer monitoring for other assets held by the same vault address. - -### I-01: OKT exposes ERC20-like metadata and `transfer`, but not full ERC20 behavior - -The contract provides `name`, `symbol`, `decimals`, `totalSupply`, `balanceOf`, `transfer`, and `Transfer`, but intentionally omits `approve`, `allowance`, and `transferFrom`. - -- Token surface: `src/OriginKeyToken.sol:118` -- Transfer function: `src/OriginKeyToken.sol:307` - -This is tested as intentional behavior, but wallets, explorers, indexers, bridges, and exchanges may partially classify OKT as ERC20 and then fail on allowance-based flows. - -Recommendation: Document OKT as a custom non-ERC20 token interface wherever integrations are expected. - -### I-02: Reserve accounting assumes canonical, non-rebasing cbBTC behavior - -The constructor checks only that the reserve token address is nonzero and reports 8 decimals. - -- Location: `src/OriginKeyToken.sol:187` - -The accounting assumes exact `safeTransferFrom` and `safeTransfer` amounts, no transfer fees, no rebasing, and no blacklist/freeze behavior that would unexpectedly block reserve exits. This is reasonable for the documented canonical cbBTC deployment, but unsafe for arbitrary 8-decimal ERC20s. - -Recommendation: Deploy only with the canonical cbBTC address for the target network and document that substituting another 8-decimal token is unsupported. - -## Notes - -- The sell path updates balances and payout accounting before distributing the sell fee and before transferring cbBTC out, which matches the stated accounting model. -- `buy`, `sell`, `withdraw`, `reinvest`, and `inscribe` use `nonReentrant`; `transfer` has no external call. -- Direct cbBTC transfers into the contract create surplus backing and do not appear to create extra claimable dividends. -- Fee distribution floors division dust in favor of the contract, not users. - -## Verification Addendum - -Automated verification was completed after the initial manual audit: - -- `forge test`: 75 passed, 0 failed, 0 skipped. -- Invariant coverage included 4 invariant tests with 256 runs and 128,000 handler calls per invariant. -- `slither .`: analyzed 8 contracts with 101 detectors and produced 18 results. The results were informational/noise from OpenZeppelin assembly, dependency pragma ranges, Solidity version warnings, and the `CBBTC` naming convention. -- `slither src/OriginKeyToken.sol --foundry-ignore-compile`: completed with the same detector classes and no additional OKT-specific security findings. - -No new exploitable issue was identified by the completed automated verification.