T09 · Insecure Skill Coding Practices
- Location
contracts/artifacts/build-info/a5d75f9256251cc8c46c7f853791cdf4.json:1- Finding
Repeatable Harvest Claims Can Drain Vault Underlying Assets
- Content
View full analysis
Vulnerability Details
File Location:
contracts/artifacts/build-info/a5d75f9256251cc8c46c7f853791cdf4.json:1
Vulnerability Type: Missing reward-claim accounting and unbacked yield payment
Risk Level: HighThe affected Solidity source is embedded in the Hardhat build-information artifact. The artifact is minified onto one line.
solidity /** * @dev Harvest yields without reinvesting * In a real implementation, this would claim rewards from the underlying protocol */ function harvest() external whenNotPaused returns (uint256 yieldAmount) { require(shareBalance[msg.sender] > 0, "No shares to harvest"); // Stub: calculate yield based on shares and mock APR yieldAmount = calculateUserYield(msg.sender); require(yieldAmount > 0, "No yield available"); // Transfer yield to user (in real implementation, this would come from protocol) require( IERC20(underlying).transfer(msg.sender, yieldAmount), "Yield transfer failed" ); accumulatedYield += yieldAmount; // Emit events emit Harvest(msg.sender, yieldAmount); emit ExecutionRecorded( vaultId, "harvest", msg.sender, yieldAmount, 0, block.timestamp ); emit ActionExecuted( vaultId, "harvest", msg.sender, yieldAmount, true, "Harvest successful" ); return yieldAmount; } /** * @dev Calculate yield for a user (stub: 10% annual, daily accrual) */ function calculateUserYield(address user) public view returns (uint256) { if (shareBalance[user] == 0) { return 0; } uint256 userAssets = calculateAssetsFromShares(shareBalance[user]); // Stub: simple calculation (0.027% daily = ~10% APY) return (userAssets * 27) / 100000; }Technical Analysis
calculateUserYield()derives ...[truncated 2737 chars]- Remediation
View remediation
Remediation Suggestions
-
Replace the fixed per-call calculation with cumulative reward-index accounting:
- Maintain a global reward-per-share accumulator.
- Record each user's reward debt or last observed index.
- Calculate only rewards accrued since the previous checkpoint.
- Update the checkpoint before transferring tokens.
-
Base distributable rewards on realized assets:
- Claim rewards from the underlying protocol first.
- Measure the actual token balance increase.
- Credit only the verified increase to a reward reserve.
- Never pay nominal rewards directly from depositor principal.
-
Keep accounting synchronized:
- If assets leave the vault, update the corresponding asset or reward-reserve accounting atomically.
- Require the actual underlying balance to cover all accounted liabilities.
- Add explicit solvency checks around harvest and withdrawal operations.
-
Apply checks-effects-interactions:
- Consume the user's accrued reward and update accounting before the external token transfer.
- Add a reentrancy guard to all state-changing functions that transfer tokens.
-
Use a safe ERC-20 transfer library, such as OpenZeppelin
SafeERC20, to support tokens that do not return standard Boolean values. -
Add invariant and adversarial tests proving that:
- A second harvest without new accrual returns zero.
- Repeated harvest calls cannot reduce depositor principal.
- The actual token balance remains sufficient to satisfy accounted assets.
- Total successful claims cannot exceed realized rewards.
- Multiple users receive only their proportional accrued rewards.
-
Treat the current contract as a non-production stub and prevent deployment or funding until the reward model receives an independent smart-contract audit.
-
