diff --git "a/data/contracts.json" "b/data/contracts.json" --- "a/data/contracts.json" +++ "b/data/contracts.json" @@ -4088,5 +4088,3155 @@ "description": "Emitted when bunker mode is disabled" } ] + }, + { + "contract_name": "AToken_Old", + "file_name": "AToken.sol", + "metadata": { + "license": "agpl-3.0", + "solidity_version": "0.6.8", + "description": "Aave ERC20 AToken - interest bearing token for the DLP protocol", + "author": "Aave" + }, + "state_variables": [ + { + "name": "EIP712_REVISION", + "type": "bytes", + "visibility": "public", + "mutability": "constant", + "description": "EIP712 revision identifier" + }, + { + "name": "PERMIT_TYPEHASH", + "type": "bytes32", + "visibility": "public", + "mutability": "constant", + "description": "Typehash for permit function" + }, + { + "name": "UINT_MAX_VALUE", + "type": "uint256", + "visibility": "public", + "mutability": "constant", + "description": "Maximum uint256 value" + }, + { + "name": "ATOKEN_REVISION", + "type": "uint256", + "visibility": "public", + "mutability": "constant", + "description": "Revision number for the contract" + }, + { + "name": "UNDERLYING_ASSET_ADDRESS", + "type": "address", + "visibility": "public", + "mutability": "immutable", + "description": "Address of the underlying asset token" + }, + { + "name": "RESERVE_TREASURY_ADDRESS", + "type": "address", + "visibility": "public", + "mutability": "immutable", + "description": "Address of the reserve treasury" + }, + { + "name": "POOL", + "type": "LendingPool", + "visibility": "public", + "mutability": "immutable", + "description": "LendingPool contract reference" + }, + { + "name": "_nonces", + "type": "mapping(address => uint256)", + "visibility": "internal", + "mutability": "", + "description": "Nonces for permit functionality" + }, + { + "name": "DOMAIN_SEPARATOR", + "type": "bytes32", + "visibility": "public", + "mutability": "", + "description": "EIP712 domain separator" + } + ], + "functions": [ + { + "name": "constructor", + "signature": "constructor(LendingPool pool, address underlyingAssetAddress, address reserveTreasuryAddress, string memory tokenName, string memory tokenSymbol, address incentivesController)", + "code": "constructor(LendingPool pool, address underlyingAssetAddress, address reserveTreasuryAddress, string memory tokenName, string memory tokenSymbol, address incentivesController) public IncentivizedERC20(tokenName, tokenSymbol, 18, incentivesController) {\n POOL = pool;\n UNDERLYING_ASSET_ADDRESS = underlyingAssetAddress;\n RESERVE_TREASURY_ADDRESS = reserveTreasuryAddress;\n}", + "comment": "Constructor for AToken, initializes immutable references", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "pool", + "type": "LendingPool", + "description": "LendingPool contract" + }, + { + "name": "underlyingAssetAddress", + "type": "address", + "description": "Underlying asset address" + }, + { + "name": "reserveTreasuryAddress", + "type": "address", + "description": "Treasury address" + }, + { + "name": "tokenName", + "type": "string", + "description": "Token name" + }, + { + "name": "tokenSymbol", + "type": "string", + "description": "Token symbol" + }, + { + "name": "incentivesController", + "type": "address", + "description": "Incentives controller address" + } + ], + "returns": "", + "output_property": "Sets immutable variables POOL, UNDERLYING_ASSET_ADDRESS, RESERVE_TREASURY_ADDRESS. Inherits IncentivizedERC20 initialization. No return value.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getRevision", + "signature": "getRevision() internal virtual override pure returns (uint256)", + "code": "function getRevision() internal virtual override pure returns (uint256) {\n return ATOKEN_REVISION;\n}", + "comment": "Returns the revision number of the contract", + "visibility": "internal", + "modifiers": [], + "parameters": [], + "returns": "uint256 - The revision number", + "output_property": "Returns the constant ATOKEN_REVISION (0x1). Pure function, no state changes.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "initialize", + "signature": "initialize(uint8 underlyingAssetDecimals, string calldata tokenName, string calldata tokenSymbol) external virtual initializer", + "code": "function initialize(uint8 underlyingAssetDecimals, string calldata tokenName, string calldata tokenSymbol) external virtual initializer {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n DOMAIN_SEPARATOR = keccak256(\n abi.encode(\n EIP712_DOMAIN,\n keccak256(bytes(tokenName)),\n keccak256(EIP712_REVISION),\n chainId,\n address(this)\n )\n );\n _setName(tokenName);\n _setSymbol(tokenSymbol);\n _setDecimals(underlyingAssetDecimals);\n}", + "comment": "Initializes the AToken after deployment (used with proxy)", + "visibility": "external", + "modifiers": [ + "initializer" + ], + "parameters": [ + { + "name": "underlyingAssetDecimals", + "type": "uint8", + "description": "Decimals of the underlying asset" + }, + { + "name": "tokenName", + "type": "string", + "description": "Token name" + }, + { + "name": "tokenSymbol", + "type": "string", + "description": "Token symbol" + } + ], + "returns": "", + "output_property": "Sets DOMAIN_SEPARATOR using EIP712, sets token name, symbol, and decimals. Can only be called once due to initializer modifier. No return value.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "burn", + "signature": "burn(address user, address receiverOfUnderlying, uint256 amount, uint256 index) external override onlyLendingPool", + "code": "function burn(address user, address receiverOfUnderlying, uint256 amount, uint256 index) external override onlyLendingPool {\n _burn(user, amount.rayDiv(index));\n IERC20(UNDERLYING_ASSET_ADDRESS).safeTransfer(receiverOfUnderlying, amount);\n emit Transfer(user, address(0), amount);\n emit Burn(msg.sender, receiverOfUnderlying, amount, index);\n}", + "comment": "Burns aTokens and sends underlying to receiver. Only callable by LendingPool.", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User whose aTokens are burned" + }, + { + "name": "receiverOfUnderlying", + "type": "address", + "description": "Address to receive underlying tokens" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount of underlying to transfer (not scaled)" + }, + { + "name": "index", + "type": "uint256", + "description": "Current reserve normalized income index" + } + ], + "returns": "", + "output_property": "Burns scaled aTokens from user (amount.rayDiv(index)), transfers underlying amount to receiver, emits Transfer and Burn events. Reverts if not called by LendingPool. Vulnerable to rounding: if amount.rayDiv(index) rounds down to zero, aTokens are not burned but underlying is transferred.", + "events": [ + "Transfer", + "Burn" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Additive burn (AToken) - Rounding error leads to free withdrawal", + "severity": "Medium", + "description": "Due to rounding in conversions from underlying amount to AToken scaled amount, if the conversion rate (index) is high enough, a user can withdraw a very small amount that results in the system transferring underlying tokens but burning zero ATokens from the user's account. This allows the user to effectively withdraw funds without reducing their AToken balance.", + "mitigation": "Add a check to ensure that the scaled amount to burn is greater than zero before performing the burn and transfer, or avoid transfer when the burned amount is zero." + }, + "property": "The total assets of a user should decrease exactly by the amount of underlying withdrawn. Rounding should not allow a user to receive underlying without a corresponding decrease in AToken balance.", + "property_specification": "Pre-condition: User has AToken balance B, underlying asset balance of AToken contract is sufficient. Operation: burn(user, receiver, amount, index) where amount > 0 but amount.rayDiv(index) == 0 due to rounding. Expected post-condition: User's AToken balance decreases by the scaled amount (which is zero) and underlying amount is transferred, so user's net position decreases by amount of underlying. Actual vulnerability: User's AToken balance does not change (burn of zero), but user receives underlying amount, resulting in a net gain of underlying without any reduction in AToken balance, violating the expected invariant that AToken burn should correspond to underlying transfer." + }, + { + "name": "mint", + "signature": "mint(address user, uint256 amount, uint256 index) external override onlyLendingPool", + "code": "function mint(address user, uint256 amount, uint256 index) external override onlyLendingPool {\n _mint(user, amount.rayDiv(index));\n emit Transfer(address(0), user, amount);\n emit Mint(user, amount, index);\n}", + "comment": "Mints aTokens to user, callable only by LendingPool", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "Recipient of minted aTokens" + }, + { + "name": "amount", + "type": "uint256", + "description": "Underlying amount deposited" + }, + { + "name": "index", + "type": "uint256", + "description": "Current reserve normalized income index" + } + ], + "returns": "", + "output_property": "Mints scaled aTokens (amount.rayDiv(index)) to user, emits Transfer and Mint events. Reverts if not called by LendingPool.", + "events": [ + "Transfer", + "Mint" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "mintToTreasury", + "signature": "mintToTreasury(uint256 amount, uint256 index) external override onlyLendingPool", + "code": "function mintToTreasury(uint256 amount, uint256 index) external override onlyLendingPool {\n _mint(RESERVE_TREASURY_ADDRESS, amount.div(index));\n emit Transfer(address(0), RESERVE_TREASURY_ADDRESS, amount);\n emit Mint(RESERVE_TREASURY_ADDRESS, amount, index);\n}", + "comment": "Mints aTokens to treasury, callable only by LendingPool", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "amount", + "type": "uint256", + "description": "Underlying amount to mint" + }, + { + "name": "index", + "type": "uint256", + "description": "Current reserve normalized income index" + } + ], + "returns": "", + "output_property": "Mints scaled aTokens (amount.div(index)) to treasury address, emits Transfer and Mint events. Reverts if not called by LendingPool.", + "events": [ + "Transfer", + "Mint" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "transferOnLiquidation", + "signature": "transferOnLiquidation(address from, address to, uint256 value) external override onlyLendingPool", + "code": "function transferOnLiquidation(address from, address to, uint256 value) external override onlyLendingPool {\n _transfer(from, to, value, false);\n}", + "comment": "Transfers aTokens during liquidation, callable only by LendingPool", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "from", + "type": "address", + "description": "Sender address" + }, + { + "name": "to", + "type": "address", + "description": "Recipient address" + }, + { + "name": "value", + "type": "uint256", + "description": "Amount to transfer" + } + ], + "returns": "", + "output_property": "Calls internal _transfer with validate=false, skipping transfer validation. Reverts if not called by LendingPool.", + "events": [ + "Transfer", + "BalanceTransfer" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "balanceOf", + "signature": "balanceOf(address user) public override(IncentivizedERC20, IERC20) view returns (uint256)", + "code": "function balanceOf(address user) public override(IncentivizedERC20, IERC20) view returns (uint256) {\n return super.balanceOf(user).rayMul(POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS));\n}", + "comment": "Returns the current balance of user including accrued interest", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256 - Current balance with interest", + "output_property": "Multiplies the scaled balance by the current normalized income from the LendingPool. Reverts if POOL.getReserveNormalizedIncome reverts. Pure view function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "scaledBalanceOf", + "signature": "scaledBalanceOf(address user) external override view returns (uint256)", + "code": "function scaledBalanceOf(address user) external override view returns (uint256) {\n return super.balanceOf(user);\n}", + "comment": "Returns the scaled balance (principal without interest)", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256 - Scaled balance", + "output_property": "Returns the underlying stored balance from IncentivizedERC20. Pure view function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getScaledUserBalanceAndSupply", + "signature": "getScaledUserBalanceAndSupply(address user) external override view returns (uint256, uint256)", + "code": "function getScaledUserBalanceAndSupply(address user) external override view returns (uint256, uint256) {\n return (super.balanceOf(user), super.totalSupply());\n}", + "comment": "Returns scaled user balance and total supply", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256, uint256 - Scaled user balance, scaled total supply", + "output_property": "Returns a tuple of the user's scaled balance and the scaled total supply from IncentivizedERC20.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "totalSupply", + "signature": "totalSupply() public override(IncentivizedERC20, IERC20) view returns (uint256)", + "code": "function totalSupply() public override(IncentivizedERC20, IERC20) view returns (uint256) {\n uint256 currentSupplyScaled = super.totalSupply();\n if (currentSupplyScaled == 0) {\n return 0;\n }\n return currentSupplyScaled.rayMul(POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS));\n}", + "comment": "Returns the total supply including accrued interest", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Current total supply", + "output_property": "Multiplies scaled total supply by normalized income. Returns 0 if scaled supply is 0. Reverts if POOL.getReserveNormalizedIncome reverts.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "scaledTotalSupply", + "signature": "scaledTotalSupply() public virtual override view returns (uint256)", + "code": "function scaledTotalSupply() public virtual override view returns (uint256) {\n return super.totalSupply();\n}", + "comment": "Returns the scaled total supply (principal without interest)", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Scaled total supply", + "output_property": "Returns the total supply from IncentivizedERC20 (scaled value).", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "isTransferAllowed", + "signature": "isTransferAllowed(address user, uint256 amount) public override view returns (bool)", + "code": "function isTransferAllowed(address user, uint256 amount) public override view returns (bool) {\n return POOL.balanceDecreaseAllowed(UNDERLYING_ASSET_ADDRESS, user, amount);\n}", + "comment": "Checks if a transfer is allowed based on health factor", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to transfer" + } + ], + "returns": "bool - True if transfer allowed", + "output_property": "Delegates to LendingPool.balanceDecreaseAllowed. Returns boolean. View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "transferUnderlyingTo", + "signature": "transferUnderlyingTo(address target, uint256 amount) external override onlyLendingPool returns (uint256)", + "code": "function transferUnderlyingTo(address target, uint256 amount) external override onlyLendingPool returns (uint256) {\n IERC20(UNDERLYING_ASSET_ADDRESS).safeTransfer(target, amount);\n return amount;\n}", + "comment": "Transfers underlying asset to target, callable only by LendingPool", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "target", + "type": "address", + "description": "Recipient address" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to transfer" + } + ], + "returns": "uint256 - The transferred amount", + "output_property": "Transfers underlying asset to target, returns amount. Reverts if not called by LendingPool.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "permit", + "signature": "permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external", + "code": "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {\n require(owner != address(0), 'INVALID_OWNER');\n require(block.timestamp <= deadline, 'INVALID_EXPIRATION');\n uint256 currentValidNonce = _nonces[owner];\n bytes32 digest = keccak256(\n abi.encodePacked(\n '\\x19\\x01',\n DOMAIN_SEPARATOR,\n keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline))\n )\n );\n require(owner == ecrecover(digest, v, r, s), 'INVALID_SIGNATURE');\n _nonces[owner] = currentValidNonce.add(1);\n _approve(owner, spender, value);\n}", + "comment": "EIP2612 permit function for gasless approvals", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "owner", + "type": "address", + "description": "Token owner" + }, + { + "name": "spender", + "type": "address", + "description": "Approved spender" + }, + { + "name": "value", + "type": "uint256", + "description": "Approval amount" + }, + { + "name": "deadline", + "type": "uint256", + "description": "Expiration timestamp" + }, + { + "name": "v", + "type": "uint8", + "description": "Signature v parameter" + }, + { + "name": "r", + "type": "bytes32", + "description": "Signature r parameter" + }, + { + "name": "s", + "type": "bytes32", + "description": "Signature s parameter" + } + ], + "returns": "", + "output_property": "Allows a spender to approve on behalf of owner via signature. Validates signature, increments nonce, calls _approve. Reverts if owner is zero, deadline passed, or signature invalid.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_transfer (internal with validate)", + "signature": "_transfer(address from, address to, uint256 amount, bool validate) internal", + "code": "function _transfer(address from, address to, uint256 amount, bool validate) internal {\n if (validate) {\n require(isTransferAllowed(from, amount), Errors.TRANSFER_NOT_ALLOWED);\n }\n uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS);\n super._transfer(from, to, amount.rayDiv(index));\n emit BalanceTransfer(from, to, amount, index);\n}", + "comment": "Internal transfer with optional validation", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "from", + "type": "address", + "description": "Sender" + }, + { + "name": "to", + "type": "address", + "description": "Recipient" + }, + { + "name": "amount", + "type": "uint256", + "description": "Underlying amount (not scaled)" + }, + { + "name": "validate", + "type": "bool", + "description": "Whether to check transfer allowance" + } + ], + "returns": "", + "output_property": "If validate true, checks transfer allowed. Gets normalized income, converts amount to scaled amount, calls super._transfer, emits BalanceTransfer.", + "events": [ + "BalanceTransfer" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_transfer (overloaded)", + "signature": "_transfer(address from, address to, uint256 amount) internal override", + "code": "function _transfer(address from, address to, uint256 amount) internal override {\n _transfer(from, to, amount, true);\n}", + "comment": "Overloaded transfer with validation enabled by default", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "from", + "type": "address", + "description": "Sender" + }, + { + "name": "to", + "type": "address", + "description": "Recipient" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to transfer" + } + ], + "returns": "", + "output_property": "Calls internal _transfer with validate=true. Used for standard ERC20 transfers.", + "events": [ + "Transfer", + "BalanceTransfer" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "receive", + "signature": "receive() external payable", + "code": "receive() external payable {\n revert();\n}", + "comment": "Reject direct ETH transfers", + "visibility": "external", + "modifiers": [], + "parameters": [], + "returns": "", + "output_property": "Always reverts when ETH is sent directly to the contract.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + } + ], + "structs": [], + "modifiers": [ + { + "name": "onlyLendingPool", + "definition": "require(msg.sender == address(POOL), Errors.CALLER_MUST_BE_LENDING_POOL);", + "purpose": "Restricts function access to only the LendingPool contract" + } + ], + "inheritance": [ + "VersionedInitializable", + "IncentivizedERC20", + "IAToken" + ], + "call_graph": { + "constructor": [ + "IncentivizedERC20.constructor()" + ], + "initialize": [ + "_setName()", + "_setSymbol()", + "_setDecimals()" + ], + "burn": [ + "_burn()", + "IERC20.safeTransfer()", + "emit Transfer", + "emit Burn" + ], + "mint": [ + "_mint()", + "emit Transfer", + "emit Mint" + ], + "mintToTreasury": [ + "_mint()", + "emit Transfer", + "emit Mint" + ], + "transferOnLiquidation": [ + "_transfer()" + ], + "balanceOf": [ + "super.balanceOf()", + "POOL.getReserveNormalizedIncome()" + ], + "totalSupply": [ + "super.totalSupply()", + "POOL.getReserveNormalizedIncome()" + ], + "isTransferAllowed": [ + "POOL.balanceDecreaseAllowed()" + ], + "transferUnderlyingTo": [ + "IERC20.safeTransfer()" + ], + "permit": [ + "ecrecover()", + "_approve()" + ], + "_transfer (internal with validate)": [ + "isTransferAllowed()", + "POOL.getReserveNormalizedIncome()", + "super._transfer()", + "emit BalanceTransfer" + ], + "_transfer (overloaded)": [ + "_transfer(,,,true)" + ], + "getRevision": [], + "scaledBalanceOf": [ + "super.balanceOf()" + ], + "getScaledUserBalanceAndSupply": [ + "super.balanceOf()", + "super.totalSupply()" + ], + "scaledTotalSupply": [ + "super.totalSupply()" + ] + }, + "audit_issues": [ + { + "function": "burn", + "issue": "Additive burn (AToken) - Rounding error leads to free withdrawal", + "severity": "Medium", + "description": "Due to rounding in conversions from underlying amount to AToken scaled amount, if the conversion rate (index) is high enough, a user can withdraw a very small amount that results in the system transferring underlying tokens but burning zero ATokens from the user's account. This allows the user to effectively withdraw funds without reducing their AToken balance.", + "status": "Fixed", + "mitigation": "Add a check to ensure that the scaled amount to burn is greater than zero before performing the burn and transfer, or avoid transfer when the burned amount is zero.", + "property": "The total assets of a user should decrease exactly by the amount of underlying withdrawn. Rounding should not allow a user to receive underlying without a corresponding decrease in AToken balance.", + "property_specification": "Pre-condition: User has AToken balance B, underlying asset balance of AToken contract is sufficient. Operation: burn(user, receiver, amount, index) where amount > 0 but amount.rayDiv(index) == 0 due to rounding. Expected post-condition: User's AToken balance decreases by the scaled amount (which is zero) and underlying amount is transferred, so user's net position decreases by amount of underlying. Actual vulnerability: User's AToken balance does not change (burn of zero), but user receives underlying amount, resulting in a net gain of underlying without any reduction in AToken balance, violating the expected invariant that AToken burn should correspond to underlying transfer." + } + ], + "events": [ + { + "name": "Transfer", + "parameters": "address indexed from, address indexed to, uint256 amount", + "description": "Emitted when aTokens are transferred (including mint and burn)" + }, + { + "name": "Burn", + "parameters": "address indexed from, address indexed target, uint256 amount, uint256 index", + "description": "Emitted when aTokens are burned" + }, + { + "name": "Mint", + "parameters": "address indexed from, uint256 amount, uint256 index", + "description": "Emitted when aTokens are minted" + }, + { + "name": "BalanceTransfer", + "parameters": "address indexed from, address indexed to, uint256 amount, uint256 index", + "description": "Emitted during token transfers with interest index" + } + ] + }, + { + "contract_name": "StableDebtToken", + "file_name": "StableDebtToken.sol", + "metadata": { + "license": "agpl-3.0", + "solidity_version": "0.6.8", + "description": "Implements a stable debt token to track the user positions", + "author": "Aave" + }, + "state_variables": [ + { + "name": "DEBT_TOKEN_REVISION", + "type": "uint256", + "visibility": "public", + "mutability": "constant", + "description": "Revision number of the stable debt token implementation" + }, + { + "name": "_avgStableRate", + "type": "uint256", + "visibility": "private", + "mutability": "", + "description": "Average stable rate across all stable rate debt" + }, + { + "name": "_timestamps", + "type": "mapping(address => uint40)", + "visibility": "private", + "mutability": "", + "description": "Last update timestamp for each user" + }, + { + "name": "_totalSupplyTimestamp", + "type": "uint40", + "visibility": "private", + "mutability": "", + "description": "Timestamp of the last total supply update" + } + ], + "functions": [ + { + "name": "constructor", + "signature": "constructor(address pool, address underlyingAsset, string memory name, string memory symbol, address incentivesController)", + "code": "constructor(address pool, address underlyingAsset, string memory name, string memory symbol, address incentivesController) public DebtTokenBase(pool, underlyingAsset, name, symbol, incentivesController) {}", + "comment": "Constructor for StableDebtToken", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "pool", + "type": "address", + "description": "LendingPool address" + }, + { + "name": "underlyingAsset", + "type": "address", + "description": "Underlying asset address" + }, + { + "name": "name", + "type": "string", + "description": "Token name" + }, + { + "name": "symbol", + "type": "string", + "description": "Token symbol" + }, + { + "name": "incentivesController", + "type": "address", + "description": "Incentives controller address" + } + ], + "returns": "", + "output_property": "Calls DebtTokenBase constructor to initialize inherited state. No return value.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getRevision", + "signature": "getRevision() internal virtual override pure returns (uint256)", + "code": "function getRevision() internal virtual override pure returns (uint256) { return DEBT_TOKEN_REVISION; }", + "comment": "Returns the revision number", + "visibility": "internal", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Revision number (0x1)", + "output_property": "Pure function returning constant DEBT_TOKEN_REVISION. No state changes.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getAverageStableRate", + "signature": "getAverageStableRate() external virtual override view returns (uint256)", + "code": "function getAverageStableRate() external virtual override view returns (uint256) { return _avgStableRate; }", + "comment": "Returns the average stable rate", + "visibility": "external", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Current average stable rate", + "output_property": "View function returning the _avgStableRate state variable.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getUserLastUpdated", + "signature": "getUserLastUpdated(address user) external virtual override view returns (uint40)", + "code": "function getUserLastUpdated(address user) external virtual override view returns (uint40) { return _timestamps[user]; }", + "comment": "Returns the last update timestamp for a user", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint40 - Last update timestamp", + "output_property": "View function returning the timestamp from _timestamps mapping.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getUserStableRate", + "signature": "getUserStableRate(address user) external virtual override view returns (uint256)", + "code": "function getUserStableRate(address user) external virtual override view returns (uint256) { return _usersData[user]; }", + "comment": "Returns the stable rate of a user", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256 - User's stable rate", + "output_property": "View function returning _usersData[user] (inherited from DebtTokenBase).", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "balanceOf", + "signature": "balanceOf(address account) public virtual override view returns (uint256)", + "code": "function balanceOf(address account) public virtual override view returns (uint256) {\n uint256 accountBalance = super.balanceOf(account);\n uint256 stableRate = _usersData[account];\n if (accountBalance == 0) { return 0; }\n uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest(stableRate, _timestamps[account]);\n return accountBalance.rayMul(cumulatedInterest);\n}", + "comment": "Returns the current user debt balance including accrued interest", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "account", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256 - Current debt balance with interest", + "output_property": "If principal balance is zero, returns zero. Otherwise calculates compounded interest since last update and multiplies. View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "mint", + "signature": "mint(address user, uint256 amount, uint256 rate) external override onlyLendingPool", + "code": "function mint(address user, uint256 amount, uint256 rate) external override onlyLendingPool {\n MintLocalVars memory vars;\n (uint256 previousBalance, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user);\n vars.previousSupply = totalSupply();\n vars.currentAvgStableRate = _avgStableRate;\n vars.nextSupply = _totalSupply = vars.previousSupply.add(amount);\n vars.amountInRay = amount.wadToRay();\n vars.newStableRate = _usersData[user]\n .rayMul(currentBalance.wadToRay())\n .add(vars.amountInRay.rayMul(rate))\n .rayDiv(currentBalance.add(amount).wadToRay());\n require(vars.newStableRate < (1 << 128), 'Debt token: stable rate overflow');\n _usersData[user] = vars.newStableRate;\n _totalSupplyTimestamp = _timestamps[user] = uint40(block.timestamp);\n _avgStableRate = vars.currentAvgStableRate\n .rayMul(vars.previousSupply.wadToRay())\n .add(rate.rayMul(vars.amountInRay))\n .rayDiv(vars.nextSupply.wadToRay());\n _mint(user, amount.add(balanceIncrease), vars.previousSupply);\n emit Transfer(address(0), user, amount);\n emit MintDebt(user, amount, previousBalance, currentBalance, balanceIncrease, vars.newStableRate);\n}", + "comment": "Mints stable debt tokens to a user, updating the weighted average stable rate", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User receiving debt" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount of debt to mint (principal)" + }, + { + "name": "rate", + "type": "uint256", + "description": "Stable rate for the new debt" + } + ], + "returns": "", + "output_property": "Accrues interest, updates user's stable rate (weighted average), updates total supply and average stable rate, mints tokens, emits Transfer and MintDebt. Reverts if not called by LendingPool or if new stable rate overflows 128 bits. Vulnerable to rounding: if amount is very small and conversion rate high, amount.wadToRay() may round down to zero, resulting in minting zero debt tokens while still updating state, potentially allowing user to gain without increasing debt.", + "events": [ + "Transfer", + "MintDebt" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Additive mint (Stable debt token) - Rounding error leads to free debt creation", + "severity": "Medium", + "description": "Due to rounding in conversions to stable debt token (wadToRay), if the conversion rate is high enough, a user can deposit a very small amount that results in the system transferring underlying tokens but minting zero debt tokens to the user's account. This allows the user to receive underlying assets without incurring corresponding debt.", + "mitigation": "Add a check to ensure that the amount after conversion to ray is greater than zero before minting, or avoid minting when the minted amount would be zero." + }, + "property": "When a user borrows (mints debt), their debt balance should increase exactly by the borrowed amount (plus accrued interest). Rounding should not allow a user to receive underlying without a corresponding increase in debt.", + "property_specification": "Pre-condition: User has debt balance D, total supply S. Operation: mint(user, amount, rate) where amount > 0 but amount.wadToRay() == 0 due to rounding. Expected post-condition: User's debt balance increases by amount (scaled appropriately). Actual vulnerability: User's debt balance does not increase (mint of zero), but the LendingPool would transfer underlying to the user, resulting in a net gain without debt increase, violating the invariant that debt minting should correspond to underlying received." + }, + { + "name": "burn", + "signature": "burn(address user, uint256 amount) external override onlyLendingPool", + "code": "function burn(address user, uint256 amount) external override onlyLendingPool {\n (uint256 previousBalance, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user);\n uint256 previousSupply = totalSupply();\n if (previousSupply <= amount) {\n _avgStableRate = 0;\n _totalSupply = 0;\n } else {\n uint256 nextSupply = _totalSupply = previousSupply.sub(amount);\n _avgStableRate = _avgStableRate\n .rayMul(previousSupply.wadToRay())\n .sub(_usersData[user].rayMul(amount.wadToRay()))\n .rayDiv(nextSupply.wadToRay());\n }\n if (amount == currentBalance) {\n _usersData[user] = 0;\n _timestamps[user] = 0;\n } else {\n _timestamps[user] = uint40(block.timestamp);\n }\n _totalSupplyTimestamp = uint40(block.timestamp);\n if (balanceIncrease > amount) {\n _mint(user, balanceIncrease.sub(amount), previousSupply);\n } else {\n _burn(user, amount.sub(balanceIncrease), previousSupply);\n }\n emit Transfer(user, address(0), amount);\n emit BurnDebt(user, amount, previousBalance, currentBalance, balanceIncrease);\n}", + "comment": "Burns stable debt tokens when user repays", + "visibility": "external", + "modifiers": [ + "onlyLendingPool" + ], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User repaying debt" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to repay (principal)" + } + ], + "returns": "", + "output_property": "Accrues interest, updates total supply and average stable rate, updates user's principal and timestamp, burns or mints tokens to adjust for interest, emits Transfer and BurnDebt. Reverts if not called by LendingPool or if insufficient balance.", + "events": [ + "Transfer", + "BurnDebt" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_calculateBalanceIncrease", + "signature": "_calculateBalanceIncrease(address user) internal view returns (uint256, uint256, uint256)", + "code": "function _calculateBalanceIncrease(address user) internal view returns (uint256, uint256, uint256) {\n uint256 previousPrincipalBalance = super.balanceOf(user);\n if (previousPrincipalBalance == 0) { return (0, 0, 0); }\n uint256 balanceIncrease = balanceOf(user).sub(previousPrincipalBalance);\n return (previousPrincipalBalance, previousPrincipalBalance.add(balanceIncrease), balanceIncrease);\n}", + "comment": "Calculates the increase in user debt since last interaction", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256, uint256, uint256 - Previous principal, new principal, balance increase", + "output_property": "View function that computes accrued interest. Returns zeros if principal is zero.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getSupplyData", + "signature": "getSupplyData() public override view returns (uint256, uint256, uint256)", + "code": "function getSupplyData() public override view returns (uint256, uint256, uint256) {\n uint256 avgRate = _avgStableRate;\n return (super.totalSupply(), _calcTotalSupply(avgRate), avgRate);\n}", + "comment": "Returns principal total supply, total supply with interest, and average stable rate", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256, uint256, uint256 - Principal supply, total supply with interest, average rate", + "output_property": "View function returning three values.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getTotalSupplyAndAvgRate", + "signature": "getTotalSupplyAndAvgRate() public override view returns (uint256, uint256)", + "code": "function getTotalSupplyAndAvgRate() public override view returns (uint256, uint256) {\n uint256 avgRate = _avgStableRate;\n return (_calcTotalSupply(avgRate), avgRate);\n}", + "comment": "Returns total supply with interest and average stable rate", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256, uint256 - Total supply with interest, average rate", + "output_property": "View function returning two values.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "totalSupply", + "signature": "totalSupply() public override view returns (uint256)", + "code": "function totalSupply() public override view returns (uint256) {\n return _calcTotalSupply(_avgStableRate);\n}", + "comment": "Returns total supply including accrued interest", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Total supply with interest", + "output_property": "Calls _calcTotalSupply with current average stable rate.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getTotalSupplyLastUpdated", + "signature": "getTotalSupplyLastUpdated() public override view returns (uint40)", + "code": "function getTotalSupplyLastUpdated() public override view returns (uint40) {\n return _totalSupplyTimestamp;\n}", + "comment": "Returns timestamp of last total supply update", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint40 - Last update timestamp", + "output_property": "View function returning _totalSupplyTimestamp.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "principalBalanceOf", + "signature": "principalBalanceOf(address user) external virtual override view returns (uint256)", + "code": "function principalBalanceOf(address user) external virtual override view returns (uint256) {\n return super.balanceOf(user);\n}", + "comment": "Returns principal debt balance without interest", + "visibility": "external", + "modifiers": [], + "parameters": [ + { + "name": "user", + "type": "address", + "description": "User address" + } + ], + "returns": "uint256 - Principal balance", + "output_property": "View function returning inherited balanceOf (principal).", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_calcTotalSupply", + "signature": "_calcTotalSupply(uint256 avgRate) internal view returns (uint256)", + "code": "function _calcTotalSupply(uint256 avgRate) internal view returns (uint256) {\n uint256 principalSupply = super.totalSupply();\n if (principalSupply == 0) { return 0; }\n uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest(avgRate, _totalSupplyTimestamp);\n return principalSupply.rayMul(cumulatedInterest);\n}", + "comment": "Calculates total supply given an average rate", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "avgRate", + "type": "uint256", + "description": "Average stable rate to use" + } + ], + "returns": "uint256 - Total supply with interest", + "output_property": "Returns zero if principal supply is zero, otherwise computes compounded interest and multiplies. View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_mint", + "signature": "_mint(address account, uint256 amount, uint256 oldTotalSupply) internal", + "code": "function _mint(address account, uint256 amount, uint256 oldTotalSupply) internal {\n uint256 oldAccountBalance = _balances[account];\n _balances[account] = oldAccountBalance.add(amount);\n if (address(_incentivesController) != address(0)) {\n _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);\n }\n}", + "comment": "Internal mint function with incentives handling", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "account", + "type": "address", + "description": "Account receiving tokens" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to mint" + }, + { + "name": "oldTotalSupply", + "type": "uint256", + "description": "Total supply before mint" + } + ], + "returns": "", + "output_property": "Increases user balance, notifies incentives controller if present. No return.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_burn", + "signature": "_burn(address account, uint256 amount, uint256 oldTotalSupply) internal", + "code": "function _burn(address account, uint256 amount, uint256 oldTotalSupply) internal {\n uint256 oldAccountBalance = _balances[account];\n _balances[account] = oldAccountBalance.sub(amount, 'ERC20: burn amount exceeds balance');\n if (address(_incentivesController) != address(0)) {\n _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);\n }\n}", + "comment": "Internal burn function with incentives handling", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "account", + "type": "address", + "description": "Account burning tokens" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to burn" + }, + { + "name": "oldTotalSupply", + "type": "uint256", + "description": "Total supply before burn" + } + ], + "returns": "", + "output_property": "Decreases user balance, reverts if insufficient balance, notifies incentives controller.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + } + ], + "structs": [ + { + "name": "MintLocalVars", + "definition": "struct MintLocalVars {\n uint256 previousSupply;\n uint256 nextSupply;\n uint256 amountInRay;\n uint256 newStableRate;\n uint256 currentAvgStableRate;\n}", + "description": "Local variables used in mint function to avoid stack too deep errors" + } + ], + "modifiers": [ + { + "name": "onlyLendingPool", + "definition": "Inherited from DebtTokenBase: require(msg.sender == address(_pool), ...)", + "purpose": "Restricts function access to only the LendingPool contract" + } + ], + "inheritance": [ + "DebtTokenBase", + "IStableDebtToken" + ], + "call_graph": { + "constructor": [ + "DebtTokenBase.constructor()" + ], + "getRevision": [], + "getAverageStableRate": [], + "getUserLastUpdated": [], + "getUserStableRate": [], + "balanceOf": [ + "super.balanceOf()", + "MathUtils.calculateCompoundedInterest()" + ], + "mint": [ + "_calculateBalanceIncrease()", + "totalSupply()", + "_mint()", + "emit Transfer", + "emit MintDebt" + ], + "burn": [ + "_calculateBalanceIncrease()", + "totalSupply()", + "_mint()", + "_burn()", + "emit Transfer", + "emit BurnDebt" + ], + "_calculateBalanceIncrease": [ + "super.balanceOf()", + "balanceOf()" + ], + "getSupplyData": [ + "super.totalSupply()", + "_calcTotalSupply()" + ], + "getTotalSupplyAndAvgRate": [ + "_calcTotalSupply()" + ], + "totalSupply": [ + "_calcTotalSupply()" + ], + "getTotalSupplyLastUpdated": [], + "principalBalanceOf": [ + "super.balanceOf()" + ], + "_calcTotalSupply": [ + "super.totalSupply()", + "MathUtils.calculateCompoundedInterest()" + ], + "_mint": [ + "incentivesController.handleAction()" + ], + "_burn": [ + "incentivesController.handleAction()" + ] + }, + "audit_issues": [ + { + "function": "mint", + "issue": "Additive mint (Stable debt token) - Rounding error leads to free debt creation", + "severity": "Medium", + "description": "Due to rounding in conversions to stable debt token (wadToRay), if the conversion rate is high enough, a user can deposit a very small amount that results in the system transferring underlying tokens but minting zero debt tokens to the user's account. This allows the user to receive underlying assets without incurring corresponding debt.", + "status": "Fixed", + "mitigation": "Add a check to ensure that the amount after conversion to ray is greater than zero before minting, or avoid minting when the minted amount would be zero.", + "property": "When a user borrows (mints debt), their debt balance should increase exactly by the borrowed amount (plus accrued interest). Rounding should not allow a user to receive underlying without a corresponding increase in debt.", + "property_specification": "Pre-condition: User has debt balance D, total supply S. Operation: mint(user, amount, rate) where amount > 0 but amount.wadToRay() == 0 due to rounding. Expected post-condition: User's debt balance increases by amount (scaled appropriately). Actual vulnerability: User's debt balance does not increase (mint of zero), but the LendingPool would transfer underlying to the user, resulting in a net gain without debt increase, violating the invariant that debt minting should correspond to underlying received." + } + ], + "events": [ + { + "name": "Transfer", + "parameters": "address indexed from, address indexed to, uint256 amount", + "description": "Emitted when debt tokens are transferred (mint or burn)" + }, + { + "name": "MintDebt", + "parameters": "address indexed user, uint256 amount, uint256 previousBalance, uint256 currentBalance, uint256 balanceIncrease, uint256 newStableRate", + "description": "Emitted when stable debt is minted" + }, + { + "name": "BurnDebt", + "parameters": "address indexed user, uint256 amount, uint256 previousBalance, uint256 currentBalance, uint256 balanceIncrease", + "description": "Emitted when stable debt is burned" + } + ] + }, + { + "contract_name": "ATokenVault", + "file_name": "ATokenVault_old.sol", + "metadata": { + "license": "MIT", + "solidity_version": "0.8.10", + "description": "An ERC-4626 vault for Aave V3, with support to add a fee on yield earned.", + "author": "Aave Protocol" + }, + "state_variables": [ + { + "name": "POOL_ADDRESSES_PROVIDER", + "type": "IPoolAddressesProvider", + "visibility": "public", + "mutability": "immutable", + "description": "Aave Pool Addresses Provider" + }, + { + "name": "AAVE_POOL", + "type": "IPool", + "visibility": "public", + "mutability": "immutable", + "description": "Aave V3 Pool" + }, + { + "name": "ATOKEN", + "type": "IAToken", + "visibility": "public", + "mutability": "immutable", + "description": "Aave aToken for the underlying asset" + }, + { + "name": "UNDERLYING", + "type": "IERC20Upgradeable", + "visibility": "public", + "mutability": "immutable", + "description": "Underlying ERC20 asset" + }, + { + "name": "REFERRAL_CODE", + "type": "uint16", + "visibility": "public", + "mutability": "immutable", + "description": "Aave referral code" + }, + { + "name": "_sigNonces", + "type": "mapping(address => uint256)", + "visibility": "internal", + "mutability": "", + "description": "Nonces for EIP712 signatures" + }, + { + "name": "_s", + "type": "struct ATokenVaultStorage.VaultStorage", + "visibility": "internal", + "mutability": "", + "description": "Storage struct containing fee, lastVaultBalance, lastUpdated, accumulatedFees" + } + ], + "functions": [ + { + "name": "constructor", + "signature": "constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider)", + "code": "constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider) {\n _disableInitializers();\n POOL_ADDRESSES_PROVIDER = poolAddressesProvider;\n AAVE_POOL = IPool(poolAddressesProvider.getPool());\n REFERRAL_CODE = referralCode;\n UNDERLYING = IERC20Upgradeable(underlying);\n address aTokenAddress = AAVE_POOL.getReserveData(address(underlying)).aTokenAddress;\n require(aTokenAddress != address(0), \"ASSET_NOT_SUPPORTED\");\n ATOKEN = IAToken(aTokenAddress);\n}", + "comment": "Constructor sets immutable references and disables initializers", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "underlying", + "type": "address", + "description": "Underlying ERC20 asset address" + }, + { + "name": "referralCode", + "type": "uint16", + "description": "Aave referral code" + }, + { + "name": "poolAddressesProvider", + "type": "IPoolAddressesProvider", + "description": "Aave Pool Addresses Provider" + } + ], + "returns": "", + "output_property": "Initializes immutable variables, calls _disableInitializers, validates that the underlying asset is supported by Aave. No return value.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "initialize", + "signature": "initialize(address owner, uint256 initialFee, string memory shareName, string memory shareSymbol, uint256 initialLockDeposit) external initializer", + "code": "function initialize(address owner, uint256 initialFee, string memory shareName, string memory shareSymbol, uint256 initialLockDeposit) external initializer {\n require(initialLockDeposit != 0, \"ZERO_INITIAL_LOCK_DEPOSIT\");\n _transferOwnership(owner);\n __ERC4626_init(UNDERLYING);\n __ERC20_init(shareName, shareSymbol);\n __EIP712_init(shareName, \"1\");\n _setFee(initialFee);\n UNDERLYING.safeApprove(address(AAVE_POOL), type(uint256).max);\n _handleDeposit(initialLockDeposit, address(this), msg.sender, false);\n}", + "comment": "Initializes the vault with initial fee, name, symbol, and a lock deposit", + "visibility": "external", + "modifiers": [ + "initializer" + ], + "parameters": [ + { + "name": "owner", + "type": "address", + "description": "Owner address" + }, + { + "name": "initialFee", + "type": "uint256", + "description": "Initial fee (wad, 1e18 = 100%)" + }, + { + "name": "shareName", + "type": "string", + "description": "Vault share token name" + }, + { + "name": "shareSymbol", + "type": "string", + "description": "Vault share token symbol" + }, + { + "name": "initialLockDeposit", + "type": "uint256", + "description": "Initial deposit amount to prevent frontrunning" + } + ], + "returns": "", + "output_property": "Transfers ownership, initializes ERC4626, ERC20, EIP712, sets fee, approves unlimited underlying to Aave Pool, and makes initial deposit. Reverts if initialLockDeposit is zero. Missing check for owner != address(0), which could lock the contract.", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "Missing owner zero address check in initialize", + "severity": "Recommendation", + "description": "The initialize function does not check that owner is not the zero address. Setting owner to address(0) would make all onlyOwner functions unreachable, preventing fee updates, fee withdrawals, reward claims, and emergency rescues.", + "mitigation": "Add require(owner != address(0), 'INVALID_OWNER');" + }, + "property": "The contract must have a non-zero owner to allow administrative functions to be callable.", + "property_specification": "Pre-condition: Owner address can be any address. Operation: initialize(owner, ...) where owner == address(0). Expected post-condition: The contract owner is address(0) and administrative functions are permanently locked. Actual vulnerability: The owner becomes zero address, violating the invariant that owner must be a valid address capable of executing privileged functions." + }, + { + "name": "deposit", + "signature": "deposit(uint256 assets, address receiver) public override returns (uint256 shares)", + "code": "function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {\n shares = _handleDeposit(assets, receiver, msg.sender, false);\n}", + "comment": "Deposits underlying assets into the vault, minting shares", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount of underlying to deposit" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of vault shares" + } + ], + "returns": "uint256 - Amount of shares minted", + "output_property": "Calls _handleDeposit with asAToken=false. Reverts if assets exceed maxDeposit, if rounding yields zero shares, or if supply cap/frozen/active checks fail. May cause state inconsistency due to rounding in lastVaultBalance update.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "When depositing an amount of underlying token that isn't a whole multiple of the liquidity index, the contract may reach a dirty state that keeps reverting undesirably on every method that calls accrueYield(). This occurs due to an inaccurate increment of lastVaultBalance that doesn't correspond to the actual increment or decrement in the vault's assets.", + "mitigation": "Fixed in PR#82 merged in commit 385b397." + }, + "property": "After a deposit, the vault should remain in a consistent state where subsequent operations do not revert due to internal accounting mismatches.", + "property_specification": "Pre-condition: Vault state is consistent, lastVaultBalance equals ATOKEN.balanceOf(vault). Operation: deposit(assets) where assets is not a multiple of liquidity index. Expected post-condition: Vault remains consistent, lastVaultBalance == ATOKEN.balanceOf(vault) after deposit. Actual vulnerability: lastVaultBalance is incremented by assets, but ATOKEN.balanceOf increases by a different amount due to rounding, leading to inconsistency that causes future accrueYield() calls to revert." + }, + { + "name": "depositATokens", + "signature": "depositATokens(uint256 assets, address receiver) public override returns (uint256 shares)", + "code": "function depositATokens(uint256 assets, address receiver) public override returns (uint256 shares) {\n shares = _handleDeposit(assets, receiver, msg.sender, true);\n}", + "comment": "Deposits aTokens directly into the vault", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount of aTokens to deposit" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of vault shares" + } + ], + "returns": "uint256 - Amount of shares minted", + "output_property": "Calls _handleDeposit with asAToken=true. Reverts if assets exceed maxDeposit, which includes supply cap check that is unnecessary for aToken deposits. May incorrectly revert when converting aTokens that represent underlying value exceeding supply cap margin.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "The maxDeposit check in _handleDeposit calls _maxAssetsSuppliableToAave, which enforces supply caps. However, depositing aTokens does not introduce new money to the Aave pool and should not be subject to supply caps. If the converted aTokens' underlying value surpasses the cap margin, an unjust revert occurs.", + "mitigation": "Fixed in PR#80, merged in commit 34ad6e3." + }, + "property": "Depositing aTokens (which already represent Aave positions) should not be limited by the Aave pool's supply cap because it does not increase net supply to the pool.", + "property_specification": "Pre-condition: Aave pool has a supply cap, and the total supplied is near the cap. The vault holds aTokens whose underlying value would push the cap over if deposited as underlying. Operation: depositATokens(assets) with assets amount that converts to underlying value exceeding remaining cap. Expected post-condition: Deposit should succeed because no new underlying is supplied to Aave. Actual vulnerability: The function reverts due to the supply cap check, preventing legitimate aToken deposits." + }, + { + "name": "depositWithSig", + "signature": "depositWithSig(uint256 assets, address receiver, address depositor, EIP712Signature calldata sig) public override returns (uint256 shares)", + "code": "function depositWithSig(...) public override returns (uint256 shares) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(...);\n }\n shares = _handleDeposit(assets, receiver, depositor, false);\n}", + "comment": "Deposit with EIP712 signature for meta-transaction", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount to deposit" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of shares" + }, + { + "name": "depositor", + "type": "address", + "description": "Address of depositor" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature parameters" + } + ], + "returns": "uint256 - Shares minted", + "output_property": "Validates signature, then calls _handleDeposit. Same vulnerability as deposit() for underlying deposits, plus potential signature replay if nonce handling is flawed (but nonces are incremented).", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "Same underlying deposit rounding issue as deposit(). The signature variant inherits the same vulnerability.", + "mitigation": "Fixed in PR#82." + }, + "property": "Same as deposit().", + "property_specification": "Same as deposit()." + }, + { + "name": "depositATokensWithSig", + "signature": "depositATokensWithSig(uint256 assets, address receiver, address depositor, EIP712Signature calldata sig) public override returns (uint256 shares)", + "code": "function depositATokensWithSig(...) public override returns (uint256 shares) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(...);\n }\n shares = _handleDeposit(assets, receiver, depositor, true);\n}", + "comment": "Deposit aTokens with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount of aTokens to deposit" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of shares" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor address" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Shares minted", + "output_property": "Validates signature, then calls _handleDeposit with asAToken=true. Same undesired revert issue as depositATokens.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "Same as depositATokens. The signature variant also incorrectly applies supply cap checks.", + "mitigation": "Fixed in PR#80." + }, + "property": "Same as depositATokens.", + "property_specification": "Same as depositATokens." + }, + { + "name": "mint", + "signature": "mint(uint256 shares, address receiver) public override returns (uint256 assets)", + "code": "function mint(uint256 shares, address receiver) public override returns (uint256 assets) {\n assets = _handleMint(shares, receiver, msg.sender, false);\n}", + "comment": "Mints exactly `shares` vault shares by depositing underlying assets", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Amount of shares to mint" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of shares" + } + ], + "returns": "uint256 - Amount of underlying assets deposited", + "output_property": "Calls _handleMint with asAToken=false. Reverts if shares exceed maxMint, or if rounding issues. Same DoS vulnerability as deposit due to lastVaultBalance rounding.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "Minting via underlying assets triggers the same rounding issue in _baseDeposit where lastVaultBalance is incremented by assets amount that may not match the actual increase in ATOKEN.balanceOf.", + "mitigation": "Fixed in PR#82." + }, + "property": "Same as deposit.", + "property_specification": "Same as deposit." + }, + { + "name": "mintWithATokens", + "signature": "mintWithATokens(uint256 shares, address receiver) public override returns (uint256 assets)", + "code": "function mintWithATokens(uint256 shares, address receiver) public override returns (uint256 assets) {\n assets = _handleMint(shares, receiver, msg.sender, true);\n}", + "comment": "Mints shares by depositing aTokens", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to mint" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + } + ], + "returns": "uint256 - Amount of aTokens deposited", + "output_property": "Calls _handleMint with asAToken=true. Same undesired revert issue as depositATokens due to supply cap check in maxMint.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "The maxMint check depends on _maxAssetsSuppliableToAave, which enforces supply caps inappropriately for aToken deposits.", + "mitigation": "Fixed in PR#80." + }, + "property": "Same as depositATokens.", + "property_specification": "Same as depositATokens." + }, + { + "name": "mintWithSig", + "signature": "mintWithSig(uint256 shares, address receiver, address depositor, EIP712Signature calldata sig) public override returns (uint256 assets)", + "code": "function mintWithSig(...) public override returns (uint256 assets) {\n unchecked { ... }\n assets = _handleMint(shares, receiver, depositor, false);\n}", + "comment": "Mint with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to mint" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor address" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Assets deposited", + "output_property": "Validates signature, then calls _handleMint. Same DoS vulnerability as mint.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "Inherits the rounding issue from mint.", + "mitigation": "Fixed in PR#82." + }, + "property": "Same as mint.", + "property_specification": "Same as mint." + }, + { + "name": "mintWithATokensWithSig", + "signature": "mintWithATokensWithSig(uint256 shares, address receiver, address depositor, EIP712Signature calldata sig) public override returns (uint256 assets)", + "code": "function mintWithATokensWithSig(...) public override returns (uint256 assets) {\n unchecked { ... }\n assets = _handleMint(shares, receiver, depositor, true);\n}", + "comment": "Mint with aTokens using signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to mint" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Assets deposited", + "output_property": "Validates signature, then calls _handleMint with asAToken=true. Same undesired revert issue as mintWithATokens.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "Inherits the supply cap check issue from mintWithATokens.", + "mitigation": "Fixed in PR#80." + }, + "property": "Same as mintWithATokens.", + "property_specification": "Same as mintWithATokens." + }, + { + "name": "withdraw", + "signature": "withdraw(uint256 assets, address receiver, address owner) public override returns (uint256 shares)", + "code": "function withdraw(uint256 assets, address receiver, address owner) public override returns (uint256 shares) {\n shares = _handleWithdraw(assets, receiver, owner, msg.sender, false);\n}", + "comment": "Withdraws underlying assets from vault", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount of underlying to withdraw" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of underlying" + }, + { + "name": "owner", + "type": "address", + "description": "Owner of shares" + } + ], + "returns": "uint256 - Shares burned", + "output_property": "Calls _handleWithdraw. May be affected by fee rounding issues (lastVaultBalance mismatch) and the DoS issue from deposit state inconsistency.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Due to rounding differences between lastVaultBalance update (exact assets) and ATOKEN.balanceOf (rayMath rounding), the vault may reach a state where lastVaultBalance != ATOKEN.balanceOf(vault). This leads to either loss of fees (if lastVaultBalance > actual balance) or overcharging fees (if lastVaultBalance < actual balance). Over many operations, the discrepancy can accumulate.", + "mitigation": "Fixed in PR#86 merged in commit 385b397." + }, + "property": "After any deposit or withdrawal, lastVaultBalance should equal ATOKEN.balanceOf(vault) to ensure accurate fee calculation on future yield.", + "property_specification": "Pre-condition: lastVaultBalance = ATOKEN.balanceOf(vault) = B. Operation: withdraw(assets) where assets is not a multiple of the liquidity index. Expected post-condition: lastVaultBalance' = ATOKEN.balanceOf(vault)' = B - assets_actual. Actual vulnerability: lastVaultBalance is decreased by assets (exact), but ATOKEN.balanceOf decreases by a different amount due to rounding, causing a mismatch that leads to fee miscalculation." + }, + { + "name": "withdrawATokens", + "signature": "withdrawATokens(uint256 assets, address receiver, address owner) public override returns (uint256 shares)", + "code": "function withdrawATokens(uint256 assets, address receiver, address owner) public override returns (uint256 shares) {\n shares = _handleWithdraw(assets, receiver, owner, msg.sender, true);\n}", + "comment": "Withdraws aTokens directly", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Amount of aTokens to withdraw" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of aTokens" + }, + { + "name": "owner", + "type": "address", + "description": "Owner of shares" + } + ], + "returns": "uint256 - Shares burned", + "output_property": "Calls _handleWithdraw with asAToken=true. Same fee rounding vulnerability as withdraw.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Same fee rounding issue as withdraw, because the withdrawal updates lastVaultBalance by the exact assets amount while ATOKEN.balanceOf changes by a rounded amount.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "withdrawWithSig", + "signature": "withdrawWithSig(uint256 assets, address receiver, address owner, EIP712Signature calldata sig) public override returns (uint256 shares)", + "code": "function withdrawWithSig(...) public override returns (uint256 shares) {\n unchecked { ... }\n shares = _handleWithdraw(assets, receiver, owner, owner, false);\n}", + "comment": "Withdraw with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets to withdraw" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Owner" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Shares burned", + "output_property": "Validates signature, then calls _handleWithdraw. Same fee rounding vulnerability.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Inherits fee rounding issue.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "withdrawATokensWithSig", + "signature": "withdrawATokensWithSig(uint256 assets, address receiver, address owner, EIP712Signature calldata sig) public override returns (uint256 shares)", + "code": "function withdrawATokensWithSig(...) public override returns (uint256 shares) {\n unchecked { ... }\n shares = _handleWithdraw(assets, receiver, owner, owner, true);\n}", + "comment": "Withdraw aTokens with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "ATokens to withdraw" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Owner" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Shares burned", + "output_property": "Validates signature, then calls _handleWithdraw with asAToken=true. Same fee rounding vulnerability.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Inherits fee rounding issue.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "redeem", + "signature": "redeem(uint256 shares, address receiver, address owner) public override returns (uint256 assets)", + "code": "function redeem(uint256 shares, address receiver, address owner) public override returns (uint256 assets) {\n assets = _handleRedeem(shares, receiver, owner, msg.sender, false);\n}", + "comment": "Redeems shares for underlying assets", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of underlying" + }, + { + "name": "owner", + "type": "address", + "description": "Owner of shares" + } + ], + "returns": "uint256 - Assets withdrawn", + "output_property": "Calls _handleRedeem. Same fee rounding vulnerability as withdraw.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Redeem calls _baseWithdraw which updates lastVaultBalance, causing same rounding mismatch as withdraw.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "redeemAsATokens", + "signature": "redeemAsATokens(uint256 shares, address receiver, address owner) public override returns (uint256 assets)", + "code": "function redeemAsATokens(uint256 shares, address receiver, address owner) public override returns (uint256 assets) {\n assets = _handleRedeem(shares, receiver, owner, msg.sender, true);\n}", + "comment": "Redeems shares for aTokens", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient of aTokens" + }, + { + "name": "owner", + "type": "address", + "description": "Owner of shares" + } + ], + "returns": "uint256 - ATokens withdrawn", + "output_property": "Calls _handleRedeem with asAToken=true. Same fee rounding vulnerability.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Same as redeem.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "redeemWithSig", + "signature": "redeemWithSig(uint256 shares, address receiver, address owner, EIP712Signature calldata sig) public override returns (uint256 assets)", + "code": "function redeemWithSig(...) public override returns (uint256 assets) {\n unchecked { ... }\n assets = _handleRedeem(shares, receiver, owner, owner, false);\n}", + "comment": "Redeem with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Owner" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - Assets withdrawn", + "output_property": "Validates signature, then calls _handleRedeem. Same fee rounding vulnerability.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Inherits fee rounding issue.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "redeemWithATokensWithSig", + "signature": "redeemWithATokensWithSig(uint256 shares, address receiver, address owner, EIP712Signature calldata sig) public override returns (uint256 assets)", + "code": "function redeemWithATokensWithSig(...) public override returns (uint256 assets) {\n unchecked { ... }\n assets = _handleRedeem(shares, receiver, owner, owner, true);\n}", + "comment": "Redeem aTokens with signature", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Owner" + }, + { + "name": "sig", + "type": "EIP712Signature", + "description": "Signature" + } + ], + "returns": "uint256 - ATokens withdrawn", + "output_property": "Validates signature, then calls _handleRedeem with asAToken=true. Same fee rounding vulnerability.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Inherits fee rounding issue.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "maxDeposit", + "signature": "maxDeposit(address) public view override returns (uint256)", + "code": "function maxDeposit(address) public view override returns (uint256) {\n return _maxAssetsSuppliableToAave();\n}", + "comment": "Maximum amount of underlying that can be deposited", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "", + "type": "address", + "description": "Ignored receiver" + } + ], + "returns": "uint256 - Max deposit amount", + "output_property": "Returns result of _maxAssetsSuppliableToAave, which may revert due to arithmetic underflow in certain conditions (e.g., supply cap calculation). This violates EIP4626 which requires must-not-revert behavior.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "maxMint", + "signature": "maxMint(address) public view override returns (uint256)", + "code": "function maxMint(address) public view override returns (uint256) {\n return _convertToShares(_maxAssetsSuppliableToAave(), MathUpgradeable.Rounding.Down);\n}", + "comment": "Maximum shares that can be minted", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "", + "type": "address", + "description": "Ignored receiver" + } + ], + "returns": "uint256 - Max shares", + "output_property": "Converts max assets to shares. May revert if _maxAssetsSuppliableToAave reverts.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "maxWithdraw", + "signature": "maxWithdraw(address owner) public view override returns (uint256)", + "code": "function maxWithdraw(address owner) public view override returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return maxWithdrawable == 0 ? 0 : maxWithdrawable.min(_convertToAssets(balanceOf(owner), MathUpgradeable.Rounding.Down));\n}", + "comment": "Maximum underlying that can be withdrawn by owner", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "owner", + "type": "address", + "description": "Share owner" + } + ], + "returns": "uint256 - Max withdrawable assets", + "output_property": "Returns the minimum of available liquidity and owner's share value. May revert if _maxAssetsWithdrawableFromAave reverts.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "maxRedeem", + "signature": "maxRedeem(address owner) public view override returns (uint256)", + "code": "function maxRedeem(address owner) public view override returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable, MathUpgradeable.Rounding.Down).min(balanceOf(owner));\n}", + "comment": "Maximum shares that can be redeemed by owner", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "owner", + "type": "address", + "description": "Share owner" + } + ], + "returns": "uint256 - Max redeemable shares", + "output_property": "Converts available liquidity to shares and caps by owner balance. May revert.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "previewDeposit", + "signature": "previewDeposit(uint256 assets) public view override returns (uint256 shares)", + "code": "function previewDeposit(uint256 assets) public view override returns (uint256 shares) {\n shares = _convertToShares(_maxAssetsSuppliableToAave().min(assets), MathUpgradeable.Rounding.Down);\n}", + "comment": "Previews shares for a deposit", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets to deposit" + } + ], + "returns": "uint256 - Shares preview", + "output_property": "Applies supply cap limit, which violates EIP4626 requirement that previewDeposit must not account for maxDeposit limits. May revert.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "previewMint", + "signature": "previewMint(uint256 shares) public view override returns (uint256 assets)", + "code": "function previewMint(uint256 shares) public view override returns (uint256 assets) {\n assets = _convertToAssets(shares, MathUpgradeable.Rounding.Up).min(_maxAssetsSuppliableToAave());\n}", + "comment": "Previews assets needed to mint shares", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to mint" + } + ], + "returns": "uint256 - Assets required", + "output_property": "Applies supply cap limit, violating EIP4626 requirement for previewMint. May revert.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "previewWithdraw", + "signature": "previewWithdraw(uint256 assets) public view override returns (uint256 shares)", + "code": "function previewWithdraw(uint256 assets) public view override returns (uint256 shares) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n shares = maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable.min(assets), MathUpgradeable.Rounding.Up);\n}", + "comment": "Previews shares needed to withdraw assets", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets to withdraw" + } + ], + "returns": "uint256 - Shares preview", + "output_property": "Applies withdrawal limits (available liquidity), violating EIP4626 requirement. May revert.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "previewRedeem", + "signature": "previewRedeem(uint256 shares) public view override returns (uint256 assets)", + "code": "function previewRedeem(uint256 shares) public view override returns (uint256 assets) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n assets = maxWithdrawable == 0 ? 0 : _convertToAssets(shares, MathUpgradeable.Rounding.Down).min(maxWithdrawable);\n}", + "comment": "Previews assets for redeeming shares", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + } + ], + "returns": "uint256 - Assets preview", + "output_property": "Applies withdrawal limits, violating EIP4626 requirement. May revert.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "domainSeparator", + "signature": "domainSeparator() public view override returns (bytes32)", + "code": "function domainSeparator() public view override returns (bytes32) {\n return _domainSeparatorV4();\n}", + "comment": "Returns EIP712 domain separator", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "bytes32 - Domain separator", + "output_property": "Pure view function returning cached domain separator. No reverts.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "setFee", + "signature": "setFee(uint256 newFee) public override onlyOwner", + "code": "function setFee(uint256 newFee) public override onlyOwner {\n _accrueYield();\n _setFee(newFee);\n}", + "comment": "Updates the fee percentage", + "visibility": "public", + "modifiers": [ + "onlyOwner" + ], + "parameters": [ + { + "name": "newFee", + "type": "uint256", + "description": "New fee in wad (1e18 = 100%)" + } + ], + "returns": "", + "output_property": "Accrues yield before setting new fee. May be affected by fee rounding issues in _accrueYield. Reverts if not owner or if newFee > SCALE.", + "events": [ + "FeeUpdated" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "The fee accrual uses lastVaultBalance which may be misaligned with actual balance, causing incorrect fee calculation before fee update.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "withdrawFees", + "signature": "withdrawFees(address to, uint256 amount) public override onlyOwner", + "code": "function withdrawFees(address to, uint256 amount) public override onlyOwner {\n uint256 claimableFees = getClaimableFees();\n require(amount <= claimableFees, \"INSUFFICIENT_FEES\");\n _s.accumulatedFees = uint128(claimableFees - amount);\n _s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)) - amount);\n _s.lastUpdated = uint40(block.timestamp);\n ATOKEN.transfer(to, amount);\n emit FeesWithdrawn(to, amount, _s.lastVaultBalance, _s.accumulatedFees);\n}", + "comment": "Withdraws accumulated fees", + "visibility": "public", + "modifiers": [ + "onlyOwner" + ], + "parameters": [ + { + "name": "to", + "type": "address", + "description": "Recipient of fees" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to withdraw" + } + ], + "returns": "", + "output_property": "Withdraws fees as aTokens. Vulnerable to frontrunning attack where a user can trigger accrual before fee withdrawal to avoid fees on gifts. Also affected by fee rounding issues.", + "events": [ + "FeesWithdrawn" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Frontrun - avoiding fee charges for gifts given to the protocol", + "severity": "Informational", + "description": "A user can front-run withdrawFees() by calling any function that triggers accrueYield() (e.g., depositing dust) in the same block, then gifting aTokens directly to the vault. Since lastUpdated is set to current block, the new yield from the gift is not accounted for when withdrawFees() calculates claimableFees via getClaimableFees(), which uses the stored accumulatedFees instead of recalculating. This allows the user to avoid fee charges on the gifted amount.", + "mitigation": "Fixed in PR#82 merged in commit 385b397." + }, + "property": "All yield generated by the vault, including gifts, should be subject to fee accrual before fees are withdrawn.", + "property_specification": "Pre-condition: Vault has lastUpdated = T (current block), and some user gifts aTokens to the vault in the same block after a transaction that set lastUpdated. Operation: withdrawFees() called after the gift. Expected post-condition: The gift amount is included in new yield and fees are charged on it. Actual vulnerability: getClaimableFees() returns accumulatedFees from before the gift because block.timestamp == lastUpdated, so no new fees are calculated, allowing the gift to bypass fee charges." + }, + { + "name": "claimRewards", + "signature": "claimRewards(address to) public override onlyOwner", + "code": "function claimRewards(address to) public override onlyOwner {\n require(to != address(0), \"CANNOT_CLAIM_TO_ZERO_ADDRESS\");\n address[] memory assets = new address[](1);\n assets[0] = address(ATOKEN);\n (address[] memory rewardsList, uint256[] memory claimedAmounts) = IRewardsController(\n address(IncentivizedERC20(address(ATOKEN)).getIncentivesController())\n ).claimAllRewards(assets, to);\n emit RewardsClaimed(to, rewardsList, claimedAmounts);\n}", + "comment": "Claims Aave rewards for the vault", + "visibility": "public", + "modifiers": [ + "onlyOwner" + ], + "parameters": [ + { + "name": "to", + "type": "address", + "description": "Recipient of rewards" + } + ], + "returns": "", + "output_property": "Claims all rewards for the aToken held by the vault. Reverts if not owner or if to is zero address.", + "events": [ + "RewardsClaimed" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "emergencyRescue", + "signature": "emergencyRescue(address token, address to, uint256 amount) public override onlyOwner", + "code": "function emergencyRescue(address token, address to, uint256 amount) public override onlyOwner {\n require(token != address(ATOKEN), \"CANNOT_RESCUE_ATOKEN\");\n IERC20Upgradeable(token).safeTransfer(to, amount);\n emit EmergencyRescue(token, to, amount);\n}", + "comment": "Rescues accidentally sent ERC20 tokens (not aTokens)", + "visibility": "public", + "modifiers": [ + "onlyOwner" + ], + "parameters": [ + { + "name": "token", + "type": "address", + "description": "Token to rescue" + }, + { + "name": "to", + "type": "address", + "description": "Recipient" + }, + { + "name": "amount", + "type": "uint256", + "description": "Amount to rescue" + } + ], + "returns": "", + "output_property": "Transfers arbitrary ERC20 tokens except the vault's aToken. Reverts if not owner or if token is ATOKEN.", + "events": [ + "EmergencyRescue" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "totalAssets", + "signature": "totalAssets() public view override returns (uint256)", + "code": "function totalAssets() public view override returns (uint256) {\n return ATOKEN.balanceOf(address(this)) - getClaimableFees();\n}", + "comment": "Total assets managed by vault (net of fees)", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Total assets", + "output_property": "May revert due to arithmetic underflow if getClaimableFees > ATOKEN.balanceOf. Also may revert in getClaimableFees if calculations overflow.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getClaimableFees", + "signature": "getClaimableFees() public view override returns (uint256)", + "code": "function getClaimableFees() public view override returns (uint256) {\n if (block.timestamp == _s.lastUpdated) {\n return _s.accumulatedFees;\n } else {\n uint256 newVaultBalance = ATOKEN.balanceOf(address(this));\n uint256 newYield = newVaultBalance - _s.lastVaultBalance;\n uint256 newFees = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);\n return _s.accumulatedFees + newFees;\n }\n}", + "comment": "Returns total claimable fees (accrued + new)", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Claimable fees", + "output_property": "If timestamp unchanged, returns stored accumulatedFees. Otherwise calculates new fees based on yield since last update. May revert due to subtraction underflow if newVaultBalance < _s.lastVaultBalance (possible due to rounding mismatches).", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "The calculation of newYield depends on lastVaultBalance which may be misaligned with actual balance, leading to incorrect fee amounts. Also the frontrun vulnerability described in withdrawFees affects this function because when block.timestamp == lastUpdated, it returns stale accumulatedFees.", + "mitigation": "Fixed in PR#86 and PR#82." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "getSigNonce", + "signature": "getSigNonce(address signer) public view override returns (uint256)", + "code": "function getSigNonce(address signer) public view override returns (uint256) {\n return _sigNonces[signer];\n}", + "comment": "Returns the current nonce for a signer", + "visibility": "public", + "modifiers": [], + "parameters": [ + { + "name": "signer", + "type": "address", + "description": "Signer address" + } + ], + "returns": "uint256 - Nonce", + "output_property": "View function returning nonce. No reverts.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getLastUpdated", + "signature": "getLastUpdated() public view override returns (uint256)", + "code": "function getLastUpdated() public view override returns (uint256) {\n return _s.lastUpdated;\n}", + "comment": "Returns timestamp of last fee accrual", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Last updated timestamp", + "output_property": "View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getLastVaultBalance", + "signature": "getLastVaultBalance() public view override returns (uint256)", + "code": "function getLastVaultBalance() public view override returns (uint256) {\n return _s.lastVaultBalance;\n}", + "comment": "Returns the last recorded vault balance (for fee calculation)", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Last vault balance", + "output_property": "View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "getFee", + "signature": "getFee() public view override returns (uint256)", + "code": "function getFee() public view override returns (uint256) {\n return _s.fee;\n}", + "comment": "Returns current fee percentage", + "visibility": "public", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Fee in wad", + "output_property": "View function.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_setFee", + "signature": "_setFee(uint256 newFee) internal", + "code": "function _setFee(uint256 newFee) internal {\n require(newFee <= SCALE, \"FEE_TOO_HIGH\");\n uint256 oldFee = _s.fee;\n _s.fee = uint64(newFee);\n emit FeeUpdated(oldFee, newFee);\n}", + "comment": "Internal function to set fee", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "newFee", + "type": "uint256", + "description": "New fee" + } + ], + "returns": "", + "output_property": "Updates fee storage, emits event. Reverts if newFee > SCALE.", + "events": [ + "FeeUpdated" + ], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_accrueYield", + "signature": "_accrueYield() internal", + "code": "function _accrueYield() internal {\n if (block.timestamp != _s.lastUpdated) {\n uint256 newVaultBalance = ATOKEN.balanceOf(address(this));\n uint256 newYield = newVaultBalance - _s.lastVaultBalance;\n uint256 newFeesEarned = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);\n _s.accumulatedFees += uint128(newFeesEarned);\n _s.lastVaultBalance = uint128(newVaultBalance);\n _s.lastUpdated = uint40(block.timestamp);\n emit YieldAccrued(newYield, newFeesEarned, newVaultBalance);\n }\n}", + "comment": "Accrues yield and fees since last update", + "visibility": "internal", + "modifiers": [], + "parameters": [], + "returns": "", + "output_property": "Calculates new yield based on lastVaultBalance and current balance. May underflow if newVaultBalance < _s.lastVaultBalance due to rounding mismatches. Also subject to fee rounding errors.", + "events": [ + "YieldAccrued" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "The calculation of newYield uses lastVaultBalance which may not equal ATOKEN.balanceOf(vault) due to rounding discrepancies from deposits/withdrawals. This leads to incorrect fee accrual.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "_handleDeposit", + "signature": "_handleDeposit(uint256 assets, address receiver, address depositor, bool asAToken) internal returns (uint256 shares)", + "code": "function _handleDeposit(...) internal returns (uint256 shares) {\n require(assets <= maxDeposit(receiver), \"DEPOSIT_EXCEEDS_MAX\");\n _accrueYield();\n shares = super.previewDeposit(assets);\n require(shares != 0, \"ZERO_SHARES\");\n _baseDeposit(_convertToAssets(shares, MathUpgradeable.Rounding.Up), shares, depositor, receiver, asAToken);\n}", + "comment": "Internal deposit handler", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets to deposit" + }, + { + "name": "receiver", + "type": "address", + "description": "Share recipient" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor address" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if depositing aTokens" + } + ], + "returns": "uint256 - Shares minted", + "output_property": "Handles deposit logic with max check, accrual, share calculation, and base deposit. Vulnerable to DoS and rounding issues.", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "The maxDeposit check uses supply caps which may cause unnecessary reverts for aToken deposits (asAToken=true). Also the deposit rounding issue originates here.", + "mitigation": "Fixed in PR#80 and PR#82." + }, + "property": "Same as deposit.", + "property_specification": "Same as deposit." + }, + { + "name": "_handleMint", + "signature": "_handleMint(uint256 shares, address receiver, address depositor, bool asAToken) internal returns (uint256 assets)", + "code": "function _handleMint(...) internal returns (uint256 assets) {\n require(shares <= maxMint(receiver), \"MINT_EXCEEDS_MAX\");\n _accrueYield();\n assets = super.previewMint(shares);\n _baseDeposit(assets, shares, depositor, receiver, asAToken);\n}", + "comment": "Internal mint handler", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to mint" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if depositing aTokens" + } + ], + "returns": "uint256 - Assets required", + "output_property": "Handles mint logic. Vulnerable to same issues as _handleDeposit.", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "The maxMint check uses _maxAssetsSuppliableToAave which inappropriately applies supply caps to aToken deposits.", + "mitigation": "Fixed in PR#80." + }, + "property": "Same as mintWithATokens.", + "property_specification": "Same as mintWithATokens." + }, + { + "name": "_handleWithdraw", + "signature": "_handleWithdraw(uint256 assets, address receiver, address owner, address allowanceTarget, bool asAToken) internal returns (uint256 shares)", + "code": "function _handleWithdraw(...) internal returns (uint256 shares) {\n _accrueYield();\n require(assets <= maxWithdraw(owner), \"WITHDRAW_EXCEEDS_MAX\");\n shares = super.previewWithdraw(assets);\n _baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);\n}", + "comment": "Internal withdraw handler", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets to withdraw" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Share owner" + }, + { + "name": "allowanceTarget", + "type": "address", + "description": "Spender if different from owner" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if withdrawing aTokens" + } + ], + "returns": "uint256 - Shares burned", + "output_property": "Handles withdraw logic. Vulnerable to fee rounding issues.", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "The withdraw calls _baseWithdraw which updates lastVaultBalance by exact assets, causing mismatch with ATOKEN.balanceOf update due to rounding.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "_handleRedeem", + "signature": "_handleRedeem(uint256 shares, address receiver, address owner, address allowanceTarget, bool asAToken) internal returns (uint256 assets)", + "code": "function _handleRedeem(...) internal returns (uint256 assets) {\n _accrueYield();\n require(shares <= maxRedeem(owner), \"REDEEM_EXCEEDS_MAX\");\n assets = super.previewRedeem(shares);\n require(assets != 0, \"ZERO_ASSETS\");\n _baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);\n}", + "comment": "Internal redeem handler", + "visibility": "internal", + "modifiers": [], + "parameters": [ + { + "name": "shares", + "type": "uint256", + "description": "Shares to redeem" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "owner", + "type": "address", + "description": "Share owner" + }, + { + "name": "allowanceTarget", + "type": "address", + "description": "Spender" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if redeeming to aTokens" + } + ], + "returns": "uint256 - Assets withdrawn", + "output_property": "Handles redeem logic. Same fee rounding vulnerability as withdraw.", + "events": [], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Same as _handleWithdraw.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "_maxAssetsSuppliableToAave", + "signature": "_maxAssetsSuppliableToAave() internal view returns (uint256)", + "code": "function _maxAssetsSuppliableToAave() internal view returns (uint256) {\n AaveDataTypes.ReserveData memory reserveData = AAVE_POOL.getReserveData(address(UNDERLYING));\n uint256 reserveConfigMap = reserveData.configuration.data;\n uint256 supplyCap = (reserveConfigMap & ~AAVE_SUPPLY_CAP_MASK) >> AAVE_SUPPLY_CAP_BIT_POSITION;\n if ((reserveConfigMap & ~AAVE_ACTIVE_MASK == 0) || (reserveConfigMap & ~AAVE_FROZEN_MASK != 0) || (reserveConfigMap & ~AAVE_PAUSED_MASK != 0)) {\n return 0;\n } else if (supplyCap == 0) {\n return type(uint256).max;\n } else {\n return (supplyCap * 10 ** decimals()) - WadRayMath.rayMul((ATOKEN.scaledTotalSupply() + uint256(reserveData.accruedToTreasury)), reserveData.liquidityIndex);\n }\n}", + "comment": "Calculates max assets that can be supplied to Aave (subject to supply cap)", + "visibility": "internal", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Max suppliable assets", + "output_property": "May revert due to arithmetic underflow in the subtraction. Also incorrectly applied to aToken deposits.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_maxAssetsWithdrawableFromAave", + "signature": "_maxAssetsWithdrawableFromAave() internal view returns (uint256)", + "code": "function _maxAssetsWithdrawableFromAave() internal view returns (uint256) {\n AaveDataTypes.ReserveData memory reserveData = AAVE_POOL.getReserveData(address(UNDERLYING));\n uint256 reserveConfigMap = reserveData.configuration.data;\n if ((reserveConfigMap & ~AAVE_ACTIVE_MASK == 0) || (reserveConfigMap & ~AAVE_PAUSED_MASK != 0)) {\n return 0;\n } else {\n return UNDERLYING.balanceOf(address(ATOKEN));\n }\n}", + "comment": "Calculates max withdrawable assets (available liquidity)", + "visibility": "internal", + "modifiers": [], + "parameters": [], + "returns": "uint256 - Max withdrawable assets", + "output_property": "Returns 0 if reserve inactive or paused, otherwise returns underlying balance of aToken contract.", + "events": [], + "vulnerable": false, + "vulnerability_details": null, + "property": null, + "property_specification": null + }, + { + "name": "_baseDeposit", + "signature": "_baseDeposit(uint256 assets, uint256 shares, address depositor, address receiver, bool asAToken) private", + "code": "function _baseDeposit(...) private {\n if (asAToken) {\n ATOKEN.transferFrom(depositor, address(this), assets);\n } else {\n UNDERLYING.safeTransferFrom(depositor, address(this), assets);\n AAVE_POOL.supply(address(UNDERLYING), assets, address(this), REFERRAL_CODE);\n }\n _s.lastVaultBalance += uint128(assets);\n _mint(receiver, shares);\n emit Deposit(depositor, receiver, assets, shares);\n}", + "comment": "Base deposit logic", + "visibility": "private", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets deposited" + }, + { + "name": "shares", + "type": "uint256", + "description": "Shares minted" + }, + { + "name": "depositor", + "type": "address", + "description": "Depositor" + }, + { + "name": "receiver", + "type": "address", + "description": "Share recipient" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if depositing aTokens" + } + ], + "returns": "", + "output_property": "Transfers assets, updates lastVaultBalance by exact assets amount, mints shares. Vulnerable to rounding mismatch because ATOKEN.balanceOf may increase by a different amount due to rayMath rounding.", + "events": [ + "Deposit" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Adding assets to lastVaultBalance directly while ATOKEN.balanceOf increases by a rounded amount leads to discrepancy.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + }, + { + "name": "_baseWithdraw", + "signature": "_baseWithdraw(uint256 assets, uint256 shares, address owner, address receiver, address allowanceTarget, bool asAToken) private", + "code": "function _baseWithdraw(...) private {\n if (allowanceTarget != owner) {\n _spendAllowance(owner, allowanceTarget, shares);\n }\n _s.lastVaultBalance -= uint128(assets);\n _burn(owner, shares);\n if (asAToken) {\n ATOKEN.transfer(receiver, assets);\n } else {\n AAVE_POOL.withdraw(address(UNDERLYING), assets, receiver);\n }\n emit Withdraw(allowanceTarget, receiver, owner, assets, shares);\n}", + "comment": "Base withdraw logic", + "visibility": "private", + "modifiers": [], + "parameters": [ + { + "name": "assets", + "type": "uint256", + "description": "Assets withdrawn" + }, + { + "name": "shares", + "type": "uint256", + "description": "Shares burned" + }, + { + "name": "owner", + "type": "address", + "description": "Share owner" + }, + { + "name": "receiver", + "type": "address", + "description": "Recipient" + }, + { + "name": "allowanceTarget", + "type": "address", + "description": "Spender" + }, + { + "name": "asAToken", + "type": "bool", + "description": "True if withdrawing aTokens" + } + ], + "returns": "", + "output_property": "Subtracts assets from lastVaultBalance, burns shares, transfers assets out. Same rounding mismatch issue as deposit.", + "events": [ + "Withdraw" + ], + "vulnerable": true, + "vulnerability_details": { + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "Subtracting assets directly from lastVaultBalance while ATOKEN.balanceOf decreases by a different rounded amount leads to discrepancy.", + "mitigation": "Fixed in PR#86." + }, + "property": "Same as withdraw.", + "property_specification": "Same as withdraw." + } + ], + "structs": [ + { + "name": "EIP712Signature", + "definition": "struct EIP712Signature { uint8 v; bytes32 r; bytes32 s; uint256 deadline; }", + "description": "Signature parameters for meta-transactions" + } + ], + "modifiers": [ + { + "name": "onlyOwner", + "definition": "Inherited from OwnableUpgradeable: require(owner() == _msgSender(), 'Ownable: caller is not the owner');", + "purpose": "Restricts function access to contract owner only" + }, + { + "name": "initializer", + "definition": "From OpenZeppelin Initializable: prevents re-initialization", + "purpose": "Ensures initialize is called only once" + } + ], + "inheritance": [ + "ERC4626Upgradeable", + "OwnableUpgradeable", + "EIP712Upgradeable", + "ATokenVaultStorage", + "IATokenVault" + ], + "call_graph": { + "constructor": [], + "initialize": [ + "_transferOwnership", + "__ERC4626_init", + "__ERC20_init", + "__EIP712_init", + "_setFee", + "UNDERLYING.safeApprove", + "_handleDeposit" + ], + "deposit": [ + "_handleDeposit" + ], + "depositATokens": [ + "_handleDeposit" + ], + "depositWithSig": [ + "_handleDeposit" + ], + "depositATokensWithSig": [ + "_handleDeposit" + ], + "mint": [ + "_handleMint" + ], + "mintWithATokens": [ + "_handleMint" + ], + "mintWithSig": [ + "_handleMint" + ], + "mintWithATokensWithSig": [ + "_handleMint" + ], + "withdraw": [ + "_handleWithdraw" + ], + "withdrawATokens": [ + "_handleWithdraw" + ], + "withdrawWithSig": [ + "_handleWithdraw" + ], + "withdrawATokensWithSig": [ + "_handleWithdraw" + ], + "redeem": [ + "_handleRedeem" + ], + "redeemAsATokens": [ + "_handleRedeem" + ], + "redeemWithSig": [ + "_handleRedeem" + ], + "redeemWithATokensWithSig": [ + "_handleRedeem" + ], + "maxDeposit": [ + "_maxAssetsSuppliableToAave" + ], + "maxMint": [ + "_maxAssetsSuppliableToAave", + "_convertToShares" + ], + "maxWithdraw": [ + "_maxAssetsWithdrawableFromAave", + "_convertToAssets" + ], + "maxRedeem": [ + "_maxAssetsWithdrawableFromAave", + "_convertToShares" + ], + "previewDeposit": [ + "_convertToShares", + "_maxAssetsSuppliableToAave" + ], + "previewMint": [ + "_convertToAssets", + "_maxAssetsSuppliableToAave" + ], + "previewWithdraw": [ + "_convertToShares", + "_maxAssetsWithdrawableFromAave" + ], + "previewRedeem": [ + "_convertToAssets", + "_maxAssetsWithdrawableFromAave" + ], + "domainSeparator": [ + "_domainSeparatorV4" + ], + "setFee": [ + "_accrueYield", + "_setFee" + ], + "withdrawFees": [ + "getClaimableFees", + "ATOKEN.transfer" + ], + "claimRewards": [ + "IncentivizedERC20.getIncentivesController", + "IRewardsController.claimAllRewards" + ], + "emergencyRescue": [ + "IERC20Upgradeable.safeTransfer" + ], + "totalAssets": [ + "ATOKEN.balanceOf", + "getClaimableFees" + ], + "getClaimableFees": [ + "ATOKEN.balanceOf" + ], + "getSigNonce": [], + "getLastUpdated": [], + "getLastVaultBalance": [], + "getFee": [], + "_setFee": [], + "_accrueYield": [ + "ATOKEN.balanceOf" + ], + "_handleDeposit": [ + "maxDeposit", + "_accrueYield", + "super.previewDeposit", + "_baseDeposit" + ], + "_handleMint": [ + "maxMint", + "_accrueYield", + "super.previewMint", + "_baseDeposit" + ], + "_handleWithdraw": [ + "_accrueYield", + "maxWithdraw", + "super.previewWithdraw", + "_baseWithdraw" + ], + "_handleRedeem": [ + "_accrueYield", + "maxRedeem", + "super.previewRedeem", + "_baseWithdraw" + ], + "_maxAssetsSuppliableToAave": [ + "AAVE_POOL.getReserveData", + "ATOKEN.scaledTotalSupply", + "WadRayMath.rayMul" + ], + "_maxAssetsWithdrawableFromAave": [ + "AAVE_POOL.getReserveData", + "UNDERLYING.balanceOf" + ], + "_baseDeposit": [ + "ATOKEN.transferFrom", + "UNDERLYING.safeTransferFrom", + "AAVE_POOL.supply", + "_mint" + ], + "_baseWithdraw": [ + "_spendAllowance", + "_burn", + "ATOKEN.transfer", + "AAVE_POOL.withdraw" + ] + }, + "audit_issues": [ + { + "function": "deposit, depositWithSig, mint, mintWithSig, _handleDeposit, _baseDeposit", + "issue": "DoS - incorrect handling when depositing underlying tokens", + "severity": "Medium", + "description": "When depositing an amount of underlying token that isn't a whole multiplication of the liquidity index to the vault, the contract may reach a dirty state that keeps reverting undesirably on every method that calls accrueYield(). This occurs due to an inaccurate increment of lastVaultBalance that doesn't correspond to the actual increment or decrement in the vault's assets.", + "status": "Fixed", + "mitigation": "Fixed in PR#82 merged in commit 385b397.", + "property": "After a deposit, the vault's internal lastVaultBalance must exactly match the actual aToken balance to ensure subsequent operations do not revert.", + "property_specification": "Pre-condition: lastVaultBalance == ATOKEN.balanceOf(vault). Operation: deposit(assets) where assets is not a multiple of the liquidity index. Expected post-condition: lastVaultBalance' == ATOKEN.balanceOf(vault)'. Actual vulnerability: lastVaultBalance is incremented by assets, but ATOKEN.balanceOf increases by a different rounded amount, causing a permanent mismatch that leads to underflow/overflow in future accruals, causing reverts." + }, + { + "function": "depositATokens, depositATokensWithSig, mintWithATokens, mintWithATokensWithSig, _handleDeposit, _handleMint", + "issue": "Undesired revert upon depositing aTokens", + "severity": "Low", + "description": "When users call depositATokens or mintWithATokens, a check is performed to ensure the amount does not surpass maxDeposit/maxMint, which in turn calls _maxAssetsSuppliableToAave. This function enforces supply caps, but depositing aTokens does not introduce new money to the Aave pool and should not be subject to supply caps. If the converted aTokens' underlying value surpasses the cap margin, an unjust revert occurs.", + "status": "Fixed", + "mitigation": "Fixed in PR#80, merged in commit 34ad6e3.", + "property": "Depositing aTokens should always be allowed regardless of Aave supply caps because it does not increase net supply to the pool.", + "property_specification": "Pre-condition: Aave pool has a supply cap and the remaining cap is less than the underlying value of the aTokens being deposited. Operation: depositATokens(assets). Expected post-condition: The deposit succeeds and the user receives vault shares. Actual vulnerability: The function reverts due to the supply cap check, preventing legitimate aToken deposits." + }, + { + "function": "withdraw, withdrawATokens, withdrawWithSig, withdrawATokensWithSig, redeem, redeemAsATokens, redeemWithSig, redeemWithATokensWithSig, setFee, withdrawFees, getClaimableFees, _accrueYield, _baseDeposit, _baseWithdraw, _handleDeposit, _handleMint, _handleWithdraw, _handleRedeem", + "issue": "Loss of fees, overcharge of fees due to rounding", + "severity": "Low", + "description": "The storage variable _s.lastVaultBalance marks the portion of reserves for which fees have already been charged. In every call to accrueYield(), the vault charges fees only from the new yield since the last fee charge: ATOKEN.balanceOf(Vault) - _s.lastVaultBalance. However, the system may reach a mismatch between the two values when depositing to or withdrawing from the vault due to different update mechanisms. While _s.lastVaultBalance is updated with the exact assets amount passed to the function, aToken uses rayMath to update its balance. At the end of a deposit() or withdraw(), the vault may reach a state where _s.lastVaultBalance == ATOKEN.balanceOf(Vault) ± 1. Over many operations, this discrepancy accumulates, leading to either loss of fees (if lastVaultBalance > actual) or overcharging fees (if lastVaultBalance < actual).", + "status": "Fixed", + "mitigation": "Fixed in PR#86 merged in commit 385b397.", + "property": "At any time, the vault's lastVaultBalance should be exactly equal to ATOKEN.balanceOf(vault) to ensure accurate fee calculation.", + "property_specification": "Pre-condition: lastVaultBalance = ATOKEN.balanceOf(vault). Operation: deposit(assets) or withdraw(assets) where assets is not a multiple of the liquidity index. Expected post-condition: lastVaultBalance' = ATOKEN.balanceOf(vault)'. Actual vulnerability: lastVaultBalance changes by exactly assets, but ATOKEN.balanceOf changes by a rounded amount (assets ± rounding error), causing a persistent mismatch that accumulates over time, leading to fee miscalculation." + }, + { + "function": "initialize", + "issue": "Missing owner zero address check", + "severity": "Recommendation", + "description": "The initialize function does not check that owner ≠ address(0). Allowing the owner to be address(0) will result in all onlyOwner functions being unreachable, preventing fee updates, fee withdrawals, reward claims, and emergency rescues.", + "status": "Fixed", + "mitigation": "Fixed in PR#71, merged in commit 3927afd.", + "property": "The contract owner must be a non-zero address to ensure administrative functions are accessible.", + "property_specification": "Pre-condition: None. Operation: initialize(owner = address(0), ...). Expected post-condition: The contract owner is a non-zero address that can execute onlyOwner functions. Actual vulnerability: The owner becomes address(0), and all onlyOwner functions become permanently locked, violating the invariant that administrative functions must be callable by a valid owner." + }, + { + "function": "withdrawFees", + "issue": "Frontrun - avoiding fee charges for gifts given to the protocol", + "severity": "Informational", + "description": "A user can front-run withdrawFees() by calling any function that triggers accrueYield() (e.g., depositing dust) in the same block, then gifting aTokens directly to the vault. Since lastUpdated is set to current block, the new yield from the gift is not accounted for when withdrawFees() calculates claimableFees via getClaimableFees(), which uses the stored accumulatedFees instead of recalculating. This allows the user to avoid fee charges on the gifted amount.", + "status": "Fixed", + "mitigation": "Fixed in PR#82 merged in commit 385b397.", + "property": "All yield generated by the vault, including gifts, should be subject to fee accrual before fees are withdrawn.", + "property_specification": "Pre-condition: lastUpdated = current block after a user transaction. A gift of aTokens is sent to the vault in the same block. Operation: withdrawFees() called after the gift. Expected post-condition: The gift amount is included in newYield and fees are charged accordingly. Actual vulnerability: getClaimableFees() returns accumulatedFees from before the gift because block.timestamp == lastUpdated, so no new fees are calculated, allowing the gift to bypass fee charges." + } + ], + "events": [ + { + "name": "Deposit", + "parameters": "address indexed caller, address indexed owner, uint256 assets, uint256 shares", + "description": "Emitted when assets are deposited into the vault" + }, + { + "name": "Withdraw", + "parameters": "address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares", + "description": "Emitted when assets are withdrawn from the vault" + }, + { + "name": "FeeUpdated", + "parameters": "uint256 oldFee, uint256 newFee", + "description": "Emitted when the fee percentage is updated" + }, + { + "name": "FeesWithdrawn", + "parameters": "address indexed to, uint256 amount, uint256 lastVaultBalance, uint256 accumulatedFees", + "description": "Emitted when fees are withdrawn" + }, + { + "name": "RewardsClaimed", + "parameters": "address indexed to, address[] rewardsList, uint256[] claimedAmounts", + "description": "Emitted when Aave rewards are claimed" + }, + { + "name": "EmergencyRescue", + "parameters": "address indexed token, address indexed to, uint256 amount", + "description": "Emitted when tokens are rescued by owner" + }, + { + "name": "YieldAccrued", + "parameters": "uint256 newYield, uint256 newFeesEarned, uint256 newVaultBalance", + "description": "Emitted when yield is accrued and fees calculated" + } + ] } ] \ No newline at end of file