Skip to content

[VPD-808] Add internal cash to prevent donation attack#664

Merged
fred-venus merged 19 commits intodevelopfrom
feat/VPD-808
Mar 20, 2026
Merged

[VPD-808] Add internal cash to prevent donation attack#664
fred-venus merged 19 commits intodevelopfrom
feat/VPD-808

Conversation

@Debugger022
Copy link
Contributor

@Debugger022 Debugger022 commented Mar 16, 2026

Summary

  • Introduce internalCash state variable in VBep20 to track token balances internally, replacing direct balanceOf calls in getCashPrior() — this mitigates donation/direct-transfer attacks that inflate exchange rates.
  • Add sweepTokenAndSync(transferAmount) admin function — pass 0 for one-time migration sync after upgrade, or an amount to recover excess tokens before syncing.
  • Update __gap from 46 to 45 to account for the new storage variable.

Test plan

  • Updated unit tests for reservesTest, flashLoan, and EvilXToken to account for internalCash tracking.
  • Updated test harnesses (VBep20Harness, VBep20MockDelegate, EvilXToken) with internalCash support.
  • Consolidated fork tests into donationAttack.ts — validates that all BSC mainnet core pool markets are vulnerable before upgrade and immune after, including mint/redeem operations post-upgrade.
  • Added vTokenStorageChecks.ts — validates storage layout and __gap accounting for the new internalCash slot.
  • Updated sweepTokenAndSyncCash.ts unit tests for the sweepTokenAndSync function.

Summary by CodeRabbit

  • New Features

    • Internal on-token cash tracking with a view and admin sweep/sync operation that can transfer excess and emit sync/sweep events
    • Test harness hooks to set internal cash for testing
  • Bug Fixes

    • Changes to token transfer accounting to keep internal cash consistent and block donation-based exchange-rate manipulation
  • Tests

    • Extensive unit and fork tests validating sync, sweep, upgrade, and attack-resistance scenarios

@Debugger022 Debugger022 marked this pull request as draft March 16, 2026 06:49
…messages and additional markets

- Add descriptive error messages to all assertions for clearer failure diagnostics
- Add missing markets (vTUSDOLD, vTRXOLD, vBCH, vLUNA, vUNI, vUST, vWBETH)
- Use expect().to.not.be.reverted for accrueInterest assertions
- Log fork block number in before() hook for reproducibility
Copy link
Contributor

@fred-venus fred-venus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Rewrite sweepToken using inline assembly to fit within EIP-170 24KB
contract size limit. syncCash remains in high-level Solidity.
sweepToken uses ACM for access control via raw staticcall.
@GitGuru7 GitGuru7 self-requested a review March 16, 2026 08:53
Copy link
Contributor

@GitGuru7 GitGuru7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementation lgtm

The token parameter was always required to equal underlying, making it
unnecessary. Using underlying directly reduces contract size and removes
a superfluous check. Updated TokenSwept event accordingly.
The sweepToken assembly used hardcoded selector 0x7e131436 which does
not match isAllowedToCall(address,string). The correct selector is
0x18c5e8ab.
The transfer calldata construction writes to memory offset 0x24-0x43
via mstore(0x24, excess), which partially overwrites the Solidity free
memory pointer at 0x40. This causes the subsequent emit to use a
corrupted memory pointer and revert. Restore it from ptr at the end.
Add unit tests for sweepToken() and syncCash() covering access control,
event emission, excess recovery, idempotency, and ACM denial. Uses
delegator proxy pattern to stay under EVM contract size limit.

Also add vXAUM (0x92e6Ea74a1A3047DabF4186405a21c7D63a0612A) to the
MARKET_PROXIES arrays in both fork test files.
Use accurate listed-market data from Comptroller (43 markets),
fix exchange rate drift by using accrueInterest + exchangeRateStored,
and restructure test output into clean logical groupings.
@Debugger022 Debugger022 self-assigned this Mar 16, 2026
Consolidates two separate admin functions into a single
sweepTokenAndSync(transferAmount) that optionally transfers excess
tokens then syncs internalCash. Replaces hand-rolled assembly with
Solidity using safeTransfer, simplifying auditing and maintenance.
Merge internalCashUpgrade tests into donationAttack fork suite that
demonstrates the attack succeeding before upgrade and failing after.
Update unit tests for the renamed sweepTokenAndSync function and add
vToken storage layout checks.
@Debugger022 Debugger022 marked this pull request as ready for review March 16, 2026 14:54
@fred-venus
Copy link
Contributor

