diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 304abe9..e7436a4 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -147,6 +147,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract Deploy is Script, Sphinx { error Deploy_ExistingAddressMismatch(address expected, address actual); error Deploy_ProjectIdMismatch(uint256 expected, uint256 actual); + error Deploy_ProjectNotOwned(uint256 projectId); error Deploy_PriceFeedMismatch(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency); // ════════════════════════════════════════════════════════════════════ @@ -854,12 +855,7 @@ contract Deploy is Script, Sphinx { // ════════════════════════════════════════════════════════════════════ function _deploySuckers() internal { - _deploySuckersOptimism(); - _deploySuckersBase(); - _deploySuckersArbitrum(); - _deploySuckersCCIP(); - - // Deploy the registry and pre-approve deployers. + // Deploy the registry FIRST — singleton sucker constructors consume it as an immutable. (address registry, bool registryDeployed) = _isDeployed( SUCKER_REGISTRY_SALT, type(JBSuckerRegistry).creationCode, @@ -871,6 +867,13 @@ contract Deploy is Script, Sphinx { _directory, _permissions, safeAddress(), _trustedForwarder ); + // Deploy singleton implementations and deployers (they reference _suckerRegistry). + _deploySuckersOptimism(); + _deploySuckersBase(); + _deploySuckersArbitrum(); + _deploySuckersCCIP(); + + // Pre-approve deployers in the registry. if (_preApprovedSuckerDeployers.length != 0) { for (uint256 i; i < _preApprovedSuckerDeployers.length; i++) { if (!_suckerRegistry.suckerDeployerIsAllowed(_preApprovedSuckerDeployers[i])) { @@ -1511,6 +1514,12 @@ contract Deploy is Script, Sphinx { // ════════════════════════════════════════════════════════════════════ function _deployRevnet() internal { + // Skip revnet deployment when the Uniswap stack was not deployed — revnets require buyback + router + // registries. + if (address(_buybackRegistry) == address(0)) { + return; + } + _revProjectId = _ensureProjectExists(_REV_PROJECT_ID); // Deploy REVLoans. @@ -1661,6 +1670,9 @@ contract Deploy is Script, Sphinx { // ════════════════════════════════════════════════════════════════════ function _deployCpnRevnet() internal { + // Skip when the Uniswap stack was not deployed — CPN revnet requires buyback + router registries. + if (address(_buybackRegistry) == address(0)) return; + address operator = 0x240dc2085caEF779F428dcd103CFD2fB510EdE82; JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1); @@ -1831,6 +1843,9 @@ contract Deploy is Script, Sphinx { // ════════════════════════════════════════════════════════════════════ function _deployNanaRevnet() internal { + // Skip when the Uniswap stack was not deployed — NANA revnet requires buyback + router registries. + if (address(_buybackRegistry) == address(0)) return; + uint256 feeProjectId = _FEE_PROJECT_ID; address operator = 0x80a8F7a4bD75b539CE26937016Df607fdC9ABeb5; @@ -1906,6 +1921,9 @@ contract Deploy is Script, Sphinx { // ════════════════════════════════════════════════════════════════════ function _deployBanny() internal { + // Skip when the Uniswap stack was not deployed — Banny revnet requires buyback + router registries. + if (address(_buybackRegistry) == address(0)) return; + if (_projects.count() >= _BAN_PROJECT_ID && address(_directory.controllerOf(_BAN_PROJECT_ID)) != address(0)) { return; } @@ -2184,7 +2202,12 @@ contract Deploy is Script, Sphinx { function _ensureProjectExists(uint256 expectedProjectId) internal returns (uint256) { uint256 count = _projects.count(); - if (count >= expectedProjectId) return expectedProjectId; + if (count >= expectedProjectId) { + if (_projects.ownerOf(expectedProjectId) != safeAddress()) { + revert Deploy_ProjectNotOwned(expectedProjectId); + } + return expectedProjectId; + } uint256 created = _projects.createFor(safeAddress()); if (created != expectedProjectId) revert Deploy_ProjectIdMismatch(expectedProjectId, created); diff --git a/test/fork/HookCompositionFork.t.sol b/test/fork/HookCompositionFork.t.sol index 057f5b7..db74189 100644 --- a/test/fork/HookCompositionFork.t.sol +++ b/test/fork/HookCompositionFork.t.sol @@ -39,15 +39,16 @@ contract HookCompositionForkTest is EcosystemForkTest { uint256 cashOutCount = payerTokens / 2; vm.prank(PAYER); - jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); // Fee project balance should have increased (2.5% fee on cashout). uint256 feeBalanceAfter = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); @@ -58,9 +59,7 @@ contract HookCompositionForkTest is EcosystemForkTest { assertGt(feeAccrued, 0, "fee accrued should be positive"); // Payer should have fewer tokens. - assertEq( - jbTokens().totalBalanceOf(PAYER, revnetId), payerTokens - cashOutCount, "payer tokens should decrease" - ); + assertEq(jbTokens().totalBalanceOf(PAYER, revnetId), payerTokens - cashOutCount, "payer tokens should decrease"); } /// @notice Mock fee terminal to revert on external pay(). Cash out tokens. @@ -74,10 +73,7 @@ contract HookCompositionForkTest is EcosystemForkTest { _buildTwoStageConfigWithLPSplit(7000, 2000, 2000); (uint256 revnetId,) = REV_DEPLOYER.deployFor({ - revnetId: 0, - configuration: cfg, - terminalConfigurations: tc, - suckerDeploymentConfiguration: sdc + revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc }); // Pay to build surplus. @@ -87,15 +83,16 @@ contract HookCompositionForkTest is EcosystemForkTest { // Do a NORMAL cashout first to measure baseline fee accrual. uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId); vm.prank(BORROWER); - jbMultiTerminal().cashOutTokensOf({ - holder: BORROWER, - projectId: revnetId, - cashOutCount: borrowerTokens / 4, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(BORROWER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: BORROWER, + projectId: revnetId, + cashOutCount: borrowerTokens / 4, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(BORROWER), + metadata: "" + }); uint256 feeAfterNormalCashout = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); assertGt(feeAfterNormalCashout, 0, "normal cashout should accrue fees"); @@ -104,9 +101,7 @@ contract HookCompositionForkTest is EcosystemForkTest { // The terminal's internal _processFee path is unaffected by external mocks. vm.mockCallRevert( address(jbMultiTerminal()), - abi.encodeWithSignature( - "pay(uint256,address,uint256,address,uint256,string,bytes)", FEE_PROJECT_ID - ), + abi.encodeWithSignature("pay(uint256,address,uint256,address,uint256,string,bytes)", FEE_PROJECT_ID), "fee terminal reverted" ); @@ -117,15 +112,16 @@ contract HookCompositionForkTest is EcosystemForkTest { uint256 projectBalanceBefore = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN); vm.prank(PAYER); - jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); // Main assertion: cashout succeeded (try-catch worked). assertGt(PAYER.balance, payerEthBefore, "payer should receive ETH despite fee terminal revert"); @@ -204,10 +200,7 @@ contract HookCompositionForkTest is EcosystemForkTest { _buildTwoStageConfigWithLPSplit(7000, 2000, 2000); (uint256 revnetId,) = REV_DEPLOYER.deployFor({ - revnetId: 0, - configuration: cfg, - terminalConfigurations: tc, - suckerDeploymentConfiguration: sdc + revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc }); uint256 feeBalancePrev = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); @@ -231,15 +224,16 @@ contract HookCompositionForkTest is EcosystemForkTest { uint256 payerEthBefore = PAYER.balance; vm.prank(PAYER); - jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); assertGt(PAYER.balance, payerEthBefore, "inv: payer received ETH from cashout"); @@ -260,15 +254,16 @@ contract HookCompositionForkTest is EcosystemForkTest { if (borrowerTokens > 0) { uint256 borrowerEthBefore = BORROWER.balance; vm.prank(BORROWER); - jbMultiTerminal().cashOutTokensOf({ - holder: BORROWER, - projectId: revnetId, - cashOutCount: borrowerTokens / 2, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(BORROWER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: BORROWER, + projectId: revnetId, + cashOutCount: borrowerTokens / 2, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(BORROWER), + metadata: "" + }); assertGt(BORROWER.balance, borrowerEthBefore, "inv: borrower received ETH"); feeBalanceNow = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); @@ -285,9 +280,7 @@ contract HookCompositionForkTest is EcosystemForkTest { assertGt(tokens, 0, "inv: post-AMM pay should return tokens"); // Invariant: LP split hook position exists (accumulated tokens > 0 from step 2). - assertGt( - LP_SPLIT_HOOK.accumulatedProjectTokens(revnetId), 0, "inv: LP split hook has accumulated tokens" - ); + assertGt(LP_SPLIT_HOOK.accumulatedProjectTokens(revnetId), 0, "inv: LP split hook has accumulated tokens"); // Final invariant: fee balance only grew. feeBalanceNow = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); @@ -304,10 +297,7 @@ contract HookCompositionForkTest is EcosystemForkTest { _buildTwoStageConfigWithLPSplit(0, 0, 2000); (uint256 revnetId,) = REV_DEPLOYER.deployFor({ - revnetId: 0, - configuration: cfg, - terminalConfigurations: tc, - suckerDeploymentConfiguration: sdc + revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc }); // Pay to build surplus. @@ -323,15 +313,16 @@ contract HookCompositionForkTest is EcosystemForkTest { uint256 payerEthBefore = PAYER.balance; vm.prank(PAYER); - jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); // Payer should receive ETH (full pro-rata with 0% tax). assertGt(PAYER.balance, payerEthBefore, "should receive ETH from 0% tax cashout"); diff --git a/test/fork/PayoutReentrancyFork.t.sol b/test/fork/PayoutReentrancyFork.t.sol index afe44a2..a8e89b6 100644 --- a/test/fork/PayoutReentrancyFork.t.sol +++ b/test/fork/PayoutReentrancyFork.t.sol @@ -51,11 +51,7 @@ contract MaliciousSplitHook is IJBSplitHook { // Attempt re-entry into sendPayoutsOf. This should fail because the payout limit // was already consumed by recordPayoutFor before splits execute. try terminal.sendPayoutsOf({ - projectId: targetProjectId, - token: token, - amount: amount, - currency: currency, - minTokensPaidOut: 0 + projectId: targetProjectId, token: token, amount: amount, currency: currency, minTokensPaidOut: 0 }) { // If we get here, re-entry succeeded (should NOT happen). reentrySucceeded = true; @@ -90,7 +86,13 @@ contract AddToBalanceSplitHook is IJBSplitHook { receive() external payable {} - function processSplitWith(JBSplitHookContext calldata /* context */) external payable override { + function processSplitWith( + JBSplitHookContext calldata /* context */ + ) + external + payable + override + { if (!addToBalanceCalled && msg.value > 0) { addToBalanceCalled = true; // Re-enter via addToBalanceOf, forwarding all received ETH back to the project. @@ -117,8 +119,8 @@ contract AddToBalanceSplitHook is IJBSplitHook { /// @notice Tests that payout split hooks cannot exploit re-entry to double-spend payouts. /// /// The `sendPayoutsOf()` flow is: -/// 1. `JBTerminalStore.recordPayoutFor()` — records payout limit usage and decreases balance BEFORE any external calls -/// 2. `JBPayoutSplitGroupLib.sendPayoutsToSplitGroupOf()` — iterates splits, calling `executePayout()` for each +/// 1. `JBTerminalStore.recordPayoutFor()` — records payout limit usage and decreases balance BEFORE any external +/// calls 2. `JBPayoutSplitGroupLib.sendPayoutsToSplitGroupOf()` — iterates splits, calling `executePayout()` for each /// 3. `executePayout()` — transfers funds to split hook and calls `processSplitWith()` /// /// Since step 1 consumes the payout limit before step 3 executes, a re-entrant call to `sendPayoutsOf()` @@ -208,13 +210,14 @@ contract PayoutReentrancyForkTest is EcosystemForkTest { }); // Launch the project. - projectId = jbController().launchProjectFor({ - owner: PROJECT_OWNER, - projectUri: "", - rulesetConfigurations: rulesetConfigs, - terminalConfigurations: tc, - memo: "" - }); + projectId = jbController() + .launchProjectFor({ + owner: PROJECT_OWNER, + projectUri: "", + rulesetConfigurations: rulesetConfigs, + terminalConfigurations: tc, + memo: "" + }); } // ═══════════════════════════════════════════════════════════════════ @@ -266,13 +269,14 @@ contract PayoutReentrancyForkTest is EcosystemForkTest { // 3. Hook tries to re-enter sendPayoutsOf -> recordPayoutFor reverts (limit consumed) // 4. try-catch in the hook catches the revert // 5. The first payout still completes (hook received the funds via try-catch in executePayout) - jbMultiTerminal().sendPayoutsOf({ - projectId: projectId, - token: JBConstants.NATIVE_TOKEN, - amount: PAYOUT_LIMIT, - currency: NATIVE_CURRENCY, - minTokensPaidOut: 0 - }); + jbMultiTerminal() + .sendPayoutsOf({ + projectId: projectId, + token: JBConstants.NATIVE_TOKEN, + amount: PAYOUT_LIMIT, + currency: NATIVE_CURRENCY, + minTokensPaidOut: 0 + }); // Verify the hook attempted re-entry. assertTrue(maliciousHook.reentryCalled(), "hook should have attempted re-entry"); @@ -299,13 +303,14 @@ contract PayoutReentrancyForkTest is EcosystemForkTest { // A second sendPayoutsOf should also fail since payout limit is consumed for this cycle. // Since duration=0, same ruleset stays active, so payout limit persists. vm.expectRevert(); - jbMultiTerminal().sendPayoutsOf({ - projectId: projectId, - token: JBConstants.NATIVE_TOKEN, - amount: PAYOUT_LIMIT, - currency: NATIVE_CURRENCY, - minTokensPaidOut: 0 - }); + jbMultiTerminal() + .sendPayoutsOf({ + projectId: projectId, + token: JBConstants.NATIVE_TOKEN, + amount: PAYOUT_LIMIT, + currency: NATIVE_CURRENCY, + minTokensPaidOut: 0 + }); } /// @notice A split hook re-enters via `addToBalanceOf()` during payout processing. @@ -341,13 +346,14 @@ contract PayoutReentrancyForkTest is EcosystemForkTest { // Step 5: Trigger payouts. // The hook will receive its split amount (after fee), then re-enter via addToBalanceOf // to send the ETH back to the project. - jbMultiTerminal().sendPayoutsOf({ - projectId: projectId, - token: JBConstants.NATIVE_TOKEN, - amount: PAYOUT_LIMIT, - currency: NATIVE_CURRENCY, - minTokensPaidOut: 0 - }); + jbMultiTerminal() + .sendPayoutsOf({ + projectId: projectId, + token: JBConstants.NATIVE_TOKEN, + amount: PAYOUT_LIMIT, + currency: NATIVE_CURRENCY, + minTokensPaidOut: 0 + }); // Verify the hook called addToBalanceOf. assertTrue(addHook.addToBalanceCalled(), "hook should have called addToBalanceOf"); @@ -373,12 +379,13 @@ contract PayoutReentrancyForkTest is EcosystemForkTest { // No double-payout occurred: we can verify the payout limit is consumed by trying again. vm.expectRevert(); - jbMultiTerminal().sendPayoutsOf({ - projectId: projectId, - token: JBConstants.NATIVE_TOKEN, - amount: PAYOUT_LIMIT, - currency: NATIVE_CURRENCY, - minTokensPaidOut: 0 - }); + jbMultiTerminal() + .sendPayoutsOf({ + projectId: projectId, + token: JBConstants.NATIVE_TOKEN, + amount: PAYOUT_LIMIT, + currency: NATIVE_CURRENCY, + minTokensPaidOut: 0 + }); } } diff --git a/test/fork/ReservedInflationFork.t.sol b/test/fork/ReservedInflationFork.t.sol index eef5687..2f497fc 100644 --- a/test/fork/ReservedInflationFork.t.sol +++ b/test/fork/ReservedInflationFork.t.sol @@ -53,15 +53,16 @@ contract ReservedInflationForkTest is EcosystemForkTest { uint256 payerEthBefore = PAYER.balance; vm.prank(PAYER); - uint256 reclaimAmount = jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + uint256 reclaimAmount = jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); uint256 ethReceived = PAYER.balance - payerEthBefore; assertGt(ethReceived, 0, "should receive ETH from cashout"); @@ -90,7 +91,8 @@ contract ReservedInflationForkTest is EcosystemForkTest { ); // Document the magnitude of the reduction. - uint256 reductionBps = ((hypotheticalReclaimNoReserved - reclaimAmount) * 10_000) / hypotheticalReclaimNoReserved; + uint256 reductionBps = + ((hypotheticalReclaimNoReserved - reclaimAmount) * 10_000) / hypotheticalReclaimNoReserved; assertGt(reductionBps, 0, "reclaim reduction should be measurable"); // With 80% reserved and 70% cashOutTaxRate, the inflation effect should be very significant. @@ -151,41 +153,37 @@ contract ReservedInflationForkTest is EcosystemForkTest { uint256 totalWithReservedA = jbController().totalTokenSupplyWithReservedTokensOf(revnetA); uint256 totalWithReservedB = jbController().totalTokenSupplyWithReservedTokensOf(revnetB); assertEq( - totalWithReservedA, - totalWithReservedB, - "totalSupplyWithReserved should be equal regardless of distribution" + totalWithReservedA, totalWithReservedB, "totalSupplyWithReserved should be equal regardless of distribution" ); // Cash out half tokens from both revnets. uint256 cashOutCount = tokensA / 2; // Preview cashout for revnet A (undistributed). - (, uint256 reclaimA,,) = jbMultiTerminal().previewCashOutFrom({ - holder: payerA, - projectId: revnetA, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - beneficiary: payable(payerA), - metadata: "" - }); + (, uint256 reclaimA,,) = jbMultiTerminal() + .previewCashOutFrom({ + holder: payerA, + projectId: revnetA, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + beneficiary: payable(payerA), + metadata: "" + }); // Preview cashout for revnet B (distributed). - (, uint256 reclaimB,,) = jbMultiTerminal().previewCashOutFrom({ - holder: payerB, - projectId: revnetB, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - beneficiary: payable(payerB), - metadata: "" - }); + (, uint256 reclaimB,,) = jbMultiTerminal() + .previewCashOutFrom({ + holder: payerB, + projectId: revnetB, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + beneficiary: payable(payerB), + metadata: "" + }); // The reclaim amounts should be equal: distributing reserved tokens does not change the totalSupply // used in the bonding curve because `totalTokenSupplyWithReservedTokensOf` includes pending regardless. - assertEq( - reclaimA, - reclaimB, - "reclaim should be equal whether or not reserved tokens are distributed first" - ); + assertEq(reclaimA, reclaimB, "reclaim should be equal whether or not reserved tokens are distributed first"); // Document the values. emit log_named_uint("Reclaim A (undistributed reserved)", reclaimA); @@ -233,14 +231,15 @@ contract ReservedInflationForkTest is EcosystemForkTest { // Preview cash out to capture the totalSupply used in the bonding curve calculation. uint256 cashOutCount = payerTokens / 2; - (, uint256 reclaimAmount,,) = jbMultiTerminal().previewCashOutFrom({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - beneficiary: payable(PAYER), - metadata: "" - }); + (, uint256 reclaimAmount,,) = jbMultiTerminal() + .previewCashOutFrom({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + beneficiary: payable(PAYER), + metadata: "" + }); // Manually compute what the bonding curve should return using totalSupplyWithReserved. uint256 surplus = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN); @@ -272,7 +271,8 @@ contract ReservedInflationForkTest is EcosystemForkTest { // then uses for the final bonding curve computation. // When no pool is set, the buyback hook returns context values unchanged. // REVDeployer overrides cashOutCount to nonFeeCashOutCount and sets totalSupply. - // The terminal store then computes: reclaimAmount = cashOutFrom(surplus, nonFeeCashOutCount, totalSupply, taxRate) + // The terminal store then computes: reclaimAmount = cashOutFrom(surplus, nonFeeCashOutCount, totalSupply, + // taxRate) assertEq( reclaimAmount, expectedPostFeeReclaim, @@ -285,15 +285,16 @@ contract ReservedInflationForkTest is EcosystemForkTest { // So: actualReclaim = previewReclaim - feeAmountFrom(previewReclaim, FEE) uint256 payerEthBefore = PAYER.balance; vm.prank(PAYER); - uint256 actualReclaim = jbMultiTerminal().cashOutTokensOf({ - holder: PAYER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(PAYER), - metadata: "" - }); + uint256 actualReclaim = jbMultiTerminal() + .cashOutTokensOf({ + holder: PAYER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(PAYER), + metadata: "" + }); // The terminal applies a 2.5% fee on the reclaimAmount when cashOutTaxRate != 0. uint256 terminalFee = JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: 25}); diff --git a/test/fork/SuckerBuybackFork.t.sol b/test/fork/SuckerBuybackFork.t.sol index bbd56a5..57ca7d2 100644 --- a/test/fork/SuckerBuybackFork.t.sol +++ b/test/fork/SuckerBuybackFork.t.sol @@ -15,15 +15,10 @@ contract SuckerBuybackForkTest is EcosystemForkTest { /// @notice Deploy a single-stage revnet with buyback hook active and a meaningful cashOutTaxRate. /// No pool setup — the buyback hook is registered but pre-AMM (no liquidity). - function _deployRevnetForSuckerTest(uint16 cashOutTaxRate) - internal - returns (uint256 revnetId) - { + function _deployRevnetForSuckerTest(uint16 cashOutTaxRate) internal returns (uint256 revnetId) { JBAccountingContext[] memory acc = new JBAccountingContext[](1); acc[0] = JBAccountingContext({ - token: JBConstants.NATIVE_TOKEN, - decimals: 18, - currency: uint32(uint160(JBConstants.NATIVE_TOKEN)) + token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)) }); JBTerminalConfig[] memory tc = new JBTerminalConfig[](1); tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc}); @@ -59,15 +54,11 @@ contract SuckerBuybackForkTest is EcosystemForkTest { }); REVSuckerDeploymentConfig memory sdc = REVSuckerDeploymentConfig({ - deployerConfigurations: new JBSuckerDeployerConfig[](0), - salt: keccak256(abi.encodePacked("SUCKER_TEST")) + deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("SUCKER_TEST")) }); (revnetId,) = REV_DEPLOYER.deployFor({ - revnetId: 0, - configuration: cfg, - terminalConfigurations: tc, - suckerDeploymentConfiguration: sdc + revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc }); } @@ -116,15 +107,16 @@ contract SuckerBuybackForkTest is EcosystemForkTest { // Sucker cashes out all tokens. vm.prank(MOCK_SUCKER); - uint256 reclaimAmount = jbMultiTerminal().cashOutTokensOf({ - holder: MOCK_SUCKER, - projectId: revnetId, - cashOutCount: suckerTokens, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(MOCK_SUCKER), - metadata: "" - }); + uint256 reclaimAmount = jbMultiTerminal() + .cashOutTokensOf({ + holder: MOCK_SUCKER, + projectId: revnetId, + cashOutCount: suckerTokens, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(MOCK_SUCKER), + metadata: "" + }); // Sucker should receive ETH. assertGt(reclaimAmount, 0, "sucker should reclaim ETH"); @@ -175,37 +167,37 @@ contract SuckerBuybackForkTest is EcosystemForkTest { // --- Non-sucker cashes out first --- vm.prank(NON_SUCKER); - uint256 nonSuckerReclaim = jbMultiTerminal().cashOutTokensOf({ - holder: NON_SUCKER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(NON_SUCKER), - metadata: "" - }); + uint256 nonSuckerReclaim = jbMultiTerminal() + .cashOutTokensOf({ + holder: NON_SUCKER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(NON_SUCKER), + metadata: "" + }); assertGt(nonSuckerReclaim, 0, "non-sucker should reclaim some ETH"); // Fee project balance should increase from non-sucker cashout. uint256 feeBalanceAfterNonSucker = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN); assertGt( - feeBalanceAfterNonSucker, - feeBalanceBefore, - "fee project balance should increase from non-sucker cashout" + feeBalanceAfterNonSucker, feeBalanceBefore, "fee project balance should increase from non-sucker cashout" ); // --- Sucker cashes out second --- vm.prank(MOCK_SUCKER); - uint256 suckerReclaim = jbMultiTerminal().cashOutTokensOf({ - holder: MOCK_SUCKER, - projectId: revnetId, - cashOutCount: cashOutCount, - tokenToReclaim: JBConstants.NATIVE_TOKEN, - minTokensReclaimed: 0, - beneficiary: payable(MOCK_SUCKER), - metadata: "" - }); + uint256 suckerReclaim = jbMultiTerminal() + .cashOutTokensOf({ + holder: MOCK_SUCKER, + projectId: revnetId, + cashOutCount: cashOutCount, + tokenToReclaim: JBConstants.NATIVE_TOKEN, + minTokensReclaimed: 0, + beneficiary: payable(MOCK_SUCKER), + metadata: "" + }); assertGt(suckerReclaim, 0, "sucker should reclaim ETH");