T09 · Insecure Skill Coding Practices
- Location
SKILL.md:156- Finding
Unsafe Vault Accounting Can Corrupt Share and Asset Balances
- Content
View full analysis
Vulnerability Details
File Location:
SKILL.md, lines 156-170
Vulnerability Type: Unsafe integer downcasting and incorrect token receipt accounting
Risk Level: MediumVulnerable Code
solidity mintedShares = totalDeposits == 0 ? amount : (amount * totalShares) / totalDeposits; // Effects before interactions (CEI pattern) totalDeposits += uint128(amount); totalShares += uint128(mintedShares); shares[msg.sender] += mintedShares; // Interaction last asset.safeTransferFrom(msg.sender, address(this), amount);Technical Analysis
The reference vault performs explicit conversions from
uint256touint128without verifying thatamountandmintedSharesfit within 128 bits. Explicit narrowing conversions can truncate high-order bits rather than safely rejecting an out-of-range value. Consequently, the user's fulluint256share balance can diverge from the truncated global accounting values.For example, during the first deposit,
mintedSharesequalsamount. If that amount exceedstype(uint128).max,shares[msg.sender]records the full value whiletotalDepositsandtotalSharesreceive narrowed values. Future share calculations then operate on corrupted global totals.The implementation also assumes that
safeTransferFromdelivers exactlyamounttokens. Fee-on-transfer, deflationary, or otherwise non-standard tokens can transfer fewer tokens than requested. The vault nevertheless credits the requested amount, causing its recorded assets and issued shares to exceed its actual token balance.The associated fuzz test restricts deposits to
uint128values and uses a conventional mock ERC-20 token. It therefore does not exercise oversized deposits or fee-on-transfer behavior.Attack Path
Oversized-deposit path:
- A vault is deployed using the documented reference implementation.
- The configured token permits a deposit whose raw token amount exceeds ` ...[truncated 1624 chars]
- Remediation
View remediation
Remediation Suggestions
-
Use
uint256fortotalDepositsandtotalSharesunless storage packing is demonstrably necessary. -
If
uint128storage is required, use checked conversions that revert when values exceedtype(uint128).max, such as OpenZeppelinSafeCast.toUint128. -
Measure the actual amount received using token balance differences:
solidity uint256 balanceBefore = asset.balanceOf(address(this)); asset.safeTransferFrom(msg.sender, address(this), amount); uint256 received = asset.balanceOf(address(this)) - balanceBefore; -
Calculate and issue shares from
received, not from the caller-suppliedamount. -
Alternatively, explicitly reject fee-on-transfer and rebasing tokens and document that restriction as an enforced invariant.
-
Revert when a nonzero deposit would mint zero shares.
-
Ensure all global totals, individual balances, and actual token balances remain consistent through invariant tests.
-
Add tests for:
type(uint128).maxand values above that boundary.- Fee-on-transfer and deflationary tokens.
- Tokens with unusual decimal configurations.
- Rounding behavior for small deposits.
- The invariant that actual assets are at least recorded liabilities.
- Full deposit and withdrawal sequences under fuzzing.
-