@claude review

@claude
Copy link

claude bot commented Mar 17, 2026

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit in Settings → Usage.

Once credits are available, comment @claude review on this pull request to trigger a review.

@fred-venus
Copy link
Contributor

@claude review

Debugger022 and others added 3 commits March 17, 2026 18:42
getCashPrior, doTransferIn, and doTransferOut now use internalCash
instead of balanceOf. Update their natspec to document this behavior
and remove stale comments about excluding msg.value.
New implementation with internalCash tracking and
sweepTokenAndSync, deployed and verified on BscScan.
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found, but this changes the core exchange rate calculation (getCashPrior) and storage layout for all mainnet markets — a human should verify the upgrade path and migration strategy before merging.

Extended reasoning...

Overview

This PR introduces an internalCash state variable in VBep20 that replaces direct balanceOf() calls in getCashPrior(), mitigating donation/direct-transfer attacks that can inflate exchange rates. It also adds a sweepTokenAndSync(transferAmount) admin function for post-upgrade migration (pass 0 to sync) and excess token recovery. The __gap is correctly reduced from 46 to 45. Test harnesses, unit tests, and comprehensive fork tests covering 40+ BSC mainnet core pool markets are included.

Security risks

The internalCash pattern is well-established in DeFi (Compound V3, etc.) and the implementation looks correct: doTransferIn increments, doTransferOut decrements before transfer, sweepTokenAndSync is admin-only with require(msg.sender == admin). Flash loan interactions are sound — _getCashPriorWithFlashLoan() adds flashLoanAmount back to getCashPrior(), maintaining correct exchange rate during active loans. No injection, auth bypass, or data exposure risks identified.

Level of scrutiny

This PR touches the most critical code path in a lending protocol — how cash is tracked and exchange rates are computed. A subtle bug here could cause incorrect exchange rates, stuck funds, or exploitable conditions across all 40+ production markets. The storage layout change and required governance-driven migration (_setImplementation + sweepTokenAndSync(0)) add deployment risk. This demands thorough human review of both the code and the deployment/migration plan.

Other factors

The test coverage is impressive: fork tests validate before/after upgrade behavior on all real mainnet markets, storage layout checks verify no slot collisions, and unit tests cover sweep/sync edge cases. Reviewer fred-venus has been actively engaged and provided feedback that was addressed (merging syncCash + sweepToken). However, the overall scope — modifying core VToken logic, storage layout, and requiring coordinated governance upgrades — makes this unsuitable for automated approval.

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

