246 Club engaged Guardian to review the security of their 246 Club’s re-a-token. From the 29th of July to the 5th of August, a team of 2 auditors reviewed the source code in scope.
- Published
- Review window
- July 29 to August 5, 2025
- Language
- Solidity
- Chains
- Sonic
- Sector
- Lending
- 0 Critical
- 4 High
- 7 Medium
- 2 Low
- 6 Informational
Scope
Overview
246 Club engaged Guardian to review the security of their 246 Club’s re-a-token. From the 29th of July to the 5th of August, a team of 2 auditors reviewed the source code in scope.
Findings 19
-
H-01 High ReAToken.mint Is Permanently Unusable DoS Resolved
Description
The
ReAToken.mintfunction callsOpenZeppelin’sERC4626pre‑flight checkrequire(shares <maxMint(receiver)).maxMintis implemented as_convertToShares(_maxDeposit(),Math.Rounding.Floor).When the underlying restaking pair is enabled,
_maxDepositreturns the literal maximumuint256. Passing this value into_convertToSharesmakes the library multiply by the currenttotalSupply, which is equal to 1e12 initially due to the+ 10 ** _decimalsOffset()addition (for aUSDC/USDC pair), which inevitably overflows with apanic: arithmetic underflow or overflow (0x11)error.As a consequence every external call to
mint(and any higher‑level flow that relies on it, such as front‑end integrations or automated strategies) reverts even when the vault is perfectly solvent and its supply cap on Aave has room.Recommendation
Given that there is no max. cap if the asset is restakable, consider updating the
ReAToken.maxMintfunction to:function maxMint(address) public view override(ERC4626, IERC4626) returns (uint256) { return _maxDeposit(); }Resolution
246 Club Team: The issue was resolved in commit c596260.
-
H-02 High Deposits To ReAToken Fail If Interest Is 0 DoS Resolved
Description
Every call to
ReAToken._deposit(deposit,mintand theirERC‑4626helpers) executes the sequence:super._deposit(caller, receiver, assets, shares); // share accounting _supplyClub246(assets); // forward aTokens to Diamond(Club246) _accrueInterest(); // ← fatal if no interest is readyInside
_accrueInterestthe vault consults theDiamond(Club246) expectedRestakeAmountin itsViewerFacet:(_, uint256 expectedInterest) = IViewerFacet(DIAMOND246).expectedRestakeAmount(delegationPairAssets, address(this)); bool restakable = IViewerFacet(DIAMOND246).isRestakable(delegationPairAssets); if (restakable || expectedInterest > maxAssetsSuppliableToAave()) return; // no guard for zero uint256 interest = IInterestManagementFacet(DIAMOND246).claimInterest(delegationPairAssets, address(this), address(this));IInterestManagementFacet.claimInterestreverts withZeroInterestwhenexpectedInterestis zero.- Even if that revert were removed, the next line
_supplyAave(interest)would call Aave V3’sIPool.supplywithamount=0, which
would also reverts
Hence any call arriving while
expectedInterest =0fails outright. That situation is not limited to a brand‑new vault:- Two users depositing in the same block see the second transaction revert (interest cannot accrue between calls).
- Two different users would not be able to deposit and withdraw in the same block.
- Any long‑lived vault whose interest has just been swept and who's following deposits occur before another accrue cycle will fail
likewise.
In effect, the contract is only usable in blocks where a positive, claimable interest balance already exists. Because the vault reverts any deposit or mint when there is no immediately claimable interest, it effectively renders the contract unusable whenever the interest buffer drops to zero. Integrations and front-ends that batch multiple operations in the same block, such as Zaps, will fail unpredictably. In practice, in active
ReATokenvaults, users will encounter failed transactions even though the pool is fully funded and restakable.Recommendation
Short‑circuit _accrueInterest when
expectedInterest = 0to prevent the zero‑interest path from invoking eitherclaimInterestor_supplyAave. The most localized patch is to extend the early‑return guard:if (isRestakable || expectedInterest = 0 || expectedInterest > maxAssetsSuppliableToAave()) return;With this change a fresh vault accepts the first deposit and interest will be claimed only once it has accrued to a positive value.
Resolution
246 Club Team: The issue was resolved in commit 32f9666.
-
H-03 High Missing UNDERLYING_ASSET Approval DoS Resolved
Description
The
ReATokenvault's constructor approves the restaking asset (e.g.,aUSDC) to the Aave V3 pool and theDiamond246protocol for transfers during deposits and restakes, but fails to approve the underlying asset (e.g., USDC, retrieved viaIAToken(_delegationPairAssets.asset).UNDERLYING_ASSET_ADDRESS()) to the Aave V3 pool, which is required for supplying claimed interest back to Aave in the compounding process, causing the supply operation to revert due to insufficient allowance and blocking the entire yield compounding loop after the first interest accrual.This issue occurs because interest claimed from the 246 protocol is denominated in the debt asset (which matches the underlying asset for typical stablecoin pairs like
aUSDC/USDC), and the_accrueInterestfunction inReATokenattempts to compound it by calling_supplyAave(interest)(line:AAVE_V3_POOL.supply(UNDERLYING_ASSET, assets, address(this), 0);), which internally requires anERC20approval from the vault to the pool for the underlying asset to execute the supply, but no such approval is set in the constructor (only for theaTokenitself).As a result, once interest is claimed (via
IInterestManagementFacet(DIAMOND246).claimInterest, transferring underlying tokens to the vault), the subsequent supply to Aave fails with anERC20"insufficient allowance" revert, halting compounding and all the underlyingReATokendeposit and withdrawal operations.This vulnerability is triggered in normal operation after the first borrow-generated interest becomes claimable and it persists because the approval is only set once at deployment, with no mechanism to approve the underlying dynamically.
Recommendation
To resolve this approval gap, add an explicit approval for all the
UNDERLYING_ASSETto theAAVE_V3_POOLin theReATokenconstructor using IERC20(UNDERLYING_ASSET).forceApprove(address(AAVE_V3_POOL), type(uint256).max);, ensuring the vault can supply claimed interest without reverts.Resolution
246 Club Team: The issue was resolved in commit f49a780.
-
H-04 High claimInterest Tries To Transfer Yet-To-Be-Paid Interest Logical Error Resolved
Description
InterestManagementFacet.claimInterestpays restakers by executing:SafeTransferLib.safeTransfer(delegationPairAssets.debt, receiver, interest);but the function never first checks whether the Diamond actually holds the required interest amount of the debt‑asset (e.g., USDC).
The contract’s balance is only topped‑up indirectly when borrowers or liquidators transfer debt tokens in the “repay to 246” code paths, or when the DAO manually calls
supplyInterestReserve.If a restaker (or, in practice, any
ReATokenvault action) callsclaimInterestbefore sufficient tokens have arrived, thesafeTransferreverts, bubbling all the way up to the ERC‑4626 call that triggered it (e.g.,deposit,withdraw).Because every
ReATokenoperation begins with an internal_accrueInterest →claimIntereststep, a single shortfall permanently blocks all user deposits and withdrawals for that vault until the debt‑asset balance is replenished.This leaves funds stranded, halts yield generation and exposes users to indefinite lock‑up.
Recommendation
Consider tracking the amount of interest paid through repayments, liquidations or
supplyInterestReservecalls. Cap the transfer to this available balance while carrying any remainder forward.Resolution
246 Club Team: The issue was resolved in commit 12cb728.
-
M-01 Medium ReATokenFactory.createReAToken Can Be Front-run Unexpected Behavior Resolved
Description
The
ReATokenFactorydeploys new vault tokens withCREATE2:function createReAToken( DelegationPairAssets memory delegationPairAssets, string memory name, string memory symbol, bytes32 salt) external returns (IReAToken reAToken){reAToken = IReAToken(address(new ReAToken{ salt: salt }(delegationPairAssets, DIAMOND246, AAVE_V3_PROVIDER, name, symbol)) ); isReAToken[address(reAToken)] = true; emit CreateReAToken( msg.sender, address(reAToken), delegationPairAssets.id(), delegationPairAssets, name, symbol, salt ); }Because the address is deterministic,
hash(0xFF, factory, salt, keccak256(init_code)), anyone who sees the pending transaction in the mempool can relay a twin transaction that uses the exact same salt and constructor arguments while bidding a higher gas price. If their transaction lands first, the vault is deployed to the expected address,isReAToken[address(reAToken)]is set by the attacker’s call and all later attempts with that salt revert with “contract already deployed.”A deployment script that queues follow‑up operations (e.g.
setWhitelist,stakeInitialor external approvals) in the same bundle will then fail mid‑flight, leaving the system in an unexpected state. Although no assets are stolen, the disruption could be enough to halt releases, cause CI/CD pipelines to fail and force manual operator intervention.Recommendation
Derive the salt on‑chain from immutable data that the attacker cannot control, such as
salt =keccak256(abi.encode(msg.sender, delegationPairAssets)). This makes an identical front‑run impossible because the attacker can not becomemsg.senderfor that salt derivation.Resolution
246 Club Team: The issue was resolved in commit c4e33d6.
-
M-02 Medium Potential Inflation Attack In ReAToken Frontrunning Partially resolved
Description
Proof of concept: PoC
The inflation attack vulnerability in the
ReATokenvault enables a malicious actor to frontrun an honest user's deposit by making a minimal initial deposit followed by a large donation of restaking assets on behalf of theReATokenvault directly to theDiamond246protocol, artificially inflating the share price within the 246 restaking system and causing the victim's deposit to mint zero or near-zero vault shares, effectively donating their assets to the attacker who can then withdraw a portion of the inflated value.Although the vault's
totalAssetsrelies onexpectedRestakeAmountfrom theViewerFacet, which queries the 246 protocol's stored position rather than the vault's direct token balance, this does not fully mitigate the attack because the donation targets the 246 protocol itself (where the assets are restaked), manipulating the underlying scaled supply and power calculations that feed into the preview functions, leading to an overstated exchange rate during the victim's deposit.To dissect this, recall that in a standard
ERC4626vault, the attack exploits rounding in share minting by donating to the vault contract to bloattotalAssetswithout increasingtotalSupply. InReAToken,totalAssetscallsexpectedRestakeAmount, which unscales the vault'sscaledSupplyin 246 (stored inrestakers[delegationPairId][vaultAddress].scaledSupply), adds previewed interest and ignores the vault's actualbalanceOf(asset). Deposits inReATokenmint shares using this previewed rate, then restake via_supplyClub246(callingRestakingFacet'srestake), which transfersaTokenstoDiamond246and updatesscaledSupply.Instead of donating to
ReAToken, the attacker donates toDiamond246after a small restake, inflating 246'saTokenbalance. Butrestakeonly updatesscaledSupplyon explicit calls, direct transfers to Diamond increase itsbalanceOf(aToken)but notscaledSupply(as unscaling uses scaled, not balance). In the POC provided, the attacker (user1) begins by supplying 3e18 WETH toAaveto obtainaWETH, then deposits a minimal 1 wei-equivalent to theReATokenvault, minting 1 share and establishing themselves as the initial shareholder.Next, they approve 2e18
aWETHto theDiamond246and callrestakewith 2e18-1 on behalf of theReATokenvault itself, this transfers the large amount ofaWETHdirectly to theDiamond246, which scales it (dividing byAave's liquidity index) and adds it to the vault'sscaledSupplyin 246 (restaker.scaledSupply += scaledAmount.toUint128();), effectively "donating" the assets to inflate the vault's position in the protocol without minting additionalReATokenshares, as the restake call is external and bypasses the vault's minting logic. At this point, the vault'stotalSupplyremains 1 (from the attacker's tiny deposit), but the 246 protocol'sscaledSupplyfor the vault has ballooned to approximately the scaled equivalent of 2e18, causingexpectedRestakeAmountto preview around 2e18 assets.When user2 (the victim) deposits 1e18
aWETH, thepreviewDepositcomputes shares as (1e18 * 1) / 2e18 = 0.5, which floors to 0 due to integer division and downward rounding, resulting in zero shares minted, with the deposit effectively donated to the protocol (transferred toDiamond246during the internal restake call). The logs confirm this: user2'sReATokenbalance remains 0 after the deposit. The attacker then withdraws 1.5e18 from the vault, reclaiming a portion of their initial contribution plus part of the donated value.The POC results show they end up with approximately 2.5e18-1 aWETH (from an initial 3e18 supplied to Aave), indicating a net loss of about 0.5e18 rather than a profit, the victim's 1e18 donation is captured by the protocol as inflated
scaledSupply, but the attacker's single share only entitles them to a fraction of it upon redemption, with the remainder effectively trapped or diluted across the vault's future users. This makes the attack primarily a griefing mechanism: it denies user2 shares and usability without netting the attacker a gain, mostly to disrupt adoption, force users to over-deposit to overcome the inflation.Recommendation
To eliminate this, enforce a minimum decimals offset in
_decimalsOffset(e.g., +6) for precision.Resolution
246 Club Team: The issue was resolved in commit c990d5d.
-
M-03 Medium maxRedeem/maxWithdraw Returns Inaccurate Values Warning Resolved
Description
The
maxRedeemandmaxWithdrawfunctions in theReATokenvault provide previews of the maximum redeemable shares or withdrawable assets for a user, but these values are inaccurately inflated because they rely on theexpectedRestakeAmountfrom theViewerFacet, which includes previewed interest in its calculations even though this interest is not immediately available as it must be realized through future borrower repayments or liquidations before it can be claimed and compounded, leading to an overestimation of available liquidity and causing attempted operations to potentially return less than expected or fail during execution.Specifically,
maxWithdrawcomputes the user's pro-rata assets via_convertToAssets(inherited fromERC4626, usingtotalAssetswhich callsexpectedRestakeAmount) and caps it against a simulatedmaxUnstakeAmount(ormaxUnstakeAmountAfterRestakeif conditions allow), whereexpectedRestakeAmountunscales the principal and adds simulated interest after previewing accrual and distribution, treating the interest as readily claimable despite it being contingent on actual inflows from repayments or liquidations.In reality, interest exists only as virtual accruals in
pool.pendingInterestandpair.totalDebtInterestand theDiamond246contract's balance of the debt asset (from which claims transfer) may not yet hold the corresponding tokens if repayments lag behind accruals.This discrepancy means the previews optimistically assume the interest is already funded and claimable, but when the actual claim occurs in
_accrueInterest, if the protocol lacks the balance, the transfer inclaimInterestwould fail, misaligning with the preview.The actual impact for users of the protocol is overestimated withdrawal limits that cannot be fully realized, resulting in failed or partial redemptions.
Recommendation
To ensure accurate previews, modify the
ViewerFacet'sexpectedRestakeAmountto distinguish between realized (claimable) and unrealized (previewed) interest by adding a separate return value for each, and updateReAToken'smaxRedeemandmaxWithdrawto conservatively use only the realized portion plus safely unstakable principal, while always claiming available interest in_accrueInterestwithout assumptions of full realization.Resolution
246 Club Team: The issue was resolved in commit 12cb728.
-
M-04 Medium Unprotected External Callback Enables Reentrancy Warning Acknowledged
Description
In
RestakingFacet.restakethe contract: 1. Updates all critical accounting (totalScaledSupply,scaledSupply,interestPad…). 2. Then calls an arbitrary hook controlled bymsg.sender:if (data.length > 0) I246RestakeCallback(msg.sender).on246Restake(amount, data);Because this hook is invoked before the actual token transfer (
safeTransferFrom) and without a reentrancy guard, a malicious contract can re-enter any publicly‑accessible 246 function while the vault’s accounting already reflects the new deposit but no tokens have been received.An attacker can, for example, call
restakea second time (inflating their weight) or attempt anunstakethat relies on balances which are not yet backed by real aTokens. Although many secondary calls will revert when the subsequentsafeTransferfinally fails, the pattern opens the door to future logic changes that might make re‑entrancy profitable.This behavior also enables free flash loans. Users can call
restakewith amount set as the whole balance of the contract. Theirrestakerstruct will be populated with the desired amount, but before the tokens are transferred, the facet will give them the execution flow. The user can then callunstake, which will successfully decrease their balance and will send them all of the tokens held in the contract. The user can then do whatever action they wish with the funds before returning them.Recommendation
Move the external callback after a successful
safeTransferFrom, following the Checks‑Effects‑Interactions pattern:// 1. EFFECTS ... accounting updates ... // 2. INTERACTIONS – first pull the tokens SafeTransferLib.safeTransferFrom(delegationPairAssets.asset, msg.sender, address(this), amount); // 3. OPTIONAL CALLBACK if (data.length = 0) { I246RestakeCallback(msg.sender).on246Restake(amount, data); }and protect the function with a
nonReentrantmodifier to forbid nested calls.Resolution
246 Club Team: Acknowledged. This callback pattern is intentional and it allows complex actions like flash loan or flash repay. Also, any reentrancy attempt would revert if the subsequent safeTransferFrom fails.
-
M-05 Medium accrueAndDistributeInterest Calls Could Revert DoS Partially resolved
Description
Proof of concept: PoC
The 246 protocol's
_accrueInterestfunction can revert with an arithmetic overflow error that can be triggered under conditions of prolonged bad debt accumulation or high pool utilization, causing subsequent calls to the accrual mechanism to revert permanently.This effectively bricks core protocol operations such as borrowing, repaying, unstaking, liquidations and interest claims, as they rely on up-to-date interest calculations.
The overflow arises from unchecked multiplications in the Taylor series approximation used for compounding interest when pool utilization exceeds 100%, leading to exponential debt growth that overwhelms
uint256limits within a finite number of accrual steps.In the provided test script, this issue is demonstrated by simulating a borrowing scenario followed by 137 days of daily accruals without repayments or liquidations, allowing utilization to creep above 1 due to unaddressed interest, after which the debt explodes quadratically and triggers the revert on the next accrual attempt.
This revert blocks all future accruals, as the function cannot complete without overflowing and, therefore, leaves the protocol in a totally broken state requiring an upgrade. This scenario is very unlikely, as the position becomes liquidatable way earlier. However, it is something that can easily happen if the pool is paused as liquidations/repayments revert in this case:
function liquidate( PairAssets memory pairAssets, address borrower, uint256 seizedAmount, uint256 repaidShare, bytes calldata data ) external returns (uint256, uint256) { LiquidateVars memory vars; Pool storage pool = _pools[pairAssets.debt]; if (borrower == address(0)) revert ErrorsLib.ZeroAddress(); if (!UtilsLib.exactlyOneZero(seizedAmount, repaidShare)) revert ErrorsLib.InvalidInput(); if (pool.paused) revert ErrorsLib.PoolPaused(); // <---------- ... }Recommendation
To mitigate this vulnerability, introduce a hard cap on the annual borrow rate in the
_borrowRatefunction ofBaseFacet, limiting it to a reasonable maximum such as 5e18 wad (500% APR) after computation but before returning, which prevents the quadratic feedback loop by enforcing linear growth at extreme utilizations and ensures calculations remain withinuint256bounds even under prolonged bad debt scenarios. Additionally, cap utilization inputs to_borrowRateat 2e18 wad (200%) to bound excess calculations.Resolution
246 Club Team: The issue was resolved in commit 5a4797c.
-
M-06 Medium migrateDelegationAsset Is Permissionless Unexpected Behavior Partially resolved
Description
The
migrateDelegationAssetfunction inEmergencyFacetis declared as external without any access modifiers or authorization checks, allowing any external caller to invoke it on any borrower's position.This function migrates the delegation asset (a restaked
aTokenused as collateral in Aave V3) for a specified borrower's position by calculating the current borrowing power, selecting a new delegation asset via a pseudo-random search in_findDelegationAssetAndAmount, delegating the equivalent power from the new asset to the borrower's contract account and undelegating the old asset back to the protocol.The lack of validation, such as checking if
msg.senderis the borrower, an authorized party (e.g., viaisAuthorized[borrower][msg.sender]), or a privileged role (e.g., guardian or dev), exposes all positions to interference.Due to the pseudo-random selection an attacker can time or spam calls to influence selection towards assets with higher utilization ratios, manipulating borrow rates.
Recommendation
To mitigate this, add access controls to restrict calls to authorized parties only. Implement a modifier like
onlyGuardianOrDev(fromACLManagerStorage) for emergency use, and/or add an ownership check such asrequire(msg.sender = borrower || isAuthorized[borrower][msg.sender],"Unauthorized caller");Resolution
246 Club Team: The issue was resolved in commit 991c822.
-
M-07 Medium Wrong Interest Repayment Logic Logical Error Acknowledged
Description
This report is about an issue that was found during the review of a file that's not part of the scope -
BorrowingFacet.sol. Therepay()function allows users to repay their debt and when they do it, Aave debt is favored. This means that until the Aave interest is not fully paid out, none of the repayments will go towards the club interest and all to Aave.The problem is in the first
if statementif (vars.positionScaledDebt = 0) { // repay to 246 pair.totalDebtInterest = pair.totalDebtInterest.zeroFloorSub(amount).toUint128(); if (data.length > 0) I246RepayCallback(msg.sender).on246Repay(amount, data); SafeTransferLib.safeTransferFrom(pairAssets.debt, msg.sender, address(this), 3965002); }It fires when the user has previously paid their full Aave debt which would make their
positionScaledDebtto 0. In the meantime, they will keep accruing Aave debt even though their position is closed because of the shares mechanism. This means that the new Aave debt will be split proportionally between the current user and all the other users.Later, when a full repayment happens and the
ifis fired,positionScaledDebtwill be 0, even though Aave debt was accrued. The wholeamountthen will be subtracted from the club debt. If the club debt is less than the current user'sclub debt + aave debt, the surplus will be still repaid by the user, but it won't be repaid to Aave.Because of this, the Aave positions will never be fully repaid and they will continue accruing debt that cannot be cleared unless a liquidation at the Aave level happens. This will not stop user withdrawals since
position.debtShareswill be 0 and they will be considered healthy.Recommendation
Consider reworking the repayment mechanism.
Resolution
246 Club Team: Acknowledged. The case you described where users with zero scaledDebt still pay interest based on the pair's Aave debt is intentional. One thing to note is that users borrow from us (246 Club), not directly from Aave. It is not considered users with zero scaledDebt socialize others Aave debt.
-
L-01 Low Stale Liquidity Index Usage Warning Resolved
Description
The
maxAssetsSuppliableToAavefunction determines the remaining supply capacity for the underlying asset in Aave V3 by subtracting the current supply from the configured supply cap, but it relies on potentially outdated values from thegetReserveDatacall, specifically theliquidityIndexandaccruedToTreasuryfields, which are not dynamically updated and may reflect stale state at the time of the last reserve interaction rather than the current block, resulting in an underestimation or overestimation of the actual supplyable amount that could lead to failed supplies when nearing the cap or unintended cap breaches if treasury accruals have increased since the last update.Recommendation
To address this, replace the direct usage of
reserveData.liquidityIndexand manual computation of current supply with a call to Aave'sgetReserveNormalizedIncomefunction on the pool contract to fetch the real-time normalized liquidity index, and adjust the current supply calculation accordingly to incorporate up-to-date accruals, ensuring the function always reflects the precise remaining capacity.Resolution
246 Club Team: The issue was resolved in commit 43afad9.
-
L-02 Low Missing DelegationPairAssets Validation Validation Resolved
Description
The
ReATokenFactorycontract permits the unrestricted creation ofReAToken ERC4626vaults for anydelegationPairAssetscomprising a restaking asset and debt asset without verifying whether the pair is properly configured, enabled or active within the underlying 246 protocol.In these vaults, any call to deposit/mint will revert as
_supplyClub246internal call will always fail if thedelegationPairAssetsare not supported.Recommendation
To remediate this deployment validation deficiency, integrate a pre-creation check in
ReATokenFactory'screateReATokenby callingif(IViewerFacet(DIAMOND246).isRestakable(delegationPairAssets)) revert NotSupportedPair(); immediately after input validation.Resolution
246 Club Team: The issue was resolved in commit 6be06e6.
-
I-01 Informational Pool Pausing In 246 Protocol DoS Partially resolved
Description
The pausing mechanism in the 246 protocol's pools, intended as an emergency control to halt risky operations like borrowing or restaking during incidents, inadvertently creates a denial-of-service vulnerability for integrated systems such as the
ReATokenvault, where all deposits and withdrawals become blocked due to the inability to claim and compound accrued interest when a pool is paused.This stems from a strict revert in the
claimInterestfunction of theInterestManagementFacet, which checks the pool's paused state and prevents any interest transfers, freezing the vault's core compounding loop and causing cascading failures in user interactions that rely on up-to-date yield accrual.Pool pausing is managed in
ConfiguratorFacet'spausePool(address debtAsset, bool paused), which setspool.paused = paused. This flag is a boolean toggle, controllable by guardians or devs (via modifiers likeonlyGuardianOrDev), designed to revert mutative functions across facets (e.g.,borrow,repay,restake,unstake,liquidate) to prevent further risk exposure during crises like oracle failures or exploits.The critical point of failure occurs in
InterestManagementFacet'sclaimInterest, which includes:if (pool.paused) revert ErrorsLib.PoolPaused();This check is executed before any accrual, distribution or transfer, meaning that whenever a pool is paused, no interest can be claimed at all. The function proceeds to accrue interest (
_accrueInterest), distribute it (_distributePendingInterest), compute the restaker's claimable amount and transfer the debt asset (e.g., USDC) to the receiver, but the pause revert blocks the entire process.Now, this becomes problematic in the
ReATokenvault, which relies on claiming interest from 246 to compound yields as part of itsERC4626operations.- If restakable and within Aave supply cap conditions are met, it calls
claimInterestto transfer the interest to the vault, supplies it to Aave to mint
more
aTokens, and restakes those to 246 for compounding.- When the pool is paused,
claimInterestreverts withPoolPaused, causing_accrueInterestto fail and propagate the revert to the calling deposit
or withdrawal.
- For deposits: Users cannot add assets, as the compounding step fails, halting new liquidity inflows.
- For withdrawals: Users cannot redeem shares, as the preview and unstake rely on up-to-date accrual, trapping funds in the vault during pauses.
This creates a full DoS: In a paused pool, the
ReATokenvault becomes non-functional for all users, as every mutative operation (deposit,mint,withdraw,redeem) triggers_accrueInterest, which will attempt a claim and revert. Which will also block the withdrawal as theif (pool.paused)revert ErrorsLib.PoolPaused();is also present there.Recommendation
Merely informative. Consider documenting this behaviour so users are aware of this behaviour.
Resolution
246 Club Team: The issue was resolved in commit 4a250b3.
- If restakable and within Aave supply cap conditions are met, it calls
-
I-02 Informational Redundant Import Statement Best Practices Resolved
Description
The codebase contains several redundant import statements, where libraries or symbols are imported multiple times or in ways that duplicate functionality already available through other imports or definitions, such as the double import of the
Mathlibrary inReAToken.soland the potential overlap ofWADconstant imported fromMathLib.solinBaseFacet.soldespite its definition inConstantsLib.sol, which could lead to unnecessary compilation overhead and reduced code clarity without affecting runtime behavior.These redundancies do not introduce runtime vulnerabilities but increase compilation time slightly and make the code harder to maintain by obscuring dependencies.
Recommendation
Consider removing the redundant imports.
Resolution
246 Club Team: The issue was resolved in commit 812f12a.
-
I-03 Informational Missing Global Reentrancy Guard Best Practices Acknowledged
Description
The protocol introduces significant reentrancy risks by incorporating optional callbacks in several key functions, which are triggered when non-empty data is provided and executed via external calls to the
msg.senderbefore completing critical interactions such as token transfers.This design violates the Checks-Effects-Interactions pattern, where external calls should ideally occur last to prevent malicious contracts from reentering the system and exploiting transient states.
Specifically, the callbacks flagged include the
on246RestakeinRestakingFacet.restake(called after updating scaled supplies and interest pads but before transferringaTokensto the diamond), theon246SupplyCollateralinCollateralManagementFacet.supplyCollateral(invoked after increasing position collateral but prior to transferring collateral to the account), theon246RepayinBorrowingFacet.repay(executed following debt reduction and delegation adjustments yet before debt token transfers), and theon246LiquidateinLiquidationFacet.liquidate(triggered after adjusting debt and collateral but preceding debt token transfers).These callbacks, absent any reentrancy guards, allow attackers to reenter functions like
unstake,borrow, orclaimInterestduring the callback, manipulating phantom supplies or health factors before the original transaction's token movements finalize.Recommendation
To mitigate these risks, implement a dedicated reentrancy guard facet using a
nonReentrantmodifier pattern, such as that provided byOpenZeppelin, applied globally to all functions invoking callbacks or handling external calls, ensuring that state modifications and interactions are fully resolved before any callback execution.Resolution
246 Club Team: Acknowledged. This callback pattern is intentional and it allows complex actions like flash loan or flash repay. Also, any reentrancy attempt would revert if the subsequent safeTransferFrom fails.
-
I-04 Informational Pool Utilization Increases After Some Operations Warning Partially resolved
Description
Proof of concept: PoC
The 246 protocol allows certain administrative and emergency operations that inadvertently increase pool utilization by reducing the effective borrowing power without proportionally decreasing debt, which can push utilization above the optimal ratio, inflating borrow rates.
This occurs because borrowing power calculations shift from total scaled supply to total scaled usage when restaking is disabled and coverage operations further decrease usage without impacting debt, both of which shrink the denominator in the utilization formula of
totalDebtdivided bytotalPower, leading to higher utilization.In the provided test script, we can see how the utilization increases from 6.26% to 6.46% once
enableRestaking(delegationPairAssets, false)is called. Finally, we can also see how the utilization raises again whencoverDelegationAssetis called, from 6.46% to 6.91%.Recommendation
Merely informative. Be aware that certain operations, and not exclusively
borrowcalls, do also increases the utilization and therefore the borrowing rates.Resolution
246 Club Team: The issue was resolved in commit bbed4e0.
-
I-05 Informational Lack Of Event Emissions In Setter Functions Best Practices Acknowledged
Description
Throughout the codebase, several setter functions modify critical global state variables (e.g., configuration parameters, ratios and addresses) without emitting corresponding events.
This omission hinders off-chain monitoring, auditing and user notifications, as external observers (e.g., frontends, indexers, or security tools) rely on events to track changes efficiently.
For instance, while some setters like
setConnectoremitSetConnector, others such as internal updates inBaseFacetfor authorization mappings or nonce increments, lack events, making it difficult to detect unauthorized or unexpected modifications.In high-stakes DeFi protocols, this can obscure governance actions, delay incident response and complicate historical state reconstruction.
Recommendation
Ensure all setter functions that alter mutable state emit descriptive events, including old and new values where applicable, to enhance transparency and enable reliable off-chain tracking. Adopt a consistent pattern across facets, such as using a library like
EventsLibfor all emissions.Resolution
246 Club Team: Acknowledged. Could you provide us more specific case where we are lack of? We think authorization, nonce and others are emit events to track its state.
-
I-06 Informational Club246 Interest Impacts Available Power Informational Acknowledged
Description
In
RestakingFacet.unstake(), the following check ensures the pool's borrowing power won't fall below its current debt after the restake is executed:pool.totalDebt > totalPower - unstakeBorrowingPower.Both
totalPowerandunstakeBorrowingpower represent the Aave borrowing power for the particularasset. On the other hand,totalDebtincludes not only the interest accrued by Aave, but by the club as well.Because of that, the calculations will result in less available amount for unstaking even if it's not directly backing the Aave position. The same is true in
ViewerFacet._calculateSafeUnstakeAmount().Recommendation
Acknowledge this finding and reconsider if the available power calculation should be changed.
Resolution
246 Club Team: Acknowledged. Removing this validation in unstake() would cause the following borrow() calls to revert, since the same validation (pool.totalDebt > vars.totalPower) still exists in borrow().
No findings match.
More from 246 Club
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