@Debugger022: Sure! I'll review the PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • audits/177_Venus VBep20 Donation Patch Final.pdf is excluded by !**/*.pdf
  • audits/178_CertiK-REP-final-20260320T090021Z.pdf is excluded by !**/*.pdf

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 112b259d-9633-4ce8-9d27-c3c8bedff148

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds internal cash accounting to VBep20 tokens (new internalCash state and events), changes cash transfer functions to update/use this internal ledger, and adds an admin-only sweepTokenAndSync(uint256) to optionally transfer excess tokens and sync internalCash. Tests and harnesses updated to set/verify internalCash.

Changes

Cohort / File(s) Summary
Core VToken changes
contracts/Tokens/VTokens/VBep20.sol, contracts/Tokens/VTokens/VTokenInterfaces.sol
Introduce internalCash state and events CashSynced/TokenSwept; adjust storage gap; switch getCashPrior, doTransferIn, doTransferOut to use/update internalCash; add admin sweepTokenAndSync(uint256).
Test harnesses & mocks
contracts/test/VBep20Harness.sol, contracts/test/VBep20MockDelegate.sol, contracts/test/EvilXToken.sol
Add harnessSetInternalCash(uint256) setters and update mock transfer logic to track internalCash for tests.
Deployment metadata
deployments/bscmainnet.json, deployments/bscmainnet_addresses.json
Update VBep20Delegate address and ABI/metadata to include internalCash() view, sweepTokenAndSync(uint256), and new events.
Existing test updates
tests/hardhat/Comptroller/Diamond/flashLoan.ts, tests/hardhat/EvilXToken.ts, tests/hardhat/VToken/reservesTest.ts
Initialize internalCash in test setups via harness calls so vToken internal ledger matches underlying balances.
New fork/integration tests
tests/hardhat/Fork/donationAttack.ts, tests/hardhat/Fork/vTokenStorageChecks.ts
Add mainnet-fork suites validating upgrade flow, storage-layout preservation, donation-attack resistance, sweepTokenAndSync behavior, and various market operations post-upgrade.
Sweep-specific tests
tests/hardhat/VToken/sweepTokenAndSyncCash.ts
New focused Smock/Hardhat tests covering sweepTokenAndSync idempotence, access control, edge cases (zero/exact/excess), and emitted events.

Sequence Diagram(s)

sequenceDiagram
    participant Admin
    participant VToken as VToken/Proxy
    participant ERC20 as UnderlyingToken

    Admin->>VToken: sweepTokenAndSync(transferAmount)
    activate VToken

    alt transferAmount > 0
        VToken->>ERC20: transfer(Admin, transferAmount)
        ERC20-->>VToken: success
        VToken->>VToken: emit TokenSwept(Admin, transferAmount)
    end

    VToken->>ERC20: balanceOf(address(this))
    ERC20-->>VToken: actualBalance

    VToken->>VToken: old = internalCash
    VToken->>VToken: internalCash = actualBalance
    VToken->>VToken: emit CashSynced(old, actualBalance)

    deactivate VToken
Loading
sequenceDiagram
    participant Donor
    participant ERC20 as UnderlyingToken
    participant VToken as VToken (post-upgrade)

    Donor->>ERC20: transfer(vTokenAddress, donatedAmount)
    ERC20-->>Donor: success

    Donor->>VToken: getCash()
    VToken-->>Donor: return internalCash

    note over ERC20,VToken: ERC20.balanceOf(vToken) = internalCash + donatedAmount\nVToken.internalCash unchanged — donation ignored by accounting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hop in code to mark the change,
I tuck cash safe where numbers range,
Sweep a gift, then sync what's true,
Donations land — but bookkeeping knew,
Small paws tidy ledgers new.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a clear summary of changes and test plan, but the required checklist items are not completed or addressed. Complete the checklist by checking off applicable items or explicitly noting why items are not applicable (e.g., mark documentation and testing items as complete if applicable).
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: introducing an internal cash mechanism to prevent donation attacks on VBep20 tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/VPD-808
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@Debugger022
Copy link
Contributor Author

@coderabbitai review this PR

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (9)
contracts/test/VBep20MockDelegate.sol (1)

3-7: Remove duplicate IERC20 import.

IERC20 is imported twice (lines 3 and 7). While this doesn't cause a compilation error, it's unnecessary clutter.

🧹 Proposed fix
 pragma solidity 0.8.25;
 
 import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
 
 import { ComptrollerInterface } from "../Comptroller/ComptrollerInterface.sol";
-import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import { VToken } from "../Tokens/VTokens/VToken.sol";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/test/VBep20MockDelegate.sol` around lines 3 - 7, The imports in
VBep20MockDelegate.sol include IERC20 twice; remove the redundant import of
IERC20 so it's imported only once (keep the single IERC20 import alongside
SafeERC20 and ComptrollerInterface), updating the import block that references
IERC20 and SafeERC20 to eliminate the duplicate line.
contracts/Tokens/VTokens/VBep20.sol (1)

259-270: Consider integrating sweepTokenAndSync with Access Control Manager for consistency.

The sweepTokenAndSync function uses a direct require(msg.sender == admin) check. However, other admin functions in the VToken codebase use the Access Control Manager pattern via ensureAllowed() (e.g., _setReserveFactor, _reduceReserves, setFlashLoanEnabled). Aligning this function with the ACM pattern would provide consistency and enable granular permission control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/Tokens/VTokens/VBep20.sol` around lines 259 - 270, The direct admin
check in sweepTokenAndSync should be replaced with the Access Control Manager
call to match other admin flows: remove require(msg.sender == admin) and invoke
ensureAllowed(...) at the start of sweepTokenAndSync (using the same
role/permission identifier used by other admin functions, e.g., the ADMIN_ROLE
or the same permission string used by _setReserveFactor/_reduceReserves), then
proceed with the existing transfer and cash-sync logic so behavior and events
(TokenSwept, CashSynced) remain unchanged.
tests/hardhat/Fork/vTokenStorageChecks.ts (3)

69-99: Code duplication: findBalanceSlot and setTokenBalance are duplicated in donationAttack.ts.

Both findBalanceSlot and setTokenBalance helper functions are identical to those in tests/hardhat/Fork/donationAttack.ts. Consider extracting them to tests/hardhat/Fork/utils.ts for reuse and maintainability.

Suggested refactor

Add to tests/hardhat/Fork/utils.ts:

export async function findBalanceSlot(tokenAddress: string): Promise<number | null> {
  // ... implementation
}

export async function setTokenBalance(tokenAddress: string, account: string, amount: BigNumber, slot: number) {
  // ... implementation
}

Then import in both test files:

-import { FORK_MAINNET, forking, initMainnetUser } from "./utils";
+import { FORK_MAINNET, forking, initMainnetUser, findBalanceSlot, setTokenBalance } from "./utils";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/vTokenStorageChecks.ts` around lines 69 - 99, The
findBalanceSlot and setTokenBalance helpers are duplicated; extract both
functions (findBalanceSlot and setTokenBalance) into a shared module (e.g.,
tests/hardhat/Fork/utils.ts) as exported functions, update both tests
(vTokenStorageChecks.ts and donationAttack.ts) to import these functions instead
of defining them locally, and ensure the new module exports the same signatures
(Promise<number|null> for findBalanceSlot and async
setTokenBalance(tokenAddress: string, account: string, amount: BigNumber, slot:
number)) so existing calls continue to work.

182-186: Consider logging full error for debugging purposes.

The error message is truncated to 80 characters which may hide important diagnostic information. Consider logging the full error or at minimum the error type.

Suggested improvement
           } catch (e: any) {
-            console.log(`        Failed to upgrade ${market.name}: ${e.message?.slice(0, 80)}`);
+            console.log(`        Failed to upgrade ${market.name}: ${e.message || e}`);
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/vTokenStorageChecks.ts` around lines 182 - 186, The catch
block that currently logs a truncated error message (catch (e: any) {
console.log(`        Failed to upgrade ${market.name}: ${e.message?.slice(0,
80)}`); }) should be changed to log the full error and type for debugging:
replace the truncated e.message with either e.stack (preferred) or a combination
of e.name and e.message, and use console.error instead of console.log to
indicate failure; update the log to something like: console.error(`Failed to
upgrade ${market.name}:`, e) or console.error(`Failed to upgrade ${market.name}:
${e.name} - ${e.message}`, e.stack) so full diagnostic data is preserved.

10-10: Unused import: IAccessControlManagerV5__factory.

This import is not used anywhere in the file and can be removed.

Remove unused import
 import {
   ERC20__factory,
-  IAccessControlManagerV5__factory,
   VBep20Delegate__factory,
   VBep20Delegator__factory,
 } from "../../../typechain";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/vTokenStorageChecks.ts` at line 10, Remove the unused
import IAccessControlManagerV5__factory from the imports list in the file (it is
declared but never referenced); locate the import statement that includes
IAccessControlManagerV5__factory and delete that symbol (or the entire import
line if it contains only that factory) so there are no unused imports remaining.
tests/hardhat/Fork/donationAttack.ts (2)

228-232: Unusual assertion pattern: .to.be.rejectedWith("").

Using .to.be.rejectedWith("") with an empty string is unusual and may not work as intended. Consider using .to.be.reverted for consistency with other tests in this PR, or specify the expected error message.

Suggested fix
         it("reverts for non-admin callers", async () => {
           const [, randomUser] = await ethers.getSigners();
           const vToken = await ethers.getContractAt("VBep20Delegate", upgradedMarkets[0].proxy, randomUser);
-          await expect(vToken.sweepTokenAndSync(0)).to.be.rejectedWith("");
+          await expect(vToken.sweepTokenAndSync(0)).to.be.reverted;
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/donationAttack.ts` around lines 228 - 232, The test uses
an odd assertion `.to.be.rejectedWith("")` which is unreliable; update the
assertion for the call to VBep20Delegate.sweepTokenAndSync invoked on vToken
(created with ethers.getContractAt and randomUser) to use the proper
Hardhat/Chai matcher — either `.to.be.reverted()` for a generic revert or
`.to.be.revertedWith("<expected revert message>")` if a specific revert reason
is known — so replace the `.to.be.rejectedWith("")` on the
expect(vToken.sweepTokenAndSync(0)) line accordingly.

321-353: Early return exits after first successful market.

The return statement at line 351 causes the test to exit after the first market is successfully verified. This is acceptable for a "happy path" sanity check, but be aware the test title suggests it tests all markets while it only tests one. Consider clarifying the test name if this is intentional.

Optional: Clarify test name
-          it("mint increases internalCash", async () => {
+          it("mint increases internalCash (first compatible market)", async () => {

Same applies to the "redeem decreases internalCash" test at line 355.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/donationAttack.ts` around lines 321 - 353, The test "mint
increases internalCash" currently returns after the first successful market due
to the stray return inside the loop, so it only verifies one market; either
remove that return to let the for-loop continue validating all entries in
upgradedMarkets (keeping the try/catch so supply-cap or paused markets are
skipped) or, if only a single-success check was intended, rename the test/title
to reflect that behavior; apply the same change to the analogous "redeem
decreases internalCash" test so both tests' behavior and names match intent,
referencing the test block containing upgradedMarkets, vToken.mint, and the
internalCashBefore/internalCashAfter assertions.
tests/hardhat/VToken/sweepTokenAndSyncCash.ts (2)

13-14: Consider adding type annotations for signers.

Adding explicit types improves code readability and IDE support.

Suggested type annotations
+import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
+
 describe("VToken", function () {
-  let root, nonAdmin;
+  let root: SignerWithAddress;
+  let nonAdmin: SignerWithAddress;
   let vToken: Contract;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/VToken/sweepTokenAndSyncCash.ts` around lines 13 - 14, The test
declares signer variables root and nonAdmin without types; add explicit signer
types (e.g., SignerWithAddress or ethers.Signer) to those variables to improve
readability and IDE support, and add the corresponding type import (for example
import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers") so
the declarations like root and nonAdmin and any other signer variables have
correct type annotations while leaving vToken typed as Contract.

76-78: Consider asserting specific revert reasons for better test precision.

Using generic .to.be.reverted may pass even if the contract reverts for an unexpected reason. Asserting the specific error message ensures the correct access control check is triggered.

Example improvement
       it("reverts when called by non-admin", async () => {
-        await expect(vToken.connect(nonAdmin).sweepTokenAndSync(0)).to.be.reverted;
+        await expect(vToken.connect(nonAdmin).sweepTokenAndSync(0)).to.be.revertedWith(
+          "only admin can call this function"
+        );
       });

Apply similar changes to lines 127 and 132.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/VToken/sweepTokenAndSyncCash.ts` around lines 76 - 78, Replace
the generic `.to.be.reverted` assertions in the vToken.sweepTokenAndSync tests
with precise revert checks: find the actual admin-access revert string used by
the contract (e.g., the Ownable or custom admin error) and use `await
expect(vToken.connect(nonAdmin).sweepTokenAndSync(0)).to.be.revertedWith("<actual
revert message>")`; apply the same change to the other two assertions mentioned
(lines around 127 and 132) so the tests assert the specific revert reason for
`sweepTokenAndSync`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@contracts/test/VBep20MockDelegate.sol`:
- Around line 3-7: The imports in VBep20MockDelegate.sol include IERC20 twice;
remove the redundant import of IERC20 so it's imported only once (keep the
single IERC20 import alongside SafeERC20 and ComptrollerInterface), updating the
import block that references IERC20 and SafeERC20 to eliminate the duplicate
line.

In `@contracts/Tokens/VTokens/VBep20.sol`:
- Around line 259-270: The direct admin check in sweepTokenAndSync should be
replaced with the Access Control Manager call to match other admin flows: remove
require(msg.sender == admin) and invoke ensureAllowed(...) at the start of
sweepTokenAndSync (using the same role/permission identifier used by other admin
functions, e.g., the ADMIN_ROLE or the same permission string used by
_setReserveFactor/_reduceReserves), then proceed with the existing transfer and
cash-sync logic so behavior and events (TokenSwept, CashSynced) remain
unchanged.

In `@tests/hardhat/Fork/donationAttack.ts`:
- Around line 228-232: The test uses an odd assertion `.to.be.rejectedWith("")`
which is unreliable; update the assertion for the call to
VBep20Delegate.sweepTokenAndSync invoked on vToken (created with
ethers.getContractAt and randomUser) to use the proper Hardhat/Chai matcher —
either `.to.be.reverted()` for a generic revert or
`.to.be.revertedWith("<expected revert message>")` if a specific revert reason
is known — so replace the `.to.be.rejectedWith("")` on the
expect(vToken.sweepTokenAndSync(0)) line accordingly.
- Around line 321-353: The test "mint increases internalCash" currently returns
after the first successful market due to the stray return inside the loop, so it
only verifies one market; either remove that return to let the for-loop continue
validating all entries in upgradedMarkets (keeping the try/catch so supply-cap
or paused markets are skipped) or, if only a single-success check was intended,
rename the test/title to reflect that behavior; apply the same change to the
analogous "redeem decreases internalCash" test so both tests' behavior and names
match intent, referencing the test block containing upgradedMarkets,
vToken.mint, and the internalCashBefore/internalCashAfter assertions.

In `@tests/hardhat/Fork/vTokenStorageChecks.ts`:
- Around line 69-99: The findBalanceSlot and setTokenBalance helpers are
duplicated; extract both functions (findBalanceSlot and setTokenBalance) into a
shared module (e.g., tests/hardhat/Fork/utils.ts) as exported functions, update
both tests (vTokenStorageChecks.ts and donationAttack.ts) to import these
functions instead of defining them locally, and ensure the new module exports
the same signatures (Promise<number|null> for findBalanceSlot and async
setTokenBalance(tokenAddress: string, account: string, amount: BigNumber, slot:
number)) so existing calls continue to work.
- Around line 182-186: The catch block that currently logs a truncated error
message (catch (e: any) { console.log(`        Failed to upgrade ${market.name}:
${e.message?.slice(0, 80)}`); }) should be changed to log the full error and
type for debugging: replace the truncated e.message with either e.stack
(preferred) or a combination of e.name and e.message, and use console.error
instead of console.log to indicate failure; update the log to something like:
console.error(`Failed to upgrade ${market.name}:`, e) or console.error(`Failed
to upgrade ${market.name}: ${e.name} - ${e.message}`, e.stack) so full
diagnostic data is preserved.
- Line 10: Remove the unused import IAccessControlManagerV5__factory from the
imports list in the file (it is declared but never referenced); locate the
import statement that includes IAccessControlManagerV5__factory and delete that
symbol (or the entire import line if it contains only that factory) so there are
no unused imports remaining.

In `@tests/hardhat/VToken/sweepTokenAndSyncCash.ts`:
- Around line 13-14: The test declares signer variables root and nonAdmin
without types; add explicit signer types (e.g., SignerWithAddress or
ethers.Signer) to those variables to improve readability and IDE support, and
add the corresponding type import (for example import type { SignerWithAddress }
from "@nomiclabs/hardhat-ethers/signers") so the declarations like root and
nonAdmin and any other signer variables have correct type annotations while
leaving vToken typed as Contract.
- Around line 76-78: Replace the generic `.to.be.reverted` assertions in the
vToken.sweepTokenAndSync tests with precise revert checks: find the actual
admin-access revert string used by the contract (e.g., the Ownable or custom
admin error) and use `await
expect(vToken.connect(nonAdmin).sweepTokenAndSync(0)).to.be.revertedWith("<actual
revert message>")`; apply the same change to the other two assertions mentioned
(lines around 127 and 132) so the tests assert the specific revert reason for
`sweepTokenAndSync`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b25c26a5-7dc6-4225-b2b7-4b597a2fe594

📥 Commits

Reviewing files that changed from the base of the PR and between 9c6467d and 0a905ab.

📒 Files selected for processing (15)
  • contracts/Tokens/VTokens/VBep20.sol
  • contracts/Tokens/VTokens/VTokenInterfaces.sol
  • contracts/test/EvilXToken.sol
  • contracts/test/VBep20Harness.sol
  • contracts/test/VBep20MockDelegate.sol
  • deployments/bscmainnet.json
  • deployments/bscmainnet/VBep20Delegate.json
  • deployments/bscmainnet/solcInputs/86bcc0e5783c73f34a3d5488fc875748.json
  • deployments/bscmainnet_addresses.json
  • tests/hardhat/Comptroller/Diamond/flashLoan.ts
  • tests/hardhat/EvilXToken.ts
  • tests/hardhat/Fork/donationAttack.ts
  • tests/hardhat/Fork/vTokenStorageChecks.ts
  • tests/hardhat/VToken/reservesTest.ts
  • tests/hardhat/VToken/sweepTokenAndSyncCash.ts

@Debugger022 Debugger022 changed the title Feat/vpd 808 [VPD-808] Add internal cash to prevent donation attack Mar 19, 2026
Remove redundant IERC20 import in VBep20MockDelegate and
unused factory imports in vTokenStorageChecks fork test.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/hardhat/Fork/vTokenStorageChecks.ts (1)

67-94: Cache discovered balance slots to reduce fork-test cost/flakiness.

findBalanceSlot is re-run many times for the same underlying token addresses. Caching per token will materially reduce RPC/storage writes and runtime.

Refactor sketch
+const balanceSlotCache = new Map<string, number | null>();
+
+async function getBalanceSlotCached(tokenAddress: string): Promise<number | null> {
+  if (balanceSlotCache.has(tokenAddress)) return balanceSlotCache.get(tokenAddress)!;
+  const slot = await findBalanceSlot(tokenAddress);
+  balanceSlotCache.set(tokenAddress, slot);
+  return slot;
+}

Then replace each findBalanceSlot(market.underlying) call with getBalanceSlotCached(market.underlying).

Also applies to: 272-273, 326-327, 387-388, 445-446, 506-507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hardhat/Fork/vTokenStorageChecks.ts` around lines 67 - 94,
findBalanceSlot is being called repeatedly for the same token addresses which
causes extra RPC/storage writes and flakiness; add a simple in-memory cache
(e.g., a Map<string, number | null>) and implement a wrapper
getBalanceSlotCached(tokenAddress: string) that returns a cached slot if present
or calls findBalanceSlot, stores the result, and returns it; update all call
sites (the existing findBalanceSlot(...) invocations noted in the review) to
call getBalanceSlotCached(...) instead so repeated lookups reuse the cached slot
and avoid redundant setStorageAt/getStorageAt operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/hardhat/Fork/vTokenStorageChecks.ts`:
- Around line 204-210: Add assertions for the captured fields that are missing
in the storage-collision checks: assert that after.interestRateModel equals
before.interestRateModel, after.borrowIndex equals before.borrowIndex, and
after.totalReserves equals before.totalReserves using the same
expect(...).to.equal(...) pattern and `${market.name}: <field>` message; do this
for both places where the snapshot comparisons occur (the blocks that already
check reserveFactorMantissa/totalBorrows/totalSupply/underlying) so the test
fully verifies interestRateModel, borrowIndex, and totalReserves remain
unchanged across the upgrade.
- Around line 237-246: The tests assume upgradedMarkets[0] exists and will throw
a TypeError if the setup upgraded none; add a guard at the start of each
affected test (e.g., the "reverts for non-admin callers" and "can be called
again by admin (re-syncs)" tests) that asserts upgradedMarkets.length > 0 (for
example using expect(upgradedMarkets.length).to.be.gt(0)) or calls this.skip()
when empty, then proceed to use upgradedMarkets[0]; reference the
upgradedMarkets array and the test blocks to locate where to add the check.
- Around line 156-182: The current loop over MARKET_PROXIES swallows upgrade
errors (occurring during snapshotViaProxy,
VBep20Delegator__factory.connect/_setImplementation, or
vToken.sweepTokenAndSync) by logging and continuing, which can mask regressions;
change the logic to collect failures (e.g., push {name, error} into a failures
array) or immediately rethrow instead of silently logging, and after the loop
assert that failures.length === 0 (or throw a consolidated error listing failed
market names and messages) so the setup fails unless failures are explicitly
allowlisted; reference MARKET_PROXIES, snapshotViaProxy,
VBep20Delegator__factory.connect, _setImplementation, vToken.sweepTokenAndSync,
and upgradedMarkets when making the change.

---

Nitpick comments:
In `@tests/hardhat/Fork/vTokenStorageChecks.ts`:
- Around line 67-94: findBalanceSlot is being called repeatedly for the same
token addresses which causes extra RPC/storage writes and flakiness; add a
simple in-memory cache (e.g., a Map<string, number | null>) and implement a
wrapper getBalanceSlotCached(tokenAddress: string) that returns a cached slot if
present or calls findBalanceSlot, stores the result, and returns it; update all
call sites (the existing findBalanceSlot(...) invocations noted in the review)
to call getBalanceSlotCached(...) instead so repeated lookups reuse the cached
slot and avoid redundant setStorageAt/getStorageAt operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d79a530b-b671-47a9-a4f2-b41ea59a3869

📥 Commits

Reviewing files that changed from the base of the PR and between 0a905ab and 5c311b0.

📒 Files selected for processing (2)
  • contracts/test/VBep20MockDelegate.sol
  • tests/hardhat/Fork/vTokenStorageChecks.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/test/VBep20MockDelegate.sol

Move the upgradedMarkets length guard into the before() hook so
it halts the entire suite on failure. Add interestRateModel and
borrowIndex assertions that aren't covered by the exchange rate check.
@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.Admin 88% 41%
contracts.Comptroller 100% 90%
contracts.Comptroller.Diamond 95% 61%
contracts.Comptroller.Diamond.facets 82% 69%
contracts.Comptroller.Diamond.interfaces 100% 100%
contracts.Comptroller.Types 100% 100%
contracts.Comptroller.legacy 100% 100%
contracts.Comptroller.legacy.Diamond 0% 0%
contracts.Comptroller.legacy.Diamond.facets 0% 0%
contracts.Comptroller.legacy.Diamond.interfaces 100% 100%
contracts.DelegateBorrowers 100% 89%
contracts.FlashLoan.interfaces 100% 100%
contracts.Governance 68% 45%
contracts.InterestRateModels 74% 59%
contracts.Lens 41% 34%
contracts.Liquidator 83% 60%
contracts.Oracle 100% 100%
contracts.PegStability 88% 84%
contracts.Swap 87% 58%
contracts.Swap.interfaces 100% 100%
contracts.Swap.lib 81% 55%
contracts.Tokens 100% 100%
contracts.Tokens.Prime 96% 72%
contracts.Tokens.Prime.Interfaces 100% 100%
contracts.Tokens.Prime.libs 90% 76%
contracts.Tokens.VAI 82% 53%
contracts.Tokens.VRT 20% 9%
contracts.Tokens.VTokens 67% 51%
contracts.Tokens.VTokens.legacy 0% 0%
contracts.Tokens.VTokens.legacy.Utils 0% 0%
contracts.Tokens.XVS 19% 8%
contracts.Tokens.test 100% 100%
contracts.Utils 52% 31%
contracts.VAIVault 50% 45%
contracts.VRTVault 49% 36%
contracts.XVSVault 63% 50%
contracts.external 100% 100%
contracts.lib 89% 71%
Summary 56% (3642 / 6509) 42% (1375 / 3260)

@fred-venus fred-venus merged commit 7c95843 into develop Mar 20, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants